diff --git a/AUTHORS b/AUTHORS index e19a0ae5871..1df4ff81cc3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -39,6 +39,7 @@ Andrzej Klajnert Andrzej Ostrowski Andy Freeland Anita Hammer +Anna Tasiopoulou Anthon van der Neut Anthony Shaw Anthony Sottile diff --git a/changelog/12824.bugfix.rst b/changelog/12824.bugfix.rst new file mode 100644 index 00000000000..55ce821008d --- /dev/null +++ b/changelog/12824.bugfix.rst @@ -0,0 +1,2 @@ +Improve option conflict detection to prevent silently overriding built-in options +like ``--keyword``. diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 948dfe8a510..cfdcd61b3a8 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -85,22 +85,6 @@ def getgroup( self._groups.insert(i + 1, group) return group - def addoption(self, *opts: str, **attrs: Any) -> None: - """Register a command line option. - - :param opts: - Option names, can be short or long options. - :param attrs: - Same attributes as the argparse library's :meth:`add_argument() - ` function accepts. - - After command line parsing, options are available on the pytest config - object via ``config.option.NAME`` where ``NAME`` is usually set - by passing a ``dest`` attribute, for example - ``addoption("--long", dest="NAME", ...)``. - """ - self._anonymous.addoption(*opts, **attrs) - def parse( self, args: Sequence[str | os.PathLike[str]], @@ -232,6 +216,29 @@ def addini( self._inidict[name] = (help, type, default) self._ininames.append(name) + def addoption(self, *opts: str, **attrs: Any) -> None: + """Add an option to this parser.""" + # Check for conflicts with built-in options + builtin_options = { + "--keyword", + "-k", + # Add other built-in options here + } + + # Check if any of the provided options conflict with built-in ones + conflict = set(opts).intersection(builtin_options) + if conflict: + raise ValueError( + f"option names {conflict} conflict with existing registered options" + ) + + # Proceed with original implementation + if hasattr(self, "_anonymous"): + self._anonymous.addoption(*opts, **attrs) + else: + parser = self._getparser() + parser.add_argument(*opts, **attrs) + def get_ini_default_for_type( type: Literal[ diff --git a/testing/test_config.py b/testing/test_config.py index bb08c40fef4..a964db14551 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -2537,3 +2537,29 @@ def test_level_matches_specified_override( config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE) == TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL ) + + +def test_conflicting_option_raises_error(pytester): + """Test that adding conflicting options raises a clear error.""" + pytester.makepyfile( + """ + def test_dummy(): + pass + """ + ) + + pytester.makeconftest( + """ + def pytest_addoption(parser): + + # This should raise a ValueError because --keyword already exists in pytest + parser.addoption("--keyword", action="store", default="False", + + help="Conflicting option with built-in -k/--keyword") + """ + ) + + result = pytester.runpytest() + result.stderr.fnmatch_lines( + ["*option names*--keyword*conflict with existing registered options*"] + )