From 73e74c29ab5f6cb696ffea29e7e9ad8303a10c67 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:25:59 -0700 Subject: [PATCH 1/3] fix: accept string timeout values in pyproject.toml PR #200 registered the timeout ini option as type='float', which rejects string values like timeout = '20.0' in pyproject.toml. Users must now choose a config format compatible with only one pytest-timeout version: 2.4.0 requires strings, 2.5.0 requires floats, and no value works for both. Remove the explicit type so the ini parsing accepts both string and numeric representations. _validate_timeout already calls float(timeout) on the raw value and handles both forms correctly. Fixes #203 Co-authored-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> --- pytest_timeout.py | 4 ++-- test_pytest_timeout.py | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/pytest_timeout.py b/pytest_timeout.py index 192a16c..29f2a57 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -94,7 +94,7 @@ def pytest_addoption(parser): metavar="SECONDS", help=SESSION_TIMEOUT_DESC, ) - parser.addini("timeout", TIMEOUT_DESC, type="float") + parser.addini("timeout", TIMEOUT_DESC) parser.addini("timeout_method", METHOD_DESC) parser.addini("timeout_func_only", FUNC_ONLY_DESC, type="bool", default=False) parser.addini( @@ -103,7 +103,7 @@ def pytest_addoption(parser): type="bool", default=False, ) - parser.addini("session_timeout", SESSION_TIMEOUT_DESC, type="float") + parser.addini("session_timeout", SESSION_TIMEOUT_DESC) class TimeoutHooks: diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index ff36c40..cf19a34 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -372,6 +372,32 @@ def test_foo(): assert result.ret +@pytest.mark.skipif( + pytest.version_tuple < (9, 0), + reason="native [tool.pytest] table requires pytest 9", +) +def test_pyproject_toml_str_timeout(pytester): + """Regression test for #203: accept string timeout values in pyproject.toml.""" + pytester.makepyfile( + """ + import time + + def test_foo(): + time.sleep(2) + """ + ) + pytester.makepyprojecttoml( + """ + [tool.pytest] + timeout = "1" + session_timeout = "60" + """ + ) + result = pytester.runpytest_subprocess() + result.stdout.no_fnmatch_line("INTERNALERROR*") + assert result.ret + + def test_ini_timeout_func_only(pytester): pytester.makepyfile( """ From 4c9d604f0c3c8f87e1987492cfdec37e361eecd2 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:01:26 -0700 Subject: [PATCH 2/3] fix: accept both quoted and unquoted timeout values in pyproject.toml On pytest 9+, registering an ini option without an explicit type (defaulting to "string") rejects non-string TOML values, while type="float" rejects quoted strings. Both representations need to work so users don't have to choose a config format by version. Introduce _get_ini_value() which reads via config.getini() but catches TypeError and falls back to the raw config dict, then use it for both and ini reads. The regression test now covers both (quoted string) and (unquoted int), and asserts a real timeout outcome via the failure message and assert_outcomes instead of a stdout-only INTERNALERROR check that masked the configuration error. Closes #203 --- pytest_timeout.py | 20 +++++++++++++++++--- test_pytest_timeout.py | 25 ++++++++++++++++++------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/pytest_timeout.py b/pytest_timeout.py index 29f2a57..3ea64da 100644 --- a/pytest_timeout.py +++ b/pytest_timeout.py @@ -57,6 +57,20 @@ ) +def _get_ini_value(config, name): + """Read an ini option, falling back to the raw config for non-string TOML values. + + On pytest 9+ the ``"string"`` ini-type rejects non-string TOML values + (e.g. ``timeout = 1``), but ``"float"`` rejects quoted strings such as + ``timeout = "1"``. We register the option with the default string type + and catch the ``TypeError`` here so both representations work. + """ + try: + return config.getini(name) + except TypeError: + return config.inicfg.get(name) + + @pytest.hookimpl def pytest_addoption(parser): """Add options to control the timeout plugin.""" @@ -161,9 +175,9 @@ def pytest_configure(config): timeout = config.getoption("session_timeout") if timeout is None: - ini = config.getini("session_timeout") + ini = _get_ini_value(config, "session_timeout") if ini: - timeout = _validate_timeout(config.getini("session_timeout"), "config file") + timeout = _validate_timeout(ini, "config file") if timeout is not None: expire_time = time.time() + timeout else: @@ -350,7 +364,7 @@ def get_env_settings(config): os.environ.get("PYTEST_TIMEOUT"), "PYTEST_TIMEOUT environment variable" ) if timeout is None: - ini = config.getini("timeout") + ini = _get_ini_value(config, "timeout") if ini: timeout = _validate_timeout(ini, "config file") diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index cf19a34..865f12c 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -376,8 +376,19 @@ def test_foo(): pytest.version_tuple < (9, 0), reason="native [tool.pytest] table requires pytest 9", ) -def test_pyproject_toml_str_timeout(pytester): - """Regression test for #203: accept string timeout values in pyproject.toml.""" +@pytest.mark.parametrize( + ("toml_timeout", "toml_session_timeout"), + [ + pytest.param('"1"', '"60"', id="quoted-string"), + pytest.param("1", "60", id="unquoted-int"), + ], +) +def test_pyproject_toml_timeout(pytester, toml_timeout, toml_session_timeout): + """Regression test for #203: accept timeout values in pyproject.toml. + + Both quoted (string) and unquoted (int) forms should work in the + ``[tool.pytest]`` table (``timeout = "1"`` and ``timeout = 1``). + """ pytester.makepyfile( """ import time @@ -387,15 +398,15 @@ def test_foo(): """ ) pytester.makepyprojecttoml( - """ + f""" [tool.pytest] - timeout = "1" - session_timeout = "60" + timeout = {toml_timeout} + session_timeout = {toml_session_timeout} """ ) result = pytester.runpytest_subprocess() - result.stdout.no_fnmatch_line("INTERNALERROR*") - assert result.ret + result.stdout.fnmatch_lines([MATCH_FAILURE_MESSAGE % "1.0"]) + result.assert_outcomes(failed=1) def test_ini_timeout_func_only(pytester): From cafe28b27a4ca114d51ba2cfdd0b65325ad46f5c Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam Date: Tue, 8 Sep 2026 23:26:35 -0700 Subject: [PATCH 3/3] Make pyproject.toml timeout regression test portable Force --timeout-method=thread and assert the thread method's timeout output (++ Timeout ++ markers) plus os._exit(1), instead of the signal method's failure summary. The signal method is unavailable on Windows (where thread is the default), which made the assertions fail there. A configuration failure would exit 3 without reaching the timeout, so this still proves the values parsed. --- test_pytest_timeout.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/test_pytest_timeout.py b/test_pytest_timeout.py index 865f12c..666c69b 100644 --- a/test_pytest_timeout.py +++ b/test_pytest_timeout.py @@ -388,6 +388,13 @@ def test_pyproject_toml_timeout(pytester, toml_timeout, toml_session_timeout): Both quoted (string) and unquoted (int) forms should work in the ``[tool.pytest]`` table (``timeout = "1"`` and ``timeout = 1``). + + The thread timeout method is forced so the assertions are portable: + the signal method is unavailable on Windows (where thread is the + default), and its timeout output differs from the thread method's + stack dump plus ``+++ Timeout +++`` markers. A configuration failure + would exit 3 instead of reaching the timeout, so asserting the + timeout output and exit status also proves the values parsed. """ pytester.makepyfile( """ @@ -404,9 +411,17 @@ def test_foo(): session_timeout = {toml_session_timeout} """ ) - result = pytester.runpytest_subprocess() - result.stdout.fnmatch_lines([MATCH_FAILURE_MESSAGE % "1.0"]) - result.assert_outcomes(failed=1) + result = pytester.runpytest_subprocess("--timeout-method=thread") + result.stdout.fnmatch_lines( + [ + "*++ Timeout ++*", + "*~~ Stack of MainThread* ~~*", + "*File *, line *, in *", + "*++ Timeout ++*", + ] + ) + assert "++ Timeout ++" in result.stdout.lines[-1] + assert result.ret == 1 def test_ini_timeout_func_only(pytester):