From 3ce8f7498f3e1476eba1d89c45ac50cf08b8a7c6 Mon Sep 17 00:00:00 2001 From: Praneeth Kodumagulla Date: Wed, 6 May 2026 00:22:59 -0500 Subject: [PATCH 1/2] Fix strict options from addopts --- AUTHORS | 1 + changelog/14442.bugfix.rst | 1 + src/_pytest/config/__init__.py | 4 +++- testing/test_config.py | 14 ++++++++++++++ testing/test_mark.py | 25 +++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 changelog/14442.bugfix.rst diff --git a/AUTHORS b/AUTHORS index d6d2737a4bf..3c399ce1892 100644 --- a/AUTHORS +++ b/AUTHORS @@ -380,6 +380,7 @@ Piotr Banaszkiewicz Piotr Helm Poulami Sau Prakhar Gurunani +Praneeth Kodumagulla Prashant Anand Prashant Sharma Pulkit Goyal diff --git a/changelog/14442.bugfix.rst b/changelog/14442.bugfix.rst new file mode 100644 index 00000000000..ac517fcdf46 --- /dev/null +++ b/changelog/14442.bugfix.rst @@ -0,0 +1 @@ +Fixed a regression where ``--strict-markers`` and ``--strict-config`` specified through ``addopts`` were silently ignored. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 2dab4e279b5..b525d5a50e6 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -50,6 +50,7 @@ from .findpaths import ConfigDict from .findpaths import ConfigValue from .findpaths import determine_setup +from .findpaths import parse_override_ini from _pytest import __version__ import _pytest._code from _pytest._code import ExceptionInfo @@ -1520,6 +1521,8 @@ def parse(self, args: list[str], addopts: bool = True) -> None: self.known_args_namespace = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) + self._inicfg.update(parse_override_ini(self.known_args_namespace.override_ini)) + self._inicache.clear() self._checkversion() self._consider_importhook() self._configure_python_path() @@ -1541,7 +1544,6 @@ def parse(self, args: list[str], addopts: bool = True) -> None: self.known_args_namespace = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) - self._validate_plugins() self._warn_about_skipped_plugins() diff --git a/testing/test_config.py b/testing/test_config.py index 296461c12fc..14a54c957d2 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -491,6 +491,20 @@ def test_strict_config_ini_option( result.stderr.fnmatch_lines("ERROR: Unknown config option: unknown_option") assert result.ret == pytest.ExitCode.USAGE_ERROR + def test_strict_config_from_addopts(self, pytester: Pytester) -> None: + pytester.makeini( + """ + [pytest] + addopts = --strict-config + unknown_option = 1 + """ + ) + + result = pytester.runpytest() + + result.stderr.fnmatch_lines(["ERROR: Unknown config option: unknown_option"]) + assert result.ret == pytest.ExitCode.USAGE_ERROR + @pytest.mark.filterwarnings("default::pytest.PytestConfigWarning") def test_disable_warnings_plugin_disables_config_warnings( self, pytester: Pytester diff --git a/testing/test_mark.py b/testing/test_mark.py index 7ae82fefd3c..d41c9621d67 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -213,6 +213,31 @@ def test_hello(): ) +def test_strict_markers_from_addopts(pytester: Pytester) -> None: + pytester.makeini( + """ + [pytest] + addopts = --strict-markers + """ + ) + pytester.makepyfile( + """ + import pytest + + @pytest.mark.unregisteredmark + def test_hello(): + pass + """ + ) + + result = pytester.runpytest() + + result.stdout.fnmatch_lines( + ["'unregisteredmark' not found in `markers` configuration option"] + ) + assert result.ret == pytest.ExitCode.INTERRUPTED + + @pytest.mark.parametrize( ("expr", "expected_passed"), [ From 08f1e7a2f6e90856b8074cb60d4733b4604627d4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 8 May 2026 22:58:50 +0300 Subject: [PATCH 2/2] Tweaks --- changelog/14442.bugfix.rst | 4 ++- src/_pytest/config/__init__.py | 9 +++++-- testing/test_config.py | 29 ++++++++------------- testing/test_mark.py | 46 ++++++++++------------------------ 4 files changed, 33 insertions(+), 55 deletions(-) diff --git a/changelog/14442.bugfix.rst b/changelog/14442.bugfix.rst index ac517fcdf46..90999cc9572 100644 --- a/changelog/14442.bugfix.rst +++ b/changelog/14442.bugfix.rst @@ -1 +1,3 @@ -Fixed a regression where ``--strict-markers`` and ``--strict-config`` specified through ``addopts`` were silently ignored. +Fixed a regression in pytest 9.0 where :option:`--strict-markers` and :option:`--strict-config` specified through :confval:`addopts` were silently ignored. + +Note that when targeting pytest >= 9.0, it's nicer to use :confval:`strict_markers` and :confval:`strict_config`, or :ref:`strict mode `. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b525d5a50e6..44d606b00a4 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1521,8 +1521,12 @@ def parse(self, args: list[str], addopts: bool = True) -> None: self.known_args_namespace = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) - self._inicfg.update(parse_override_ini(self.known_args_namespace.override_ini)) - self._inicache.clear() + if addopts: + # addopts may have added overrides (especially via OverrideIniAction). + # The thing can be endlessly circular but we only do one level (#14442). + if overrides := parse_override_ini(self.known_args_namespace.override_ini): + self._inicfg.update(overrides) + self._inicache.clear() self._checkversion() self._consider_importhook() self._configure_python_path() @@ -1544,6 +1548,7 @@ def parse(self, args: list[str], addopts: bool = True) -> None: self.known_args_namespace = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) + self._validate_plugins() self._warn_about_skipped_plugins() diff --git a/testing/test_config.py b/testing/test_config.py index 14a54c957d2..8026c108db0 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -475,36 +475,27 @@ def test_silence_unknown_key_warning(self, pytester: Pytester) -> None: result = pytester.runpytest() result.stdout.no_fnmatch_line("*PytestConfigWarning*") - @pytest.mark.parametrize("option_name", ["strict_config", "strict"]) - def test_strict_config_ini_option( - self, pytester: Pytester, option_name: str - ) -> None: + @pytest.mark.parametrize( + "option", + [ + "strict_config = true", + "strict = true", + "addopts = --strict-config", + ], + ) + def test_strict_config_ini_option(self, pytester: Pytester, option: str) -> None: """Test that strict_config and strict ini options enable strict config checking.""" pytester.makeini( f""" [pytest] unknown_option = 1 - {option_name} = True + {option} """ ) result = pytester.runpytest() result.stderr.fnmatch_lines("ERROR: Unknown config option: unknown_option") assert result.ret == pytest.ExitCode.USAGE_ERROR - def test_strict_config_from_addopts(self, pytester: Pytester) -> None: - pytester.makeini( - """ - [pytest] - addopts = --strict-config - unknown_option = 1 - """ - ) - - result = pytester.runpytest() - - result.stderr.fnmatch_lines(["ERROR: Unknown config option: unknown_option"]) - assert result.ret == pytest.ExitCode.USAGE_ERROR - @pytest.mark.filterwarnings("default::pytest.PytestConfigWarning") def test_disable_warnings_plugin_disables_config_warnings( self, pytester: Pytester diff --git a/testing/test_mark.py b/testing/test_mark.py index d41c9621d67..fd1e92a88cd 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -184,11 +184,16 @@ def test_hello(): @pytest.mark.parametrize( - "option_name", ["--strict-markers", "--strict", "strict_markers", "strict"] + "option", + [ + "--strict-markers", + "--strict", + "strict_markers = true", + "strict = true", + "addopts = --strict-markers", + ], ) -def test_strict_prohibits_unregistered_markers( - pytester: Pytester, option_name: str -) -> None: +def test_strict_prohibits_unregistered_markers(pytester: Pytester, option: str) -> None: pytester.makepyfile( """ import pytest @@ -197,47 +202,22 @@ def test_hello(): pass """ ) - if option_name in ("strict_markers", "strict"): + if option.startswith("-"): + result = pytester.runpytest(option) + else: pytester.makeini( f""" [pytest] - {option_name} = true + {option} """ ) result = pytester.runpytest() - else: - result = pytester.runpytest(option_name) assert result.ret != 0 result.stdout.fnmatch_lines( ["'unregisteredmark' not found in `markers` configuration option"] ) -def test_strict_markers_from_addopts(pytester: Pytester) -> None: - pytester.makeini( - """ - [pytest] - addopts = --strict-markers - """ - ) - pytester.makepyfile( - """ - import pytest - - @pytest.mark.unregisteredmark - def test_hello(): - pass - """ - ) - - result = pytester.runpytest() - - result.stdout.fnmatch_lines( - ["'unregisteredmark' not found in `markers` configuration option"] - ) - assert result.ret == pytest.ExitCode.INTERRUPTED - - @pytest.mark.parametrize( ("expr", "expected_passed"), [