diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9763840..866622f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,10 +8,6 @@ jobs: fail-fast: false matrix: python: - - v: "3.8" - tox_env: "py38" - - v: "3.9" - tox_env: "py39" - v: "3.10" tox_env: "py310" - v: "3.11" @@ -28,9 +24,9 @@ jobs: run: | git config --global core.autocrlf false git config --global core.eol lf - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python.v }} - name: Install tox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8cc9509..d8d1388 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,50 +1,58 @@ --- repos: - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 25.11.0 hooks: - id: black - args: [--safe, --quiet, --target-version, py37] + args: [--safe, --quiet, --target-version, py310] - repo: https://github.com/asottile/blacken-docs - rev: 1.16.0 + rev: 1.20.0 hooks: - id: blacken-docs - additional_dependencies: [black==23.12.1] + additional_dependencies: [black==25.11.0] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: fix-encoding-pragma - args: [--remove] + - id: check-ast + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-toml - id: check-yaml + - id: check-illegal-windows-names + - id: check-symlinks + - id: check-added-large-files + args: ['--maxkb=500'] - id: debug-statements + - id: destroyed-symlinks + - id: end-of-file-fixer + - id: mixed-line-ending + args: ["--fix=lf"] + - id: trailing-whitespace + - id: name-tests-test + args: + - --pytest-test-first language_version: python3 - repo: https://github.com/PyCQA/flake8 rev: 7.0.0 hooks: - id: flake8 language_version: python3 - additional_dependencies: [flake8-typing-imports==1.15.0] - - repo: https://github.com/PyCQA/flake8 + additional_dependencies: [flake8-typing-imports==1.17.0] + - repo: https://github.com/pycqa/isort rev: 7.0.0 hooks: - - id: flake8 - language_version: python3 - - repo: https://github.com/asottile/reorder_python_imports - rev: v3.12.0 - hooks: - - id: reorder-python-imports + - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.21.1 hooks: - id: pyupgrade - args: [--keep-percent-format, --py37-plus] + args: [--keep-percent-format, --py310-plus] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: rst-backticks - repo: https://github.com/adrienverge/yamllint.git - rev: v1.33.0 + rev: v1.37.1 hooks: - id: yamllint diff --git a/README.rst b/README.rst index cf3d10d..a194180 100644 --- a/README.rst +++ b/README.rst @@ -394,10 +394,9 @@ Changelog x.y.z ----- -- Add support Python3.13 and Python3.14. Thanks Vladimir - Roshchin. -- Detect debuggers registered with sys.monitoring. Thanks Rich - Chiodo. +- Minimum support Python3.10 and pytest=8.0. Thanks Vladimir Roshchin. +- Add support Python3.13 and Python3.14. Thanks Vladimir Roshchin. +- Detect debuggers registered with sys.monitoring. Thanks Rich Chiodo. 2.3.1 ----- diff --git a/failure_demo.py b/failure_demo.py index 5271a77..1e6e7b4 100644 --- a/failure_demo.py +++ b/failure_demo.py @@ -4,6 +4,7 @@ pytest failure_demo.py """ + import threading import time diff --git a/pytest_timeout.py b/pytest_timeout.py index 932509c..365aeb5 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -6,6 +6,7 @@ If the platform supports SIGALRM this is used to raise an exception in the test, otherwise os._exit(1) is used. """ + import inspect import os import signal @@ -17,17 +18,13 @@ import pytest - -__all__ = ("is_debugging", "Settings") +__all__ = ("Settings", "is_debugging") SESSION_TIMEOUT_KEY = pytest.StashKey[float]() SESSION_EXPIRE_KEY = pytest.StashKey[float]() PYTEST_FAILURE_MESSAGE = "Timeout (>%ss) from pytest-timeout." HAVE_SIGALRM = hasattr(signal, "SIGALRM") -if HAVE_SIGALRM: - DEFAULT_METHOD = "signal" -else: - DEFAULT_METHOD = "thread" +DEFAULT_METHOD = "signal" if HAVE_SIGALRM else "thread" TIMEOUT_DESC = """ Timeout in seconds before dumping the stacks. Default is 0 which means no timeout. @@ -112,7 +109,7 @@ class TimeoutHooks: """Timeout specific hooks.""" @pytest.hookspec(firstresult=True) - def pytest_timeout_set_timer(item, settings): + def pytest_timeout_set_timer(self, item, settings): """Called at timeout setup. 'item' is a pytest node to setup timeout for. @@ -122,7 +119,7 @@ def pytest_timeout_set_timer(item, settings): """ @pytest.hookspec(firstresult=True) - def pytest_timeout_cancel_timer(item): + def pytest_timeout_cancel_timer(self, item): """Called at timeout teardown. 'item' is a pytest node which was used for timeout setup. @@ -223,12 +220,9 @@ def pytest_report_header(config): if config._env_timeout: timeout_header.append( - "timeout: %ss\ntimeout method: %s\ntimeout func_only: %s" - % ( - config._env_timeout, - config._env_timeout_method, - config._env_timeout_func_only, - ) + f"timeout: {config._env_timeout}s\n" + f"timeout method: {config._env_timeout_method}\n" + f"timeout func_only: {config._env_timeout_func_only}" ) session_timeout = config.getoption("session_timeout") @@ -236,6 +230,7 @@ def pytest_report_header(config): timeout_header.append("session timeout: %ss" % session_timeout) if timeout_header: return timeout_header + return None @pytest.hookimpl(tryfirst=True) @@ -249,10 +244,10 @@ def pytest_exception_interact(node): def pytest_enter_pdb(): """Stop the timeouts when we entered pdb. - This stops timeouts from triggering when pytest's builting pdb + This stops timeouts from triggering when pytest's builtin pdb support notices we entered pdb. """ - # Since pdb.set_trace happens outside of any pytest control, we don't have + # Since pdb.set_trace happens outside any pytest control, we don't have # any pytest ``item`` here, so we cannot use timeout_teardown. Thus, we # need another way to signify that the timeout should not be performed. global SUPPRESS_TIMEOUT @@ -326,7 +321,7 @@ def cancel(): signal.setitimer(signal.ITIMER_REAL, settings.timeout) elif timeout_method == "thread": timer = threading.Timer(settings.timeout, timeout_timer, (item, settings)) - timer.name = "%s %s" % (__name__, item.nodeid) + timer.name = f"{__name__} {item.nodeid}" def cancel(): timer.cancel() @@ -427,14 +422,15 @@ def _parse_marker(marker): elif kw == "func_only": func_only = val else: - raise TypeError("Invalid keyword argument for timeout marker: %s" % kw) + msg = f"Invalid keyword argument for timeout marker: {kw}" + raise TypeError(msg) if len(marker.args) >= 1 and timeout is not NOTSET: raise TypeError("Multiple values for timeout argument of timeout marker") - elif len(marker.args) >= 1: + if len(marker.args) >= 1: timeout = marker.args[0] if len(marker.args) >= 2 and method is not NOTSET: raise TypeError("Multiple values for method argument of timeout marker") - elif len(marker.args) >= 2: + if len(marker.args) >= 2: method = marker.args[1] if len(marker.args) > 2: raise TypeError("Too many arguments for timeout marker") @@ -453,14 +449,16 @@ def _validate_timeout(timeout, where): try: return float(timeout) except ValueError: - raise ValueError("Invalid timeout %s from %s" % (timeout, where)) + msg = f"Invalid timeout {timeout} from {where}" + raise ValueError(msg) def _validate_method(method, where): if method is None: return None if method not in ["signal", "thread"]: - raise ValueError("Invalid method %s from %s" % (method, where)) + msg = f"Invalid method {method} from {where}" + raise ValueError(msg) return method @@ -468,7 +466,8 @@ def _validate_func_only(func_only, where): if func_only is None: return None if not isinstance(func_only, bool): - raise ValueError("Invalid func_only value %s from %s" % (func_only, where)) + msg = f"Invalid func_only value {func_only} from {where}" + raise ValueError(msg) return func_only @@ -555,5 +554,5 @@ def dump_stacks(terminal): break else: thread_name = "" - terminal.sep("~", title="Stack of %s (%s)" % (thread_name, thread_ident)) + terminal.sep("~", title=f"Stack of {thread_name} ({thread_ident})") terminal.write("".join(traceback.format_stack(frame))) diff --git a/setup.cfg b/setup.cfg index 106c2ae..b634af8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,9 +21,6 @@ classifiers = Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 @@ -35,8 +32,8 @@ classifiers = [options] py_modules = pytest_timeout install_requires = - pytest>=7.0.0 -python_requires = >=3.7 + pytest>=8.0.0 +python_requires = >=3.10 [options.entry_points] pytest11 = diff --git a/setup.py b/setup.py index 4289c21..90e22e1 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ """Setuptools install script for pytest-timeout.""" + from setuptools import setup if __name__ == "__main__": diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index 8593a8d..68e5690 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -8,7 +8,6 @@ from pytest_timeout import PYTEST_FAILURE_MESSAGE - MATCH_FAILURE_MESSAGE = f"*Failed: {PYTEST_FAILURE_MESSAGE}*" pytest_plugins = "pytester" @@ -136,7 +135,7 @@ def test_foo(): @pytest.mark.parametrize("scope", ["function", "class", "module", "session"]) def test_fix_setup(meth, scope, pytester): pytester.makepyfile( - """ + f""" import time, pytest class TestFoo: @@ -147,9 +146,7 @@ def fix(self): def test_foo(self, fix): pass - """.format( - scope=scope - ) + """ ) result = pytester.runpytest_subprocess("--timeout=1", f"--timeout-method={meth}") assert result.ret > 0 @@ -466,15 +463,13 @@ def test_suppresses_timeout_when_debugger_is_entered( pytester, debugging_module, debugging_set_trace ): p1 = pytester.makepyfile( - """ + f""" import pytest, {debugging_module} @pytest.mark.timeout(1) def test_foo(): {debugging_module}.{debugging_set_trace} - """.format( - debugging_module=debugging_module, debugging_set_trace=debugging_set_trace - ) + """ ) child = pytester.spawn_pytest(str(p1)) child.expect("test_foo") @@ -512,15 +507,13 @@ def test_disable_debugger_detection_flag( pytester, debugging_module, debugging_set_trace ): p1 = pytester.makepyfile( - """ + f""" import pytest, {debugging_module} @pytest.mark.timeout(1) def test_foo(): {debugging_module}.{debugging_set_trace} - """.format( - debugging_module=debugging_module, debugging_set_trace=debugging_set_trace - ) + """ ) child = pytester.spawn_pytest(f"{p1} --timeout-disable-debugger-detection") child.expect("test_foo") diff --git a/tox.ini b/tox.ini index 285c703..79ee2e0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [pytest] -minversion = 7.0 +minversion = 8.0 addopts = -ra [tox] -envlist = py38,py39,py310,py311,py312,py313,py314,pypy3 +envlist = py310,py311,py312,py313,py314,pypy3 [testenv] deps = pytest @@ -16,7 +16,7 @@ commands = pytest {posargs} [testenv:linting] skip_install = True basepython = python3 -deps = pre-commit>=1.11.0 +deps = pre-commit>=4.0.0 commands = pre-commit run --all-files --show-diff-on-failure