From 3abf79995d13f29f69133702d0cac51a2a3d1140 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 01:23:15 +0000 Subject: [PATCH 1/4] Allow int for ``max_warnings`` in TOML (#14953) The max_warnings option was registered without a type (defaulting to 'string'), so integer values in native TOML config raised a TypeError. It is now registered with type=int | str, accepting both int and string values in TOML while keeping the string form working for backward compatibility. An explicit integer 0 is distinguished from the unset default. Co-authored-by: Cursor Grok 4.6 --- changelog/14953.bugfix.rst | 1 + doc/en/reference/reference.rst | 4 +- src/_pytest/main.py | 4 ++ src/_pytest/terminal.py | 6 +-- testing/test_warnings.py | 96 ++++++++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 changelog/14953.bugfix.rst diff --git a/changelog/14953.bugfix.rst b/changelog/14953.bugfix.rst new file mode 100644 index 00000000000..bacb105878e --- /dev/null +++ b/changelog/14953.bugfix.rst @@ -0,0 +1 @@ +The :confval:`max_warnings` configuration option now accepts integer values in TOML configuration files, while still accepting strings for backward compatibility. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 362d5c917e2..62165abfb30 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1705,7 +1705,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: max_warnings - :type: ``int`` + :type: ``int | str`` .. versionadded:: 9.1 @@ -3671,7 +3671,7 @@ All the command-line flags can also be obtained by running ``pytest --help``:: Each line specifies a pattern for warnings.filterwarnings. Processed after -W/--pythonwarnings. - max_warnings (string): + max_warnings (int | string): Exit with error if all tests pass but the number of warnings exceeds this threshold norecursedirs (args): Directory patterns to avoid for recursion diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 1b337e20c7e..6f2e34ed56e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -141,9 +141,13 @@ def pytest_addoption(parser: Parser) -> None: "warnings.filterwarnings. " "Processed after -W/--pythonwarnings.", ) + # ``int | str`` (not plain ``int``) for backward compatibility: INI files + # and ``-o`` overrides provide the value as a string. parser.addini( "max_warnings", help="Exit with error if all tests pass but the number of warnings exceeds this threshold", + type=int | str, + default=None, ) group = parser.getgroup("collect", "collection") diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 825435225b3..26eb1d341cf 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1093,9 +1093,9 @@ def _get_max_warnings(self) -> int | None: if value is not None: return int(value) ini_value = self.config.getini("max_warnings") - if ini_value: - return int(ini_value) - return None + if ini_value is None: + return None + return int(ini_value) # # Summaries for sessionfinish. diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 017781c2355..402333bb298 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1005,6 +1005,12 @@ def test_two(): warnings.warn(UserWarning("warning two")) """ + ONE_WARNING_PYFILE = """ + import warnings + def test_warning(): + warnings.warn(UserWarning("example warning")) + """ + @pytest.mark.filterwarnings("default::UserWarning") def test_max_warnings_not_set(self, pytester: Pytester) -> None: """Without --max-warnings, warnings don't affect exit code.""" @@ -1145,6 +1151,96 @@ def test_one(): result.assert_outcomes(passed=1, warnings=1) assert result.ret == ExitCode.OK + @pytest.mark.filterwarnings("default::UserWarning") + @pytest.mark.parametrize( + ("writer", "source"), + [ + pytest.param( + "toml", + """ + [pytest] + max_warnings = 0 + """, + id="pytest-toml-int", + ), + pytest.param( + "toml", + """ + [pytest] + max_warnings = "0" + """, + id="pytest-toml-string", + ), + pytest.param( + "pyproject", + """ + [tool.pytest] + max_warnings = 0 + """, + id="pyproject-native-int", + ), + pytest.param( + "pyproject", + """ + [tool.pytest] + max_warnings = "0" + """, + id="pyproject-native-string", + ), + pytest.param( + "ini", + """ + [pytest] + max_warnings = 0 + """, + id="pytest-ini", + ), + ], + ) + def test_max_warnings_config_accepts_int_and_string( + self, pytester: Pytester, writer: str, source: str + ) -> None: + """Native TOML integers and quoted/INI strings are accepted (#14953). + + An explicit zero must be distinguished from the unset default. + """ + if writer == "toml": + pytester.maketoml(source) + elif writer == "pyproject": + pytester.makepyprojecttoml(source) + else: + assert writer == "ini" + pytester.makeini(source) + pytester.makepyfile(self.ONE_WARNING_PYFILE) + result = pytester.runpytest() + assert result.ret == ExitCode.MAX_WARNINGS_ERROR + result.stdout.fnmatch_lines( + ["*Tests pass, but maximum allowed warnings exceeded: 1 > 0*"] + ) + + @pytest.mark.filterwarnings("default::UserWarning") + def test_max_warnings_override_ini(self, pytester: Pytester) -> None: + """-o max_warnings=0 is accepted and treated as an explicit zero (#14953).""" + pytester.makepyfile(self.ONE_WARNING_PYFILE) + result = pytester.runpytest("-o", "max_warnings=0") + assert result.ret == ExitCode.MAX_WARNINGS_ERROR + result.stdout.fnmatch_lines( + ["*Tests pass, but maximum allowed warnings exceeded: 1 > 0*"] + ) + + @pytest.mark.filterwarnings("default::UserWarning") + def test_max_warnings_cli_overrides_ini(self, pytester: Pytester) -> None: + """--max-warnings takes precedence over the configuration value.""" + pytester.makeini( + """ + [pytest] + max_warnings = 10 + """ + ) + pytester.makepyfile(self.PYFILE) + result = pytester.runpytest("--max-warnings", "0") + assert result.ret == ExitCode.MAX_WARNINGS_ERROR + def test_pythonwarnings_not_duplicated(pytester: Pytester) -> None: """Regression test for #13484: -W values should not be duplicated in From 52e0c79ba6ca734c1c34569a688deca6479e68c6 Mon Sep 17 00:00:00 2001 From: CenFangyu <164994318+Dmao233@users.noreply.github.com> Date: Mon, 31 Aug 2026 01:54:40 +0000 Subject: [PATCH 2/4] Add Cen Fangyu to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index ba5672d4c51..093d5fd472a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -87,6 +87,7 @@ croc100 Cal Leeming Carl Friedrich Bolz Carlos Jenkins +Cen Fangyu (Dmao233) Ceridwen Charles Cloud Charles Machalow From f2141d76472a811a5d9a4a10ff31621e252f10e9 Mon Sep 17 00:00:00 2001 From: CenFangyu <164994318+Dmao233@users.noreply.github.com> Date: Mon, 31 Aug 2026 07:21:54 +0000 Subject: [PATCH 3/4] Remove extra max_warnings config tests Co-authored-by: CenFangyu --- testing/test_warnings.py | 96 ---------------------------------------- 1 file changed, 96 deletions(-) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 402333bb298..017781c2355 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1005,12 +1005,6 @@ def test_two(): warnings.warn(UserWarning("warning two")) """ - ONE_WARNING_PYFILE = """ - import warnings - def test_warning(): - warnings.warn(UserWarning("example warning")) - """ - @pytest.mark.filterwarnings("default::UserWarning") def test_max_warnings_not_set(self, pytester: Pytester) -> None: """Without --max-warnings, warnings don't affect exit code.""" @@ -1151,96 +1145,6 @@ def test_one(): result.assert_outcomes(passed=1, warnings=1) assert result.ret == ExitCode.OK - @pytest.mark.filterwarnings("default::UserWarning") - @pytest.mark.parametrize( - ("writer", "source"), - [ - pytest.param( - "toml", - """ - [pytest] - max_warnings = 0 - """, - id="pytest-toml-int", - ), - pytest.param( - "toml", - """ - [pytest] - max_warnings = "0" - """, - id="pytest-toml-string", - ), - pytest.param( - "pyproject", - """ - [tool.pytest] - max_warnings = 0 - """, - id="pyproject-native-int", - ), - pytest.param( - "pyproject", - """ - [tool.pytest] - max_warnings = "0" - """, - id="pyproject-native-string", - ), - pytest.param( - "ini", - """ - [pytest] - max_warnings = 0 - """, - id="pytest-ini", - ), - ], - ) - def test_max_warnings_config_accepts_int_and_string( - self, pytester: Pytester, writer: str, source: str - ) -> None: - """Native TOML integers and quoted/INI strings are accepted (#14953). - - An explicit zero must be distinguished from the unset default. - """ - if writer == "toml": - pytester.maketoml(source) - elif writer == "pyproject": - pytester.makepyprojecttoml(source) - else: - assert writer == "ini" - pytester.makeini(source) - pytester.makepyfile(self.ONE_WARNING_PYFILE) - result = pytester.runpytest() - assert result.ret == ExitCode.MAX_WARNINGS_ERROR - result.stdout.fnmatch_lines( - ["*Tests pass, but maximum allowed warnings exceeded: 1 > 0*"] - ) - - @pytest.mark.filterwarnings("default::UserWarning") - def test_max_warnings_override_ini(self, pytester: Pytester) -> None: - """-o max_warnings=0 is accepted and treated as an explicit zero (#14953).""" - pytester.makepyfile(self.ONE_WARNING_PYFILE) - result = pytester.runpytest("-o", "max_warnings=0") - assert result.ret == ExitCode.MAX_WARNINGS_ERROR - result.stdout.fnmatch_lines( - ["*Tests pass, but maximum allowed warnings exceeded: 1 > 0*"] - ) - - @pytest.mark.filterwarnings("default::UserWarning") - def test_max_warnings_cli_overrides_ini(self, pytester: Pytester) -> None: - """--max-warnings takes precedence over the configuration value.""" - pytester.makeini( - """ - [pytest] - max_warnings = 10 - """ - ) - pytester.makepyfile(self.PYFILE) - result = pytester.runpytest("--max-warnings", "0") - assert result.ret == ExitCode.MAX_WARNINGS_ERROR - def test_pythonwarnings_not_duplicated(pytester: Pytester) -> None: """Regression test for #13484: -W values should not be duplicated in From 72679ce2bd8a48d85dc2d4dc5d79fa6606ded3cb Mon Sep 17 00:00:00 2001 From: CenFangyu <164994318+Dmao233@users.noreply.github.com> Date: Mon, 31 Aug 2026 07:24:26 +0000 Subject: [PATCH 4/4] Restore native TOML max_warnings tests Co-authored-by: CenFangyu <164994318+Dmao233@users.noreply.github.com> --- testing/test_warnings.py | 96 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 017781c2355..402333bb298 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1005,6 +1005,12 @@ def test_two(): warnings.warn(UserWarning("warning two")) """ + ONE_WARNING_PYFILE = """ + import warnings + def test_warning(): + warnings.warn(UserWarning("example warning")) + """ + @pytest.mark.filterwarnings("default::UserWarning") def test_max_warnings_not_set(self, pytester: Pytester) -> None: """Without --max-warnings, warnings don't affect exit code.""" @@ -1145,6 +1151,96 @@ def test_one(): result.assert_outcomes(passed=1, warnings=1) assert result.ret == ExitCode.OK + @pytest.mark.filterwarnings("default::UserWarning") + @pytest.mark.parametrize( + ("writer", "source"), + [ + pytest.param( + "toml", + """ + [pytest] + max_warnings = 0 + """, + id="pytest-toml-int", + ), + pytest.param( + "toml", + """ + [pytest] + max_warnings = "0" + """, + id="pytest-toml-string", + ), + pytest.param( + "pyproject", + """ + [tool.pytest] + max_warnings = 0 + """, + id="pyproject-native-int", + ), + pytest.param( + "pyproject", + """ + [tool.pytest] + max_warnings = "0" + """, + id="pyproject-native-string", + ), + pytest.param( + "ini", + """ + [pytest] + max_warnings = 0 + """, + id="pytest-ini", + ), + ], + ) + def test_max_warnings_config_accepts_int_and_string( + self, pytester: Pytester, writer: str, source: str + ) -> None: + """Native TOML integers and quoted/INI strings are accepted (#14953). + + An explicit zero must be distinguished from the unset default. + """ + if writer == "toml": + pytester.maketoml(source) + elif writer == "pyproject": + pytester.makepyprojecttoml(source) + else: + assert writer == "ini" + pytester.makeini(source) + pytester.makepyfile(self.ONE_WARNING_PYFILE) + result = pytester.runpytest() + assert result.ret == ExitCode.MAX_WARNINGS_ERROR + result.stdout.fnmatch_lines( + ["*Tests pass, but maximum allowed warnings exceeded: 1 > 0*"] + ) + + @pytest.mark.filterwarnings("default::UserWarning") + def test_max_warnings_override_ini(self, pytester: Pytester) -> None: + """-o max_warnings=0 is accepted and treated as an explicit zero (#14953).""" + pytester.makepyfile(self.ONE_WARNING_PYFILE) + result = pytester.runpytest("-o", "max_warnings=0") + assert result.ret == ExitCode.MAX_WARNINGS_ERROR + result.stdout.fnmatch_lines( + ["*Tests pass, but maximum allowed warnings exceeded: 1 > 0*"] + ) + + @pytest.mark.filterwarnings("default::UserWarning") + def test_max_warnings_cli_overrides_ini(self, pytester: Pytester) -> None: + """--max-warnings takes precedence over the configuration value.""" + pytester.makeini( + """ + [pytest] + max_warnings = 10 + """ + ) + pytester.makepyfile(self.PYFILE) + result = pytester.runpytest("--max-warnings", "0") + assert result.ret == ExitCode.MAX_WARNINGS_ERROR + def test_pythonwarnings_not_duplicated(pytester: Pytester) -> None: """Regression test for #13484: -W values should not be duplicated in