Python: Как перехватить стандартный вывод stdout/stderr в переменной?
Как записать stdout/stderr юнит-теста в переменную? Мне нужно захватить весь выходной результат следующего модульного теста и отправить его в SQS. Я попробовал это:
import unittest, io from contextlib import redirect_stdout, redirect_stderr class LogProcessorTests(unittest.TestCase): def setUp(self): self.var = 'this value' def test_var_value(self): with io.StringIO() as buf, redirect_stderr(buf): print('Running LogProcessor tests. ') print('Inside test_var_value') self.assertEqual(self.var, 'that value') print('-----------------------') print(buf.getvalue())
Однако это не работает, и следующий вывод появляется только в stdout/stderr.
Testing started at 20:32 . /Users/myuser/Documents/virtualenvs/app-venv3/bin/python3 "/Applications/PyCharm CE.app/Contents/helpers/pycharm/_jb_unittest_runner.py" --path /Users/myuser/Documents/projects/application/LogProcessor/tests/test_processor_tests.py Launching unittests with arguments python -m unittest /Users/myuser/Documents/projects/application/LogProcessor/tests/test_processor_tests.py in /Users/myuser/Documents/projects/application/LogProcessor/tests Running LogProcessor tests. Inside test_var_value that value != this value Expected :this value Actual :that value Traceback (most recent call last): File "/Applications/PyCharm CE.app/Contents/helpers/pycharm/teamcity/diff_tools.py", line 32, in _patched_equals old(self, first, second, msg) File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 839, in assertEqual assertion_func(first, second, msg=msg) File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 1220, in assertMultiLineEqual self.fail(self._formatMessage(msg, standardMsg)) File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 680, in fail raise self.failureException(msg) AssertionError: 'this value' != 'that value' - this value ? ^^ + that value ? ^^ During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 59, in testPartExecutor yield File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 615, in run testMethod() File "/Users/myuser/Documents/projects/application/LogProcessor/tests/test_processor_tests.py", line 15, in test_var_value self.assertEqual(self.var, 'that value') Ran 1 test in 0.004s FAILED (failures=1) Process finished with exit code 1 Assertion failed Assertion failed
Любая идея? Пожалуйста, дайте мне знать, если вам нужна дополнительная информация.
2 ответа
Если вы вручную создаете тестовый запуск (например, unittest.TextTestRunner ), вы можете указать (файл) поток, в который он пишет. По умолчанию это sys.stderr , но вы можете использовать вместо StringIO. Это будет захватывать результаты самого unittest. Вывод ваших собственных операторов печати не будет записан, но вы можете использовать redirect_stdout контекстный менеджер для этого, использующий тот же объект StringIO.
Обратите внимание, что я бы рекомендовал избегать использования операторов print, так как они будут мешать выводу инфраструктуры unittest (ваш тестовый вывод сломает выходные строки инфраструктуры unittest), и перенаправление stdout/stderr — это хакерство потоки. Лучшим решением было бы использовать logging модуль вместо. Затем вы можете добавить обработчик регистрации, который записывает все сообщения журнала в StringIO для дальнейшей обработки (в вашем случае: отправка в SQS).
Ниже приведен пример кода на основе вашего кода с использованием print-операторов.
#!/usr/bin/env python3 import contextlib import io import unittest class LogProcessorTests(unittest.TestCase): def setUp(self): self.var = 'this value' def test_var_value(self): print('Running LogProcessor tests. ') print('Inside test_var_value') self.assertEqual(self.var, 'that value') print('-----------------------') if __name__ == '__main__': # find all tests in this module import __main__ suite = unittest.TestLoader().loadTestsFromModule(__main__) with io.StringIO() as buf: # run the tests with contextlib.redirect_stdout(buf): unittest.TextTestRunner(stream=buf).run(suite) # process (in this case: print) the results print('*** CAPTURED TEXT***:\n%s' % buf.getvalue())
*** CAPTURED TEXT***: Running LogProcessor tests. Inside test_var_value F ====================================================================== FAIL: test_var_value (__main__.LogProcessorTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 16, in test_var_value self.assertEqual(self.var, 'that value') AssertionError: 'this value' != 'that value' - this value ? ^^ + that value ? ^^ ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)
Это подтверждает, что все выходные данные (из среды unittest и самого тестового примера) были захвачены в объекте StringIO.
Перехватывать выражение `print` и отображать его в графическом интерфейсе Python
У меня есть несколько сложная функция командной строки в Python (давайте назовем ее myFunction() ), и я работаю над ее интеграцией в графический интерфейс (используя PySide / Qt).
Графический интерфейс используется для выбора входов и отображения выходов. Тем не менее, myFunction предназначен для работы в качестве автономной функции командной строки и время от времени выводит информацию о ходе выполнения.
У меня вопрос: как я могу перехватить эти print вызовы и отобразить их в графическом интерфейсе? Я знаю, что можно было бы изменить myFunction() для отправки processEvents() в графический интерфейс, но я бы тогда потерял способность выполнять myFunction() в терминале.
В идеале мне хотелось бы что-то похожее на графический программный модуль обновления Ubuntu, который имеет небольшой встроенный виджет терминала, отображающий то, что apt-get будет отображать, если бы он выполнялся в терминале.
4 ответа
Вы можете перенаправить стандартный вывод и восстановить после. например:
import StringIO import sys # somewhere to store output out = StringIO.StringIO() # set stdout to our StringIO instance sys.stdout = out # print something (nothing will print) print 'herp derp' # restore stdout so we can really print (__stdout__ stores the original stdout) sys.stdout = sys.__stdout__ # print the stored value from previous print print out.getvalue()
Оберните его функцией, которая захватывает стандартный вывод:
def stdin2file(func, file): def innerfunc(*args, **kwargs): old = sys.stdout sys.stdout = file try: return func(*args, **kwargs) finally: sys.stdout = old return innerfunc
Затем просто предоставьте файл, подобный объекту, который поддерживает write() :
class GUIWriter: def write(self, stuff): #send stuff to GUI MyFunction = stdin2file(MyFunction, GUIWriter())
Обертку тоже можно превратить в декоратор:
def redirect_stdin(file): def stdin2file(func, file): def innerfunc(*args, **kwargs): old = sys.stdout sys.stdout = file try: return func(*args, **kwargs) finally: sys.stdout = old return innerfunc return stdin2file
Используйте его при объявлении MyFunction() :
@redirect_stdin(GUIWriter()) def MyFunction(a, b, c, d): # any calls to print will call the 'write' method of the GUIWriter # do stuff
Вот шаблон Python 3, использующий contextmanager, который инкапсулирует метод обезьяньего патча, а также гарантирует, что sys.stdout восстановлено в случае исключения.
from io import StringIO import sys from contextlib import contextmanager @contextmanager def capture_stdout(): """ context manager encapsulating a pattern for capturing stdout writes and restoring sys.stdout even upon exceptions Examples: >>> with capture_stdout() as get_value: >>> print("here is a print") >>> captured = get_value() >>> print('Gotcha: ' + captured) >>> with capture_stdout() as get_value: >>> print("here is a print") >>> raise Exception('oh no!') >>> print('Does printing still work?') """ # Redirect sys.stdout out = StringIO() sys.stdout = out # Yield a method clients can use to obtain the value try: yield out.getvalue finally: # Restore the normal stdout sys.stdout = sys.__stdout__
Вся печать выполняется через sys.stdout , который является обычным файловым объектом: iirc, для него требуется метод write(str) . До тех пор, пока у вашей замены есть такой метод, его легко зацепить:
import sys class CaptureOutput: def write(self, message): log_message_to_textbox(message) sys.stdout = CaptureOutput()
Фактическое содержание log_message_to_textbox зависит от вас.
Как перехватить стандартный вывод подпроцесса Python в unittest
Я пытаюсь написать unit тест, который выполняет функцию, которая записывает в stdout, записывает этот вывод и проверяет результат. Эта функция является черным ящиком: мы не можем изменить способ ее вывода. Для целей этого примера я немного упростил его, но по существу функция генерирует свой результат с помощью subprocess.call(). Независимо от того, что я пытаюсь, я не могу захватить вывод. Он всегда записывается на экран, и тест терпит неудачу, потому что он ничего не фиксирует. Я экспериментировал как с print(), так и с os.system(). С print() я могу захватить stdout, но не с os.system(). Это также не относится к унитатингу. Я написал свой тестовый пример без этого с теми же результатами. Вопросы, подобные этому, были заданы очень часто, и все ответы, похоже, сводятся к использованию подпроцессов.Popen() и communication(), но для этого потребуется изменить черный ящик. Я уверен, что есть ответ, который я просто не встречал, но я в тупике. Мы используем Python-2.7. Во всяком случае, мой примерный код:
#!/usr/bin/env python from __future__ import print_function import sys sys.dont_write_bytecode = True import os import unittest import subprocess from contextlib import contextmanager from cStringIO import StringIO # from somwhere import my_function def my_function(arg): #print('my_function:', arg) subprocess.call(['/bin/echo', 'my_function: ', arg], shell=False) #os.system('echo my_function: ' + arg) @contextmanager def redirect_cm(new_stdout): old_stdout = sys.stdout sys.stdout = new_stdout try: yield finally: sys.stdout = old_stdout class Test_something(unittest.TestCase): def test(self): fptr = StringIO() with redirect_cm(fptr): my_function("some_value") self.assertEqual("my_function: some_value\n", fptr.getvalue()) if __name__ == '__main__': unittest.main()
На самом деле это довольно нетривиально в зависимости от того, как реализован subprocess , и процессов в целом. Без взломов на уровне ОС вы не можете легко получить контроль над файловыми дескрипторами процесса, скажем, до его exec . Каков фактический вариант использования? Можете ли вы заменить функцию «черного ящика» фиктивной функцией? Или, может быть, используйте monkeypatch для временного изменения стандартных аргументов subprocess.call , скажем, для перенаправления stdout в файл.
Благодарю. Наша действительная функция my_function лишь немного сложнее, чем мой пример; он просто массирует аргументы, создает команду и запускает ее. Мне нужно проверить функцию «my_function», поэтому насмешка над ней побеждает цель. Я рассмотрел monkeypatching subprocess.call с чем-то, что просто печатает список аргументов, который я мог затем захватить с помощью диспетчера контекста выше. Это может даже быть лучшим подходом, так как таким образом командная строка не выполняется.