NOTE: The below is a workaround, as capsys
/capfd
should be able to solve this problem, but doesn't work for my particular project for an unknown reason.
I've been able to accomplish this via runtime monkeypatching the print
and logging.info
functions in an independent script that I can run during CI, e.g.:
import builtins
from contextlib import contextmanager
import functools as ft
from importlib import import_module
import logging
import os
import sys
orig_print = builtins.print
orig_info, orig_warning, orig_error, orig_critical = logging.info, logging.warning, logging.error, logging.critical
NO_ARG = object()
sys.path.insert(0, 'src')
def main():
orig_print("Checking files for print() & logging on import...")
for path in files_under_watch():
orig_print(" " + path)
output = detect_toplevel_output(path)
if output:
raise SyntaxWarning(f"Top-level output (print & logging) detected in {path}: {output}")
def files_under_watch():
for root, _, files in os.walk('src'):
for file in files:
if should_watch_file(file): # your impl here
yield os.path.join(root, file)
def detect_toplevel_output(python_file_path):
with capture_print() as printed, capture_logging() as logged:
module_name = python_file_path[:-3].replace('/', '.')
import_module(module_name)
output = {'print': printed, 'logging': logged}
return {k: v for k, v in output.items() if v}
@contextmanager
def capture_print():
calls = []
@ft.wraps(orig_print)
def captured_print(*args, **kwargs):
calls.append((args, kwargs))
return orig_print(*args, **kwargs)
builtins.print = captured_print
yield calls
builtins.print = orig_print
@contextmanager
def capture_logging():
calls = []
@ft.wraps(orig_info)
def captured_info(*args, **kwargs):
calls.append(('info', args, kwargs))
return orig_info(*args, **kwargs)
@ft.wraps(orig_warning)
def captured_warning(*args, **kwargs):
calls.append(('warning', args, kwargs))
return orig_warning(*args, **kwargs)
@ft.wraps(orig_error)
def captured_error(*args, **kwargs):
calls.append(('error', args, kwargs))
return orig_error(*args, **kwargs)
@ft.wraps(orig_critical)
def captured_critical(*args, **kwargs):
calls.append(('critical', args, kwargs))
return orig_critical(*args, **kwargs)
logging.info, logging.warning, logging.error, logging.critical = captured_info, captured_warning, captured_error, captured_critical
yield calls
logging.info, logging.warning, logging.error, logging.critical = orig_info, orig_warning, orig_error, orig_critical
if __name__ == '__main__':
main()