繁体   English   中英

如何确保 pytest 正确关闭 PIL.tkImage object

[英]How to make sure pytest properly close PIL.tkImage object

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

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 测试,其中创建了 View 实例(每个测试一个)。 在添加了相当多的代码行(包括方案)之后,这些测试开始返回失败并显示消息(数量正在增加):

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

所以我做了搜索。 此消息出现在不同的情况下,但我的理解是,在大多数情况下,以前的应用程序运行没有正确关闭图像文件。 这一点我之前可能应该弄清楚——我故意做了这个self.img参考,以防止在应用程序结束之前破坏图像。 当我对 with 语句进行一项测试时

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语句不是处理这个问题的 Python 方式。

编辑:测试配置如下:

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 是否有任何配置,即覆盖每个视图?

这是造成@jasonharper 在评论中建议的原因吗?

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

那是因为 pytest 通过所有测试都持有对每个视图变量的引用,并且垃圾收集器不会删除它吗?

不,这是不同的。

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

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

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

我能用它做什么,而不是用 with 语句编写每个测试? pytest 是否有任何配置,即覆盖每个视图?

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

选项 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()
奖金

为了保持图像的引用,可以在 object 本身中直接添加图像变量。 这样可以避免创建多个实例级变量。 该图像的生命将与其拥有的 object 的生命相关联。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM