如何确保 pytest 正确关闭 PIL.tkImage 对象

How to make sure pytest properly close PIL.tkImage object

我有相当大的 tkinter GUI。有一些 matplotlib 图表,还有一个 .png 方案,应该在框架中不断显示。据我所知,必须有一个对 PIL.TkImage 对象的引用,以便在关闭构造函数对象后保持对象存活。

import tkinter as tk
from tkinter import ttk 
from PIL import ImageTk, Image

class View(tk.Tk):
    def __init__(self)
        ...
        self.image_cons()
        # Prevents keeping unfinished processes when quiting app by 'X' button,
        # caused by matplotlib
        self.protocol("WM_DELETE_WINDOW", self.quit)
    ...
    def image_cons(self):
        # there are several Frames in between
        self.scheme = Image.open("./images/scheme.png")
        self.scheme.thumbnail((420, 610))

        self.img = ImageTk.PhotoImage(self.scheme)
        label = ttk.Label(self.drawframe, image=self.img)
        label.pack(expand=True, fill=tk.Y)

    def quit(self):
        # Makes sure matplotlib plots are close with closing tkinter
        self.destroy()
        tk.Tk.quit(self)

目前效果很好。我可以 运行 应用程序(通过 Pycharm 运行 或通过命令行)一次又一次,没有任何麻烦。但是。

我有几个 pytest 测试,在其中创建了视图实例(每个测试一个)。添加相当多的代码行(包括方案)后,这些测试开始 return 失败并显示消息(数量正在增加):

E       _tkinter.TclError: image "pyimage49" doesn't exist

所以我进行了搜索。此消息在不同情况下出现,但据我所知,在大多数情况下,以前的应用程序 运行 没有正确关闭图像文件。这一点我之前可能应该弄清楚 - 我有意制作此 self.img 引用以防止在应用程序结束前破坏图像。当我对 with statement

进行一项测试时
def test_A(self)
    with view_module.View(self.VARIABLES, self.DEFAULT_VALUES) as view:
        test_content()

def test_B(self)
    view = view_module.View(self.VARIABLES, self.DEFAULT_VALUES)
    ...

并将这些方法添加到视图 class:

def __enter__(self):
    return self

def __exit__(self, exc_type, exc_val, exc_tb):
    self.quit()

证实了我的结论。在使用 with 语句之前,test_A 是通过的,test_B 是第一个因错误而失败的测试。在使用 with 之后,两者都通过了,并且第一次失败进一步显示 - 在下一个实例化 View.

的测试中

我认为添加

    def __del__(self):
        self.img.destroy()
        self.scheme.destroy()
        self.destroy()
        tk.Tk.__del__(self)

结案。但这似乎不起作用 - 错误仍然出现。有什么方法可以确保当 pytest 用 View 实例完成测试时会触发 destroy() 方法?我假设在每个使用 View 的测试中编写 with 语句不是处理这个问题的 pythonic 方式。

编辑:测试配置如下:

class TestView:
    def test_A(self)
        with view_module.View(self.VARIABLES, self.DEFAULT_VALUES) as view:
            test_content()

    def test_B(self)
        view = view_module.View(self.VARIABLES, self.DEFAULT_VALUES)
        ...

    def test_C(self)
        view = view_module.View(self.VARIABLES, self.DEFAULT_VALUES)
        ...

这是@jasonharper 在评论中建议的原因吗?那是因为 pytest 在所有测试中都持有对每个视图变量的引用,而垃圾收集器不会删除它吗?我能用它做什么,而不是用 with 语句编写每个测试?是否有任何 pytest 配置,即覆盖每个视图?

Is this causing, what @jasonharper suggested in comment?

是的,这是最可能的原因。

Is that because pytest holds reference to each view variable through all tests, and garbage collector doesn't delete it?

没有。这是不一样的东西。

如前所述,当 tk.Tk() 的多个实例被创建时,tkinter 将 tk.Tk() 的第一个实例存储在 tk._default_root 中,直到 destory() .

现在,当在没有 parent/master 的情况下调用 ImageTk.PhotoImage() 时,获取默认父级,即 tk._default_root。请注意 tk._default_root 仍然是 tk.Tk() 的第一个实例。从 tkinter 的角度来看,在一个根 (tk.Tk()) 上创建的任何对象都不能从另一个根访问。

因此,调用 tk.Tk() 的下一个测试用例无法访问图像并抛出错误。

What can I do with it, instead of write every test with with statement? Is there any configuration of pytest which i.e. overwrite each view?

有两种选择。 建议同时使用两者以避免任何此类问题。

选项 1:将 master 添加到 ImageTk.PhotoImage()

class 查看(tk.Tk):

   ...
   ...
   ...

   def image_cons(self):
       ...
       ...
       self.img = ImageTk.PhotoImage(self.scheme, master=self)
       ...
       ...
选项 2:在每个测试用例结束时调用 view.destroy()
class TestView:
    def test_A(self)
        view = view_module.View(self.VARIABLES, self.DEFAULT_VALUES)
        ...
        ...
        view.destroy()

    def test_B(self)
        view = view_module.View(self.VARIABLES, self.DEFAULT_VALUES)
        ...
        ...
        view.destroy()

    def test_C(self)
        view = view_module.View(self.VARIABLES, self.DEFAULT_VALUES)
        ...
        ...
        view.destroy()
奖金

为了保持图像的引用,图像变量可以直接添加到对象本身。以这种方式可以避免创建多个实例级变量。该图像的生命将与其所属对象的生命相关联。

    img = ImageTk.PhotoImage(self.scheme)
    label = ttk.Label(self.drawframe, image=self.img)
    label.img = img
    label.pack(expand=True, fill=tk.Y)