From 132fc1556adb6ed7c36dce04c60cff379ae9a4e9 Mon Sep 17 00:00:00 2001 From: Sagar-07star Date: Sat, 10 Oct 2026 20:48:55 +0530 Subject: [PATCH 1/3] Allow fixture and marker listing without required plugin options Temporarily relax required options for informational actions and restore them after parsing. Add regression tests for listing commands, normal runs, parser reuse, invalid values, option termination, and argument files. Fixes #3042 --- changelog/3042.bugfix.rst | 1 + src/_pytest/config/argparsing.py | 51 ++++++++++++++++++++++++++++++++ src/_pytest/fixtures.py | 5 ++-- src/_pytest/mark/__init__.py | 3 +- testing/test_config.py | 50 +++++++++++++++++++++++++++++++ testing/test_parseopt.py | 45 ++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 changelog/3042.bugfix.rst diff --git a/changelog/3042.bugfix.rst b/changelog/3042.bugfix.rst new file mode 100644 index 00000000000..fbab3303cdf --- /dev/null +++ b/changelog/3042.bugfix.rst @@ -0,0 +1 @@ +Allow ``--fixtures``, ``--funcargs``, ``--fixtures-per-test``, and ``--markers`` to run without supplying required plugin options. Required options remain enforced when running tests. diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 7180cde2c4c..ffc121d926f 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -3,7 +3,10 @@ import argparse from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator from collections.abc import Sequence +import contextlib import dataclasses import os import re @@ -512,6 +515,42 @@ def __init__( # an usage error to provide more contextual information to the user. self.extra_info = extra_info + def parse_known_args( + self, + args: Iterable[str] | None = None, + namespace: Any = None, + ) -> tuple[Any, list[str]]: + with self._restore_required_options(namespace): + return super().parse_known_args(args, namespace) + + def parse_known_intermixed_args( + self, + args: Iterable[str] | None = None, + namespace: Any = None, + ) -> tuple[Any, list[str]]: + with self._restore_required_options(namespace): + return super().parse_known_intermixed_args(args, namespace) + + @contextlib.contextmanager + def _restore_required_options( + self, namespace: argparse.Namespace | None + ) -> Iterator[None]: + # Informational actions temporarily relax required options. Restore + # them even if parsing fails, so subsequent parses still enforce them. + required_actions = [action for action in self._actions if action.required] + if namespace is not None and any( + isinstance(action, InformationalAction) + and getattr(namespace, action.dest, False) + for action in self._actions + ): + for action in required_actions: + action.required = False + try: + yield + finally: + for action in required_actions: + action.required = True + def error(self, message: str) -> NoReturn: """Transform argparse error message into UsageError.""" # TODO(py313): Replace with `exit_on_error=False`. Note that while it @@ -657,3 +696,15 @@ def __call__( current_overrides = [] current_overrides.append(f"{self.ini_option}={self.ini_value}") setattr(namespace, "override_ini", current_overrides) + + +class InformationalAction(argparse.Action): + """Set a listing flag without requiring options used to run tests.""" + + def __init__(self, option_strings, dest, default=False, **kwargs): + super().__init__(option_strings, dest, nargs=0, default=default, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, True) + for action in parser._actions: + action.required = False diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 20e43f89132..9dc927535c4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -54,6 +54,7 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl +from _pytest.config.argparsing import InformationalAction from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.deprecated import CLASS_FIXTURE_INSTANCE_METHOD @@ -1658,7 +1659,7 @@ def pytest_addoption(parser: Parser) -> None: group.addoption( "--fixtures", "--funcargs", - action="store_true", + action=InformationalAction, dest="showfixtures", default=False, help="Show available fixtures, sorted by plugin appearance " @@ -1666,7 +1667,7 @@ def pytest_addoption(parser: Parser) -> None: ) group.addoption( "--fixtures-per-test", - action="store_true", + action=InformationalAction, dest="show_fixtures_per_test", default=False, help="Show fixtures per test", diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 996322d93f1..ebbd1f6f328 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -25,6 +25,7 @@ from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.config.argparsing import InformationalAction from _pytest.config.argparsing import Parser from _pytest.stash import StashKey @@ -118,7 +119,7 @@ def pytest_addoption(parser: Parser) -> None: group.addoption( "--markers", - action="store_true", + action=InformationalAction, help="show markers (builtin, plugin and per-project ones).", ) diff --git a/testing/test_config.py b/testing/test_config.py index 282e66409f7..df66bdeec93 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -3645,3 +3645,53 @@ 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 + + +@pytest.mark.parametrize( + "flag", ["--fixtures", "--funcargs", "--fixtures-per-test", "--markers"] +) +@pytest.mark.parametrize("action", ["store", "store_true"]) +def test_information_with_required_option( + pytester: Pytester, flag: str, action: str +) -> None: + pytester.makeconftest( + f""" + import pytest + + def pytest_addoption(parser): + parser.addoption("--required", action="{action}", required=True) + + @pytest.fixture + def example_fixture(): + return 1 + """ + ) + pytester.makepyfile( + """ + def test_example(example_fixture): + assert example_fixture == 1 + """ + ) + result = pytester.runpytest(flag) + assert result.ret == pytest.ExitCode.OK + expected = "@pytest.mark.skip" if flag == "--markers" else "example_fixture" + assert expected in result.stdout.str() + + +@pytest.mark.parametrize("args", [[], ["--required", "present"]]) +def test_required_option_still_enforced(pytester: Pytester, args: list[str]) -> None: + pytester.makeconftest( + """ + def pytest_addoption(parser): + parser.addoption("--required", required=True) + """ + ) + pytester.makepyfile("def test_example(): pass") + result = pytester.runpytest(*args) + if args: + result.assert_outcomes(passed=1) + else: + assert result.ret == pytest.ExitCode.USAGE_ERROR + result.stderr.fnmatch_lines( + ["*the following arguments are required: --required*"] + ) diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 8457cc6e6ba..758b637dece 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -506,3 +506,48 @@ def test_does_not_break_on_hyphens(self) -> None: def test_empty_help(self) -> None: assert parseopt._split_help_text("", 40) == [] + + +@pytest.mark.parametrize("args", [["--list"], ["--list", "--required", "value"]]) +def test_informational_action_restores_required_options(parser, args): + parser.addoption("--list", action=parseopt.InformationalAction) + parser.addoption("--required", required=True) + namespace = parser.parse(args) + assert namespace.list is True + with pytest.raises( + UsageError, match="the following arguments are required: --required" + ): + parser.parse([]) + assert parser.parse(["--required", "value"]).required == "value" + + +@pytest.mark.parametrize( + "args", [["--list", "--choice", "bad"], ["--choice", "bad", "--list"]] +) +def test_informational_action_keeps_value_validation(parser, args): + parser.addoption("--list", action=parseopt.InformationalAction) + parser.addoption("--required", required=True) + parser.addoption("--choice", choices=["good"]) + with pytest.raises(UsageError, match="invalid choice"): + parser.parse(args) + with pytest.raises( + UsageError, match="the following arguments are required: --required" + ): + parser.parse([]) + + +def test_informational_action_after_option_terminator(parser): + parser.addoption("--list", action=parseopt.InformationalAction) + parser.addoption("--required", required=True) + with pytest.raises( + UsageError, match="the following arguments are required: --required" + ): + parser.parse(["--", "--list"]) + + +def test_informational_action_from_argument_file(parser, tmp_path): + parser.addoption("--list", action=parseopt.InformationalAction) + parser.addoption("--required", required=True) + args_file = tmp_path / "args.txt" + args_file.write_text("--list\n", encoding="utf-8") + assert parser.parse([f"@{args_file}"]).list is True From dcffeba1f266629e7a1859ad92880cfbf7c5ccf0 Mon Sep 17 00:00:00 2001 From: Sagar-07star Date: Sat, 10 Oct 2026 20:53:55 +0530 Subject: [PATCH 2/3] Add Sagar to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index bd1e27cb37d..b4b7c5c3a26 100644 --- a/AUTHORS +++ b/AUTHORS @@ -439,6 +439,7 @@ Russell Martin Ryan Puddephatt Ryan Wooden Sadra Barikbin +Sagar Saiprasad Kale Samuel Colvin Samuel Dion-Girardeau From dbcfe6dd32d430d8afc5d361e7c4f692a2c44d07 Mon Sep 17 00:00:00 2001 From: Sagar-07star Date: Sat, 10 Oct 2026 21:42:31 +0530 Subject: [PATCH 3/3] Filter argparse encoding warning in argument-file regression test --- testing/test_parseopt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 758b637dece..d7f15b86df8 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -545,6 +545,8 @@ def test_informational_action_after_option_terminator(parser): parser.parse(["--", "--list"]) +# Older argparse versions read argument files without an explicit encoding. +@pytest.mark.filterwarnings("ignore:'encoding' argument not specified:EncodingWarning") def test_informational_action_from_argument_file(parser, tmp_path): parser.addoption("--list", action=parseopt.InformationalAction) parser.addoption("--required", required=True)