【问题标题】:Suppress matplotlib figures when running .py files via python or ipython terminal通过 python 或 ipython 终端运行 .py 文件时抑制 matplotlib 图形
【发布时间】:2014-08-09 05:23:20
【问题描述】:

我正在写一个test_examples.py 来测试python 示例文件夹的执行情况。目前我使用glob解析文件夹,然后使用subprocess执行每个python文件。问题是其中一些文件是绘图,它们会打开一个 Figure 窗口,该窗口会在窗口关闭之前停止。

很多关于这个问题的问题都提供了文件内的解决方案,但是如何在不进行任何修改的情况下在外部运行文件时抑制输出?

到目前为止我所做的是:

import subprocess as sb
import glob
from nose import with_setup

def test_execute():
    files = glob.glob("../*.py")
    files.sort()
    for fl in files:
        try:
            sb.call(["ipython", "--matplotlib=Qt4", fl])
        except:
            assert False, "File: %s ran with some errors\n" % (fl)

这种方法有效,因为它抑制了图形,但它不会抛出任何异常(即使程序有错误)。我也不是 100% 确定它在做什么。是将所有图形附加到 Qt4 还是在该脚本完成后将图形从内存中删除?

理想情况下,我希望运行每个.py 文件并捕获其stdoutstderr,然后使用退出条件报告stderr 并使测试失败。然后当我运行nosetests 时,它将运行程序的示例文件夹并检查它们是否都运行。

【问题讨论】:

    标签: python unit-testing matplotlib ipython nose


    【解决方案1】:

    您可以通过在每个源文件的顶部插入以下行来强制 matplotlib 使用 Agg 后端(不会打开任何窗口):

    import matplotlib
    matplotlib.use('Agg')
    

    这是一个单行 shell 命令,它将动态my_script.py 的顶部插入这些行(不修改磁盘上的文件),然后将输出传送到 Python 解释器执行:

    ~$ sed "1i import matplotlib\nmatplotlib.use('Agg')\n" my_script.py | python
    

    您应该能够使用subprocess 进行等效调用,如下所示:

    p1 = sb.Popen(["sed", "1i import matplotlib\nmatplotlib.use('Agg')\n", fl],
                  stdout=sb.PIPE)
    exit_cond = sb.call(["python"], stdin=p1.stdout)
    

    您可以通过将stdout=stderr= 参数传递给sb.call(),从脚本中捕获stderrstdout。当然,这仅适用于具有 sed 实用程序的 Unix 环境。


    更新

    这实际上是一个非常有趣的问题。我想了更多,我认为这是一个更优雅的解决方案(尽管仍然有点 hack):

    #!/usr/bin/python
    
    import sys
    import os
    import glob
    from contextlib import contextmanager
    import traceback
    
    set_backend = "import matplotlib\nmatplotlib.use('Agg')\n"
    
    @contextmanager
    def redirected_output(new_stdout=None, new_stderr=None):
        save_stdout = sys.stdout
        save_stderr = sys.stderr
        if new_stdout is not None:
            sys.stdout = new_stdout
        if new_stderr is not None:
            sys.stderr = new_stderr
        try:
            yield None
        finally:
            sys.stdout = save_stdout
            sys.stderr = save_stderr
    
    def run_exectests(test_dir, log_path='exectests.log'):
    
        test_files = glob.glob(os.path.join(test_dir, '*.py'))
        test_files.sort()
        passed = []
        failed = []
        with open(log_path, 'w') as f:
            with redirected_output(new_stdout=f, new_stderr=f):
                for fname in test_files:
                    print(">> Executing '%s'" % fname)
                    try:
                        code = compile(set_backend + open(fname, 'r').read(),
                                       fname, 'exec')
                        exec(code, {'__name__':'__main__'}, {})
                        passed.append(fname)
                    except:
                        traceback.print_exc()
                        failed.append(fname)
                        pass
    
        print ">> Passed %i/%i tests: " %(len(passed), len(test_files))
        print "Passed: " + ', '.join(passed)
        print "Failed: " + ', '.join(failed)
        print "See %s for details" % log_path
    
        return passed, failed
    
    if __name__ == '__main__':
        run_exectests(*sys.argv[1:])
    

    从概念上讲,这与我之前的解决方案非常相似——它通过将测试脚本作为字符串读取,并在它们前面加上几行将导入 matplotlib 并将后端设置为非交互式的。然后将该字符串编译为 Python 字节码,然后执行。主要优点是它应该独立于平台,因为不需要sed

    如果您像我一样倾向于这样编写脚本,则需要使用带有全局变量的 {'__name__':'__main__'} 技巧:

        def run_me():
            ...
        if __name__ == '__main__':
            run_me()
    

    需要考虑的几点:

    • 如果您尝试从已导入 matplotlib 并设置交互式后端的 ipython 会话中运行此函数,set_backend 技巧将不起作用,您仍会弹出数字。最简单的方法是直接从 shell (~$ python exectests.py testdir/ logfile.log) 或从没有为 matplotlib 设置交互式后端的 (i)python 会话运行它。如果您在 ipython 会话中的不同子进程中运行它,它也应该可以工作。
    • 我正在使用来自this answercontextmanager 技巧将stdinstdout 重定向到日志文件。请注意,这不是线程安全的,但我认为脚本打开子进程是很不寻常的。

    【讨论】:

    • 感谢这些巧妙的想法。我已经为库中嵌入的函数编写了鼻子测试。这是添加到运行示例程序的测试套件中的运行检查,以防 API 发生更改等,这将在此测试中被标记
    • @sanguineturtle 好的,这对我来说更有意义。我想我想出了一个更好的解决方案 - 请参阅我的更新。
    【解决方案2】:

    这么晚了,但我自己也想弄清楚类似的东西,这就是我到目前为止想出的。基本上,如果您的绘图正在调用,例如,matplotlib.pyplot.show 来显示绘图,您可以使用patch decorator mock 该方法输出。比如:

    from unittest.mock import patch
    
    @patch('matplotlib.pyplot.show')  # passes a mock object to the decorated function
    def test_execute(mock_show):
        assert mock_show() == None  # shouldn't do anything
        files = glob.glob("../*.py")
        files.sort()
        for fl in files:
            try:
                sb.call(["ipython", fl])
            except:
                assert False, "File: %s ran with some errors\n" % (fl)
    

    基本上,补丁装饰器应该用不做任何事情的模拟对象替换装饰函数中对matplotlib.pyplot.show 的任何调用。至少理论上它应该是这样工作的。在我的应用程序中,我的终端仍在尝试打开绘图,这会导致错误。我希望它对您有更好的效果,如果我发现上面有问题导致我的问题,我会更新。

    编辑:为了完整起见,您可能会通过调用 matplotlib.pyplot.figure()matplotlib.pyplot.subplots() 来生成图形,在这种情况下,您将模拟这些图形而不是 matplotlib.pyplot.show()。与上面的语法相同,您只需使用:

    @patch('matplotlib.pyplot.figure')
    

    或:

    @patch('matplotlib.pyplot.subplots')
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-09-12
      • 2021-11-13
      • 2022-08-11
      • 2016-07-16
      • 1970-01-01
      • 2022-10-25
      • 2020-08-28
      • 2018-12-16
      相关资源
      最近更新 更多