通过python或ipython终端运行.py文件时,抑制matplotlib数字

时间:2014-08-09 05:23:20

标签: python unit-testing matplotlib ipython nose

我正在编写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时,它将运行程序的examples文件夹并检查它们是否全部运行。

2 个答案:

答案 0 :(得分:2)

您可以通过在每个源文件的顶部插入以下行来强制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)

您可以通过将stderrstdout参数传递给stdout=来捕获脚本中的stderr=sb.call()。当然,这只适用于具有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

如果像我一样,你倾向于像这样编写脚本,那么使用全局变量的{'__name__':'__main__'}技巧是必要的:

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

要考虑几点:

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

答案 1 :(得分:0)

到现在为止,但是我想自己弄清楚类似的事情,这就是我到目前为止提出的。基本上,如果您的绘图正在调用matplotlib.pyplot.show以显示该绘图,则可以使用mockpatch decorator来显示该方法。像这样:

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()来生成图形,在这种情况下,您将模拟出这些图形而不是{{ 1}}。与上面相同的语法,您只需要使用:

matplotlib.pyplot.show()

或:

@patch('matplotlib.pyplot.figure')