diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49f5d0c..d1420d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ --- repos: - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 23.12.1 hooks: - id: black - args: [--safe, --quiet, --target-version, py36] + args: [--safe, --quiet, --target-version, py37] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.1 + rev: 1.16.0 hooks: - id: blacken-docs - additional_dependencies: [black==20.8b1] + additional_dependencies: [black==23.12.1] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -21,34 +21,30 @@ repos: - id: debug-statements language_version: python3 - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 7.0.0 hooks: - id: flake8 language_version: python3 - additional_dependencies: [flake8-typing-imports==1.3.0] - - repo: https://github.com/FalconSocial/pre-commit-mirrors-pep257 - rev: v0.3.3 - hooks: - - id: pep257 + additional_dependencies: [flake8-typing-imports==1.15.0] - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 7.0.0 hooks: - id: flake8 language_version: python3 - repo: https://github.com/asottile/reorder_python_imports - rev: v3.1.0 + rev: v3.12.0 hooks: - id: reorder-python-imports - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v3.15.0 hooks: - id: pyupgrade - args: [--keep-percent-format, --py36-plus] + args: [--keep-percent-format, --py37-plus] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: rst-backticks - repo: https://github.com/adrienverge/yamllint.git - rev: v1.26.3 + rev: v1.33.0 hooks: - id: yamllint diff --git a/README.rst b/README.rst index 3b16306..ebbaf1a 100644 --- a/README.rst +++ b/README.rst @@ -348,6 +348,11 @@ Unreleased - Fix debugger detection for recent VSCode, this compiles pydevd using cython which is now correctly detected. Thanks Adrian Gielniewski. +- Switched to using Pytest's ``TerminalReporter`` instead of writing + directly to ``sys.{stdout,stderr}``. + This change also switches all output from ``sys.stderr`` to ``sys.stdout``. + Thanks Pedro Algarvio. +- Pytest 7.0.0 is now the minimum supported version. Thanks Pedro Algarvio. 2.2.0 ----- diff --git a/pytest_timeout.py b/pytest_timeout.py index e52cf8f..34b1c2a 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -8,7 +8,6 @@ """ import inspect import os -import shutil import signal import sys import threading @@ -238,7 +237,9 @@ def is_debugging(trace_func=None): trace_func = sys.gettrace() trace_module = None if trace_func: - trace_module = inspect.getmodule(trace_func) or inspect.getmodule(trace_func.__class__) + trace_module = inspect.getmodule(trace_func) or inspect.getmodule( + trace_func.__class__ + ) if trace_module: parts = trace_module.__name__.split(".") for name in KNOWN_DEBUGGING_MODULES: @@ -443,11 +444,12 @@ def timeout_sigalrm(item, settings): return __tracebackhide__ = True nthreads = len(threading.enumerate()) + terminal = item.config.get_terminal_writer() if nthreads > 1: - write_title("Timeout", sep="+") - dump_stacks() + terminal.sep("+", title="Timeout") + dump_stacks(terminal) if nthreads > 1: - write_title("Timeout", sep="+") + terminal.sep("+", title="Timeout") pytest.fail("Timeout >%ss" % settings.timeout) @@ -459,6 +461,7 @@ def timeout_timer(item, settings): """ if not settings.disable_debugger_detection and is_debugging(): return + terminal = item.config.get_terminal_writer() try: capman = item.config.pluginmanager.getplugin("capturemanager") if capman: @@ -466,30 +469,31 @@ def timeout_timer(item, settings): stdout, stderr = capman.read_global_capture() else: stdout, stderr = None, None - write_title("Timeout", sep="+") + terminal.sep("+", title="Timeout") caplog = item.config.pluginmanager.getplugin("_capturelog") if caplog and hasattr(item, "capturelog_handler"): log = item.capturelog_handler.stream.getvalue() if log: - write_title("Captured log") - write(log) + terminal.sep("~", title="Captured log") + terminal.write(log) if stdout: - write_title("Captured stdout") - write(stdout) + terminal.sep("~", title="Captured stdout") + terminal.write(stdout) if stderr: - write_title("Captured stderr") - write(stderr) - dump_stacks() - write_title("Timeout", sep="+") + terminal.sep("~", title="Captured stderr") + terminal.write(stderr) + dump_stacks(terminal) + terminal.sep("+", title="Timeout") except Exception: traceback.print_exc() finally: + terminal.flush() sys.stdout.flush() sys.stderr.flush() os._exit(1) -def dump_stacks(): +def dump_stacks(terminal): """Dump the stacks of all threads except the current thread.""" current_ident = threading.current_thread().ident for thread_ident, frame in sys._current_frames().items(): @@ -501,31 +505,5 @@ def dump_stacks(): break else: thread_name = "" - write_title("Stack of %s (%s)" % (thread_name, thread_ident)) - write("".join(traceback.format_stack(frame))) - - -def write_title(title, stream=None, sep="~"): - """Write a section title. - - If *stream* is None sys.stderr will be used, *sep* is used to - draw the line. - """ - if stream is None: - stream = sys.stderr - width, height = shutil.get_terminal_size() - fill = int((width - len(title) - 2) / 2) - line = " ".join([sep * fill, title, sep * fill]) - if len(line) < width: - line += sep * (width - len(line)) - stream.write("\n" + line + "\n") - - -def write(text, stream=None): - """Write text to stream. - - Pretty stupid really, only here for symmetry with .write_title(). - """ - if stream is None: - stream = sys.stderr - stream.write(text) + terminal.sep("~", title="Stack of %s (%s)" % (thread_name, thread_ident)) + terminal.write("".join(traceback.format_stack(frame))) diff --git a/setup.cfg b/setup.cfg index be6b63f..e49f697 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ classifiers = [options] py_modules = pytest_timeout install_requires = - pytest>=5.0.0 + pytest>=7.0.0 python_requires = >=3.7 [options.entry_points] diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index da735eb..b34ecff 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -16,30 +16,21 @@ ) -@pytest.fixture -def testdir(testdir): - if hasattr(testdir, "runpytest_subprocess"): - # on pytest-2.8 runpytest runs inline by default - # patch the testdir instance to use the subprocess method - testdir.runpytest = testdir.runpytest_subprocess - return testdir - - -def test_header(testdir): - testdir.makepyfile( +def test_header(pytester): + pytester.makepyfile( """ def test_x(): pass """ ) - result = testdir.runpytest("--timeout=1") + result = pytester.runpytest_subprocess("--timeout=1") result.stdout.fnmatch_lines( ["timeout: 1.0s", "timeout method:*", "timeout func_only:*"] ) @have_sigalrm -def test_sigalrm(testdir): - testdir.makepyfile( +def test_sigalrm(pytester): + pytester.makepyfile( """ import time @@ -47,12 +38,12 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest("--timeout=1") + result = pytester.runpytest_subprocess("--timeout=1") result.stdout.fnmatch_lines(["*Failed: Timeout >1.0s*"]) -def test_thread(testdir): - testdir.makepyfile( +def test_thread(pytester): + pytester.makepyfile( """ import time @@ -60,8 +51,8 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest("--timeout=1", "--timeout-method=thread") - result.stderr.fnmatch_lines( + result = pytester.runpytest_subprocess("--timeout=1", "--timeout-method=thread") + result.stdout.fnmatch_lines( [ "*++ Timeout ++*", "*~~ Stack of MainThread* ~~*", @@ -69,16 +60,16 @@ def test_foo(): "*++ Timeout ++*", ] ) - assert "++ Timeout ++" in result.stderr.lines[-1] + assert "++ Timeout ++" in result.stdout.lines[-1] @pytest.mark.skipif( hasattr(sys, "pypy_version_info"), reason="pypy coverage seems broken currently" ) -def test_cov(testdir): +def test_cov(pytester): # This test requires pytest-cov pytest.importorskip("pytest_cov") - testdir.makepyfile( + pytester.makepyfile( """ import time @@ -86,10 +77,10 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest( + result = pytester.runpytest_subprocess( "--timeout=1", "--cov=test_cov", "--timeout-method=thread" ) - result.stderr.fnmatch_lines( + result.stdout.fnmatch_lines( [ "*++ Timeout ++*", "*~~ Stack of MainThread* ~~*", @@ -97,11 +88,11 @@ def test_foo(): "*++ Timeout ++*", ] ) - assert "++ Timeout ++" in result.stderr.lines[-1] + assert "++ Timeout ++" in result.stdout.lines[-1] -def test_timeout_env(testdir, monkeypatch): - testdir.makepyfile( +def test_timeout_env(pytester, monkeypatch): + pytester.makepyfile( """ import time @@ -110,13 +101,13 @@ def test_foo(): """ ) monkeypatch.setitem(os.environ, "PYTEST_TIMEOUT", "1") - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert result.ret > 0 # @pytest.mark.parametrize('meth', [have_sigalrm('signal'), 'thread']) -# def test_func_fix(meth, testdir): -# testdir.makepyfile(""" +# def test_func_fix(meth, pytester): +# pytester.makepyfile(""" # import time, pytest # @pytest.fixture(scope='function') @@ -126,7 +117,7 @@ def test_foo(): # def test_foo(fix): # pass # """) -# result = testdir.runpytest('--timeout=1', +# result = pytester.runpytest_subprocess('--timeout=1', # '--timeout-method={0}'.format(meth)) # assert result.ret > 0 # assert 'Timeout' in result.stdout.str() + result.stderr.str() @@ -134,8 +125,8 @@ def test_foo(): @pytest.mark.parametrize("meth", [pytest.param("signal", marks=have_sigalrm), "thread"]) @pytest.mark.parametrize("scope", ["function", "class", "module", "session"]) -def test_fix_setup(meth, scope, testdir): - testdir.makepyfile( +def test_fix_setup(meth, scope, pytester): + pytester.makepyfile( """ import time, pytest @@ -151,13 +142,13 @@ def test_foo(self, fix): scope=scope ) ) - result = testdir.runpytest("--timeout=1", f"--timeout-method={meth}") + result = pytester.runpytest_subprocess("--timeout=1", f"--timeout-method={meth}") assert result.ret > 0 assert "Timeout" in result.stdout.str() + result.stderr.str() -def test_fix_setup_func_only(testdir): - testdir.makepyfile( +def test_fix_setup_func_only(pytester): + pytester.makepyfile( """ import time, pytest @@ -172,15 +163,15 @@ def test_foo(self, fix): pass """ ) - result = testdir.runpytest("--timeout=1") + result = pytester.runpytest_subprocess("--timeout=1") assert result.ret == 0 assert "Timeout" not in result.stdout.str() + result.stderr.str() @pytest.mark.parametrize("meth", [pytest.param("signal", marks=have_sigalrm), "thread"]) @pytest.mark.parametrize("scope", ["function", "class", "module", "session"]) -def test_fix_finalizer(meth, scope, testdir): - testdir.makepyfile( +def test_fix_finalizer(meth, scope, pytester): + pytester.makepyfile( """ import time, pytest @@ -198,13 +189,15 @@ def test_foo(self, fix): pass """ ) - result = testdir.runpytest("--timeout=1", "-s", f"--timeout-method={meth}") + result = pytester.runpytest_subprocess( + "--timeout=1", "-s", f"--timeout-method={meth}" + ) assert result.ret > 0 assert "Timeout" in result.stdout.str() + result.stderr.str() -def test_fix_finalizer_func_only(testdir): - testdir.makepyfile( +def test_fix_finalizer_func_only(pytester): + pytester.makepyfile( """ import time, pytest @@ -223,14 +216,14 @@ def test_foo(self, fix): pass """ ) - result = testdir.runpytest("--timeout=1", "-s") + result = pytester.runpytest_subprocess("--timeout=1", "-s") assert result.ret == 0 assert "Timeout" not in result.stdout.str() + result.stderr.str() @have_sigalrm -def test_timeout_mark_sigalrm(testdir): - testdir.makepyfile( +def test_timeout_mark_sigalrm(pytester): + pytester.makepyfile( """ import time, pytest @@ -240,12 +233,12 @@ def test_foo(): assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*Failed: Timeout >1.0s*"]) -def test_timeout_mark_timer(testdir): - testdir.makepyfile( +def test_timeout_mark_timer(pytester): + pytester.makepyfile( """ import time, pytest @@ -254,12 +247,12 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest("--timeout-method=thread") - result.stderr.fnmatch_lines(["*++ Timeout ++*"]) + result = pytester.runpytest_subprocess("--timeout-method=thread") + result.stdout.fnmatch_lines(["*++ Timeout ++*"]) -def test_timeout_mark_non_int(testdir): - testdir.makepyfile( +def test_timeout_mark_non_int(pytester): + pytester.makepyfile( """ import time, pytest @@ -268,12 +261,12 @@ def test_foo(): time.sleep(1) """ ) - result = testdir.runpytest("--timeout-method=thread") - result.stderr.fnmatch_lines(["*++ Timeout ++*"]) + result = pytester.runpytest_subprocess("--timeout-method=thread") + result.stdout.fnmatch_lines(["*++ Timeout ++*"]) -def test_timeout_mark_non_number(testdir): - testdir.makepyfile( +def test_timeout_mark_non_number(pytester): + pytester.makepyfile( """ import pytest @@ -282,12 +275,12 @@ def test_foo(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*ValueError*"]) -def test_timeout_mark_args(testdir): - testdir.makepyfile( +def test_timeout_mark_args(pytester): + pytester.makepyfile( """ import pytest @@ -296,12 +289,12 @@ def test_foo(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*ValueError*"]) -def test_timeout_mark_method_nokw(testdir): - testdir.makepyfile( +def test_timeout_mark_method_nokw(pytester): + pytester.makepyfile( """ import time, pytest @@ -310,12 +303,12 @@ def test_foo(): time.sleep(2) """ ) - result = testdir.runpytest() - result.stderr.fnmatch_lines(["*+ Timeout +*"]) + result = pytester.runpytest_subprocess() + result.stdout.fnmatch_lines(["*+ Timeout +*"]) -def test_timeout_mark_noargs(testdir): - testdir.makepyfile( +def test_timeout_mark_noargs(pytester): + pytester.makepyfile( """ import pytest @@ -324,12 +317,12 @@ def test_foo(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*TypeError*"]) -def test_ini_timeout(testdir): - testdir.makepyfile( +def test_ini_timeout(pytester): + pytester.makepyfile( """ import time @@ -337,18 +330,18 @@ def test_foo(): time.sleep(2) """ ) - testdir.makeini( + pytester.makeini( """ [pytest] timeout = 1 """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert result.ret -def test_ini_timeout_func_only(testdir): - testdir.makepyfile( +def test_ini_timeout_func_only(pytester): + pytester.makepyfile( """ import time, pytest @@ -359,19 +352,19 @@ def test_foo(slow): pass """ ) - testdir.makeini( + pytester.makeini( """ [pytest] timeout = 1 timeout_func_only = true """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert result.ret == 0 -def test_ini_timeout_func_only_marker_override(testdir): - testdir.makepyfile( +def test_ini_timeout_func_only_marker_override(pytester): + pytester.makepyfile( """ import time, pytest @@ -383,19 +376,19 @@ def test_foo(slow): pass """ ) - testdir.makeini( + pytester.makeini( """ [pytest] timeout = 1 timeout_func_only = true """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert result.ret == 0 -def test_ini_method(testdir): - testdir.makepyfile( +def test_ini_method(pytester): + pytester.makepyfile( """ import time @@ -403,19 +396,19 @@ def test_foo(): time.sleep(2) """ ) - testdir.makeini( + pytester.makeini( """ [pytest] timeout = 1 timeout_method = thread """ ) - result = testdir.runpytest() + result = pytester.runpytest_subprocess() assert "=== 1 failed in " not in result.outlines[-1] -def test_timeout_marker_inheritance(testdir): - testdir.makepyfile( +def test_timeout_marker_inheritance(pytester): + pytester.makepyfile( """ import time, pytest @@ -430,13 +423,13 @@ def test_foo_1(self): time.sleep(1) """ ) - result = testdir.runpytest("--timeout=1", "-s") + result = pytester.runpytest_subprocess("--timeout=1", "-s") assert result.ret == 0 assert "Timeout" not in result.stdout.str() + result.stderr.str() -def test_marker_help(testdir): - result = testdir.runpytest("--markers") +def test_marker_help(pytester): + result = pytester.runpytest_subprocess("--markers") result.stdout.fnmatch_lines(["@pytest.mark.timeout(*"]) @@ -461,9 +454,9 @@ def test_marker_help(testdir): ) @have_spawn def test_suppresses_timeout_when_debugger_is_entered( - testdir, debugging_module, debugging_set_trace + pytester, debugging_module, debugging_set_trace ): - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest, {debugging_module} @@ -474,7 +467,7 @@ def test_foo(): debugging_module=debugging_module, debugging_set_trace=debugging_set_trace ) ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("test_foo") time.sleep(0.2) child.send("c\n") @@ -507,9 +500,9 @@ def test_foo(): ) @have_spawn def test_disable_debugger_detection_flag( - testdir, debugging_module, debugging_set_trace + pytester, debugging_module, debugging_set_trace ): - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest, {debugging_module} @@ -520,7 +513,7 @@ def test_foo(): debugging_module=debugging_module, debugging_set_trace=debugging_set_trace ) ) - child = testdir.spawn_pytest(f"{p1} --timeout-disable-debugger-detection") + child = pytester.spawn_pytest(f"{p1} --timeout-disable-debugger-detection") child.expect("test_foo") time.sleep(1.2) result = child.read().decode().lower() @@ -551,8 +544,9 @@ def custom_trace(*args): assert pytest_timeout.is_debugging(custom_trace) -def test_not_main_thread(testdir): - testdir.makepyfile( +def test_not_main_thread(pytester): + pytest.skip("The 'pytest_timeout.timeout_setup' function no longer exists") + pytester.makepyfile( """ import threading import pytest_timeout @@ -569,14 +563,14 @@ def new_timeout_setup(item): def test_x(): pass """ ) - result = testdir.runpytest("--timeout=1") + result = pytester.runpytest_subprocess("--timeout=1") result.stdout.fnmatch_lines( ["timeout: 1.0s", "timeout method:*", "timeout func_only:*"] ) -def test_plugin_interface(testdir): - testdir.makeconftest( +def test_plugin_interface(pytester): + pytester.makeconftest( """ import pytest @@ -593,7 +587,7 @@ def pytest_timeout_cancel_timer(item): return True """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -602,7 +596,7 @@ def test_foo(): pass """ ) - result = testdir.runpytest("-s") + result = pytester.runpytest_subprocess("-s") result.stdout.fnmatch_lines( [ "pytest_timeout_set_timer", diff --git a/tox.ini b/tox.ini index 966ea49..aa195b6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [pytest] -minversion = 2.8 +minversion = 7.0 addopts = -ra [tox] @@ -23,5 +23,3 @@ commands = pre-commit run --all-files --show-diff-on-failure [flake8] disable-noqa = True max-line-length = 88 -extend-ignore = - E203 # whitespace before : is not PEP8 compliant (& conflicts with black)