From 6ed587dc15259d526c66d2defcefb0f65eaae2ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:21:51 +0000 Subject: [PATCH 1/3] Accept numeric strings for verbosity options in native TOML again #15153 registered the verbosity_* options as `int | Literal["auto"]`. In native TOML mode neither member accepts a numeric string, so `verbosity_assertions = "2"` raised a UsageError, although the changelog promised strings keep working. Register them as `int | str` (like tmp_path_retention_count) and validate the string in `Config.get_verbosity`, which now raises a UsageError naming the option for values that are neither an int nor "auto". Fixes #15290. Co-Authored-By: Claude Opus 5.5 --- changelog/15290.bugfix.rst | 1 + src/_pytest/config/__init__.py | 15 ++++++++++++--- testing/test_config.py | 13 ++++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 changelog/15290.bugfix.rst diff --git a/changelog/15290.bugfix.rst b/changelog/15290.bugfix.rst new file mode 100644 index 00000000000..60045b54e24 --- /dev/null +++ b/changelog/15290.bugfix.rst @@ -0,0 +1 @@ +Native TOML configuration again accepts numeric strings such as ``"2"`` for :confval:`verbosity_assertions`, :confval:`verbosity_test_cases` and :confval:`verbosity_subtests`, as promised for backward compatibility in pytest 9.2.0 -- regressed by :pr:`15153`. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 60ba5f54a89..6aada463e24 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -2268,8 +2268,15 @@ def get_verbosity(self, verbosity_type: str | None = None) -> int: level = self.getini(ini_name) if level == Config._VERBOSITY_INI_DEFAULT: return global_level - - return int(level) + if isinstance(level, int): + return level + try: + return int(level) + except ValueError: + raise UsageError( + f"{self.inipath}: config option '{ini_name}' expects an int " + f"or 'auto', got {level!r}" + ) from None @staticmethod def _verbosity_ini_name(verbosity_type: str) -> str: @@ -2289,7 +2296,9 @@ def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None: parser.addini( Config._verbosity_ini_name(verbosity_type), help=help, - type=int | Literal["auto"], + # str rather than Literal["auto"]: numeric strings stay accepted + # in TOML for backward compatibility, see get_verbosity. + type=int | str, default=Config._VERBOSITY_INI_DEFAULT, ) diff --git a/testing/test_config.py b/testing/test_config.py index 282e66409f7..485c88d6685 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -3630,7 +3630,6 @@ def test_getini( @pytest.mark.parametrize( ("name", "value"), [ - ("verbosity_assertions", '"loud"'), ("log_file_mode", '"x"'), ("parametrize_long_str_id_strategy", '"bogus"'), ], @@ -3641,7 +3640,15 @@ def test_invalid_value(self, pytester: Pytester, name: str, value: str) -> None: with pytest.raises(UsageError, match=name): config.getini(name) - def test_get_verbosity(self, pytester: Pytester) -> None: - pytester.maketoml("[pytest]\nverbosity_assertions = 2\n") + @pytest.mark.parametrize("toml_value", ["2", '"2"']) + def test_get_verbosity(self, pytester: Pytester, toml_value: str) -> None: + pytester.maketoml(f"[pytest]\nverbosity_assertions = {toml_value}\n") config = pytester.parseconfig() assert config.get_verbosity(Config.VERBOSITY_ASSERTIONS) == 2 + + @pytest.mark.parametrize("toml_value", ['"loud"', "true", "2.5"]) + def test_get_verbosity_invalid(self, pytester: Pytester, toml_value: str) -> None: + pytester.maketoml(f"[pytest]\nverbosity_assertions = {toml_value}\n") + config = pytester.parseconfig() + with pytest.raises(UsageError, match="verbosity_assertions"): + config.get_verbosity(Config.VERBOSITY_ASSERTIONS) From 71e56049305f3c8e22166b463a2eac97e278aeec Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:27:36 +0000 Subject: [PATCH 2/3] Deprecate numeric strings for verbosity options in TOML Native TOML has real integers, so `verbosity_assertions = "2"` is only kept working for backward compatibility. Issue a PytestRemovedIn10Warning once per option when get_verbosity has to convert such a string, and document the deprecation. ini files are unaffected: the int member of the option type converts their values before get_verbosity sees them. Co-Authored-By: Claude Opus 5.5 --- changelog/15290.bugfix.rst | 2 +- changelog/15290.deprecation.rst | 1 + doc/en/deprecations.rst | 23 +++++++++++++++++++++++ src/_pytest/config/__init__.py | 18 +++++++++++++++--- src/_pytest/deprecated.py | 7 +++++++ testing/test_config.py | 32 +++++++++++++++++++++++++++++--- 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 changelog/15290.deprecation.rst diff --git a/changelog/15290.bugfix.rst b/changelog/15290.bugfix.rst index 60045b54e24..533bf64d2a1 100644 --- a/changelog/15290.bugfix.rst +++ b/changelog/15290.bugfix.rst @@ -1 +1 @@ -Native TOML configuration again accepts numeric strings such as ``"2"`` for :confval:`verbosity_assertions`, :confval:`verbosity_test_cases` and :confval:`verbosity_subtests`, as promised for backward compatibility in pytest 9.2.0 -- regressed by :pr:`15153`. +Native TOML configuration again accepts numeric strings such as ``"2"`` for :confval:`verbosity_assertions`, :confval:`verbosity_test_cases` and :confval:`verbosity_subtests`, now with a deprecation warning -- regressed by :pr:`15153`. diff --git a/changelog/15290.deprecation.rst b/changelog/15290.deprecation.rst new file mode 100644 index 00000000000..eb675995f00 --- /dev/null +++ b/changelog/15290.deprecation.rst @@ -0,0 +1 @@ +Numeric strings such as ``"2"`` for :confval:`verbosity_assertions`, :confval:`verbosity_test_cases` and :confval:`verbosity_subtests` in native TOML configuration are deprecated, use integers instead -- see :ref:`toml-verbosity-strings`. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 328411a9fcc..e45e9209922 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -14,6 +14,29 @@ Deprecated Features Below is a complete list of all pytest features which are considered deprecated. Using those features will issue :class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. +.. _toml-verbosity-strings: + +Numeric strings for verbosity options in TOML +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.1 + +In native TOML configuration, :confval:`verbosity_assertions`, +:confval:`verbosity_test_cases` and :confval:`verbosity_subtests` take an +integer or ``"auto"``. Numeric strings are still accepted for backward +compatibility, but issue a deprecation warning and will be rejected in pytest 10. + +.. code-block:: toml + + # Deprecated + verbosity_assertions = "2" + + # Use this instead + verbosity_assertions = 2 + +INI files are not affected, since all their values are strings. + + .. _terminalreporter-write-line-bytes: Passing ``bytes`` to ``TerminalReporter.write_line()`` diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6aada463e24..9055ce1a645 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1261,6 +1261,7 @@ def __init__( self.trace = self.pluginmanager.trace.root.get("config") self.hook = self.pluginmanager.hook self._inicache: dict[str, Any] = {} + self._warned_verbosity_strings: set[str] = set() self._inicfg: ConfigDict = {} self._cleanup_stack = contextlib.ExitStack() self.pluginmanager.register(self, "pytestconfig") @@ -2271,12 +2272,23 @@ def get_verbosity(self, verbosity_type: str | None = None) -> int: if isinstance(level, int): return level try: - return int(level) + int_level = int(level) except ValueError: raise UsageError( f"{self.inipath}: config option '{ini_name}' expects an int " f"or 'auto', got {level!r}" ) from None + # A numeric str only gets here from TOML: in ini mode the int member + # of the option type already converted it. + if ini_name not in self._warned_verbosity_strings: + self._warned_verbosity_strings.add(ini_name) + self.issue_config_time_warning( + _pytest.deprecated.TOML_VERBOSITY_STRING.format( + name=ini_name, value=level + ), + stacklevel=2, + ) + return int_level @staticmethod def _verbosity_ini_name(verbosity_type: str) -> str: @@ -2296,8 +2308,8 @@ def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None: parser.addini( Config._verbosity_ini_name(verbosity_type), help=help, - # str rather than Literal["auto"]: numeric strings stay accepted - # in TOML for backward compatibility, see get_verbosity. + # str rather than Literal["auto"]: numeric strings in TOML are + # deprecated but still accepted, see get_verbosity. type=int | str, default=Config._VERBOSITY_INI_DEFAULT, ) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 98b7e112bf6..3e30c00ad80 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -114,6 +114,13 @@ '(e.g. type="args") instead of relying on the implicit string default.' ) +TOML_VERBOSITY_STRING = UnformattedWarning( + PytestRemovedIn10Warning, + "{name} = {value!r} in TOML configuration is deprecated, " + "use an integer instead: {name} = {value}\n" + "See https://docs.pytest.org/en/stable/deprecations.html#toml-verbosity-strings", +) + # You want to make some `__init__` or function "private". # # def my_private_function(some, args): diff --git a/testing/test_config.py b/testing/test_config.py index 485c88d6685..71cf1779f49 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -13,6 +13,7 @@ from typing import Any from typing import cast from typing import Literal +import warnings import _pytest._code from _pytest.assertion.rewrite import AssertionRewritingHook @@ -3640,12 +3641,37 @@ def test_invalid_value(self, pytester: Pytester, name: str, value: str) -> None: with pytest.raises(UsageError, match=name): config.getini(name) - @pytest.mark.parametrize("toml_value", ["2", '"2"']) - def test_get_verbosity(self, pytester: Pytester, toml_value: str) -> None: - pytester.maketoml(f"[pytest]\nverbosity_assertions = {toml_value}\n") + def test_get_verbosity(self, pytester: Pytester) -> None: + pytester.maketoml("[pytest]\nverbosity_assertions = 2\n") config = pytester.parseconfig() assert config.get_verbosity(Config.VERBOSITY_ASSERTIONS) == 2 + def test_get_verbosity_numeric_string_deprecated(self, pytester: Pytester) -> None: + pytester.maketoml('[pytest]\nverbosity_assertions = "2"\n') + pytester.makepyfile( + """ + from pytest import Config + + def test_a(pytestconfig): + assert pytestconfig.get_verbosity(Config.VERBOSITY_ASSERTIONS) == 2 + + def test_b(pytestconfig): + assert pytestconfig.get_verbosity(Config.VERBOSITY_ASSERTIONS) == 2 + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=2, warnings=1) + result.stdout.fnmatch_lines( + ["*PytestRemovedIn10Warning: verbosity_assertions = '2' in TOML*"] + ) + + def test_get_verbosity_ini_numeric_string(self, pytester: Pytester) -> None: + pytester.makeini("[pytest]\nverbosity_assertions = 2\n") + config = pytester.parseconfig() + with warnings.catch_warnings(): + warnings.simplefilter("error") + assert config.get_verbosity(Config.VERBOSITY_ASSERTIONS) == 2 + @pytest.mark.parametrize("toml_value", ['"loud"', "true", "2.5"]) def test_get_verbosity_invalid(self, pytester: Pytester, toml_value: str) -> None: pytester.maketoml(f"[pytest]\nverbosity_assertions = {toml_value}\n") From a9b505d042fe9e11ee639c3059ca5c004bc814f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:34:01 +0000 Subject: [PATCH 3/3] Deprecate numeric strings for tmp_path_retention_count in TOML Same as the verbosity options: TOML has real integers, so a string like "3" only stays accepted for backward compatibility and now issues a PytestRemovedIn10Warning. The deprecation message and docs section are generalized to cover both. Co-Authored-By: Claude Opus 5.5 --- changelog/15290.deprecation.rst | 2 +- doc/en/deprecations.rst | 9 +++++---- src/_pytest/config/__init__.py | 2 +- src/_pytest/deprecated.py | 4 ++-- src/_pytest/tmpdir.py | 15 ++++++++++++++- testing/test_tmpdir.py | 14 ++++++++++++++ 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/changelog/15290.deprecation.rst b/changelog/15290.deprecation.rst index eb675995f00..496f0e5e125 100644 --- a/changelog/15290.deprecation.rst +++ b/changelog/15290.deprecation.rst @@ -1 +1 @@ -Numeric strings such as ``"2"`` for :confval:`verbosity_assertions`, :confval:`verbosity_test_cases` and :confval:`verbosity_subtests` in native TOML configuration are deprecated, use integers instead -- see :ref:`toml-verbosity-strings`. +Numeric strings such as ``"2"`` for :confval:`verbosity_assertions`, :confval:`verbosity_test_cases`, :confval:`verbosity_subtests` and :confval:`tmp_path_retention_count` in native TOML configuration are deprecated, use integers instead -- see :ref:`toml-numeric-strings`. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index e45e9209922..874ff25d2f4 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -14,16 +14,17 @@ Deprecated Features Below is a complete list of all pytest features which are considered deprecated. Using those features will issue :class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. -.. _toml-verbosity-strings: +.. _toml-numeric-strings: -Numeric strings for verbosity options in TOML -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Numeric strings for integer options in TOML +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 9.2.1 In native TOML configuration, :confval:`verbosity_assertions`, :confval:`verbosity_test_cases` and :confval:`verbosity_subtests` take an -integer or ``"auto"``. Numeric strings are still accepted for backward +integer or ``"auto"``, and :confval:`tmp_path_retention_count` takes an +integer. Numeric strings are still accepted for backward compatibility, but issue a deprecation warning and will be rejected in pytest 10. .. code-block:: toml diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 9055ce1a645..d1972228756 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -2283,7 +2283,7 @@ def get_verbosity(self, verbosity_type: str | None = None) -> int: if ini_name not in self._warned_verbosity_strings: self._warned_verbosity_strings.add(ini_name) self.issue_config_time_warning( - _pytest.deprecated.TOML_VERBOSITY_STRING.format( + _pytest.deprecated.TOML_NUMERIC_STRING.format( name=ini_name, value=level ), stacklevel=2, diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 3e30c00ad80..c6999dd54a8 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -114,11 +114,11 @@ '(e.g. type="args") instead of relying on the implicit string default.' ) -TOML_VERBOSITY_STRING = UnformattedWarning( +TOML_NUMERIC_STRING = UnformattedWarning( PytestRemovedIn10Warning, "{name} = {value!r} in TOML configuration is deprecated, " "use an integer instead: {name} = {value}\n" - "See https://docs.pytest.org/en/stable/deprecations.html#toml-verbosity-strings", + "See https://docs.pytest.org/en/stable/deprecations.html#toml-numeric-strings", ) # You want to make some `__init__` or function "private". diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index ca94fdde1da..3e68606f829 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -30,6 +30,7 @@ from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest +from _pytest.deprecated import TOML_NUMERIC_STRING from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch @@ -97,7 +98,18 @@ def from_config( :meta private: """ check_ispytest(_ispytest) - count = int(config.getini("tmp_path_retention_count")) + count = config.getini("tmp_path_retention_count") + if isinstance(count, str): + # A str only gets here from TOML: in ini mode the int member of + # the option type already converted it. + value = count + count = int(value) + config.issue_config_time_warning( + TOML_NUMERIC_STRING.format( + name="tmp_path_retention_count", value=value + ), + stacklevel=2, + ) if count < 0: raise ValueError( f"tmp_path_retention_count must be >= 0. Current input: {count}." @@ -299,6 +311,7 @@ def pytest_addoption(parser: Parser) -> None: "tmp_path_retention_count", help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.", default=3, + # str: numeric strings in TOML are deprecated but still accepted. type=int | str, ) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 93177e12238..5c53e59236b 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -232,6 +232,20 @@ def test_1(tmp_path): # Check the base dir itself is gone without depending on test results assert base_dir == [] + def test_retention_count_toml_string_deprecated(self, pytester: Pytester) -> None: + pytester.maketoml('[pytest]\ntmp_path_retention_count = "2"\n') + pytester.makepyfile( + """ + def test_1(tmp_path_factory): + assert tmp_path_factory._retention_count == 2 + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*PytestRemovedIn10Warning: tmp_path_retention_count = '2' in TOML*"] + ) + @pytest.mark.parametrize("policy", ['"failed"', '"all"']) @pytest.mark.parametrize("count", [0, 1, 3]) def test_retention_count(self, pytester: Pytester, policy, count) -> None: