通过Python或IPython终端运行.py文件时,如何抑制Matplotlib图形输出?

3
我正在编写一个名为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)

这种方法可以屏蔽图形,但即使程序出现错误也不会抛出任何异常。我也不确定它在做什么。它是将所有图形附加到Qt4中还是当该脚本完成时Figure将从内存中移除?
理想情况下,我想运行每个.py文件并捕获其stdout和stderr,然后使用退出条件来报告stderr并失败测试。然后当我运行nosetests时,它将运行程序的examples文件夹并检查它们是否都运行正常。
2个回答

3
您可以在每个源文件的顶部插入以下行,强制 matplotlib 使用“Agg”后端(不会打开任何窗口):
import matplotlib
matplotlib.use('Agg')

这是一个一行命令的shell指令,它会在将输出流传输到Python解释器执行之前,在my_script.py文件的顶部动态地插入这些行,而不会修改磁盘上的文件。
~$ 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环境。

更新

这实际上是一个非常有趣的问题。我想了一下,我认为这是一种更优雅的解决方案(虽然仍然有点像黑客):

#!/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。

使用globals和{'__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会话内的不同子进程中运行它,它也应该可以工作。
  • 我使用这个答案中的“contextmanager”技巧将stdin和stdout重定向到日志文件。请注意,这不是线程安全的,但我认为脚本打开子进程相当少见。

感谢这些好主意。我已经为库中嵌入的函数编写了nose测试。这是一个运行检查,添加到测试套件中,如果API发生更改等情况,将运行示例程序并在此测试中被标记。 - sanguineturtle
@sanguineturtle 好的,我现在更明白了。我想我已经想出了一个稍微更好的解决方案 - 请看我的更新。 - ali_m

0
来晚了,但我正在尝试自己解决类似的问题,这是我迄今为止想出的。基本上,如果您的图调用例如matplotlib.pyplot.show显示图,则可以使用patch装饰器将该方法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')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接