Skip to content

Commit cdd40ed

Browse files
committed
Handle --version eagerly to avoid loading the entire infrastructure
Currently, handling `--version` in `pytest_cmdline_main` requires loading the entire infrastructure, which can be slow depending on the installed plugins. This change introduces a marginal behavioral difference, though it should not cause any issues in practice. Fixes #13574
1 parent 05ad730 commit cdd40ed

File tree

6 files changed

+52
-45
lines changed

6 files changed

+52
-45
lines changed

changelog/13574.improvement.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
The single argument ``--version`` no longer loads the entire plugin infrastructure, making it faster and more reliable when displaying only the pytest version.
2+
3+
Passing ``--version`` twice (e.g., ``pytest --version --version``) retains the original behavior, showing both the pytest version and plugin information.
4+
5+
.. note::
6+
7+
Since ``--version`` is now processed early, it only takes effect when passed directly via the command line. It will not work if set through other mechanisms, such as :envvar:`PYTEST_ADDOPTS` or :confval:`addopts`.

src/_pytest/config/__init__.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,17 @@ def main(
149149
150150
:returns: An exit code.
151151
"""
152+
# Handle a single `--version` argument early to avoid starting up the entire pytest infrastructure.
153+
new_args = sys.argv[1:] if args is None else args
154+
if isinstance(new_args, Sequence) and new_args.count("--version") == 1:
155+
sys.stdout.write(f"pytest {__version__}\n")
156+
return ExitCode.OK
157+
152158
old_pytest_version = os.environ.get("PYTEST_VERSION")
153159
try:
154160
os.environ["PYTEST_VERSION"] = __version__
155161
try:
156-
config = _prepareconfig(args, plugins)
162+
config = _prepareconfig(new_args, plugins)
157163
except ConftestImportFailure as e:
158164
exc_info = ExceptionInfo.from_exception(e.cause)
159165
tw = TerminalWriter(sys.stderr)
@@ -317,12 +323,10 @@ def get_plugin_manager() -> PytestPluginManager:
317323

318324

319325
def _prepareconfig(
320-
args: list[str] | os.PathLike[str] | None = None,
326+
args: list[str] | os.PathLike[str],
321327
plugins: Sequence[str | _PluggyPlugin] | None = None,
322328
) -> Config:
323-
if args is None:
324-
args = sys.argv[1:]
325-
elif isinstance(args, os.PathLike):
329+
if isinstance(args, os.PathLike):
326330
args = [os.fspath(args)]
327331
elif not isinstance(args, list):
328332
msg = ( # type:ignore[unreachable]
@@ -1145,13 +1149,15 @@ def pytest_cmdline_parse(
11451149
try:
11461150
self.parse(args)
11471151
except UsageError:
1148-
# Handle --version and --help here in a minimal fashion.
1152+
# Handle `--version --version` and `--help` here in a minimal fashion.
11491153
# This gets done via helpconfig normally, but its
11501154
# pytest_cmdline_main is not called in case of errors.
11511155
if getattr(self.option, "version", False) or "--version" in args:
1152-
from _pytest.helpconfig import showversion
1156+
from _pytest.helpconfig import show_version_verbose
11531157

1154-
showversion(self)
1158+
# Note that `--version` (single argument) is handled early by `Config.main()`, so the only
1159+
# way we are reaching this point is via `--version --version`.
1160+
show_version_verbose(self)
11551161
elif (
11561162
getattr(self.option, "help", False) or "--help" in args or "-h" in args
11571163
):

src/_pytest/helpconfig.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -140,28 +140,28 @@ def unset_tracing() -> None:
140140
return config
141141

142142

143-
def showversion(config: Config) -> None:
144-
if config.option.version > 1:
145-
sys.stdout.write(
146-
f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
147-
)
148-
plugininfo = getpluginversioninfo(config)
149-
if plugininfo:
150-
for line in plugininfo:
151-
sys.stdout.write(line + "\n")
152-
else:
153-
sys.stdout.write(f"pytest {pytest.__version__}\n")
143+
def show_version_verbose(config: Config) -> None:
144+
"""Show verbose pytest version installation, including plugins."""
145+
sys.stdout.write(
146+
f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
147+
)
148+
plugininfo = getpluginversioninfo(config)
149+
if plugininfo:
150+
for line in plugininfo:
151+
sys.stdout.write(line + "\n")
154152

155153

156154
def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
157-
if config.option.version > 0:
158-
showversion(config)
159-
return 0
155+
# Note: a single `--version` argument is handled directly by `Config.main()` to avoid starting up the entire
156+
# pytest infrastructure just to display the version (#13574).
157+
if config.option.version > 1:
158+
show_version_verbose(config)
159+
return ExitCode.OK
160160
elif config.option.help:
161161
config._do_configure()
162162
showhelp(config)
163163
config._ensure_unconfigure()
164-
return 0
164+
return ExitCode.OK
165165
return None
166166

167167

testing/test_capture.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ def bad_snap(self):
868868
FDCapture.snap = bad_snap
869869
"""
870870
)
871-
result = pytester.runpytest_subprocess("-p", "pytest_xyz", "--version")
871+
result = pytester.runpytest_subprocess("-p", "pytest_xyz")
872872
result.stderr.fnmatch_lines(
873873
["*in bad_snap", " raise Exception('boom')", "Exception: boom"]
874874
)

testing/test_config.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -612,20 +612,14 @@ def pytest_addoption(parser):
612612
assert config.getini("custom") == "1"
613613

614614
def test_absolute_win32_path(self, pytester: Pytester) -> None:
615-
temp_ini_file = pytester.makefile(
616-
".ini",
617-
custom="""
618-
[pytest]
619-
addopts = --version
620-
""",
621-
)
615+
temp_ini_file = pytester.makeini("[pytest]")
622616
from os.path import normpath
623617

624618
temp_ini_file_norm = normpath(str(temp_ini_file))
625619
ret = pytest.main(["-c", temp_ini_file_norm])
626-
assert ret == ExitCode.OK
620+
assert ret == ExitCode.NO_TESTS_COLLECTED
627621
ret = pytest.main(["--config-file", temp_ini_file_norm])
628-
assert ret == ExitCode.OK
622+
assert ret == ExitCode.NO_TESTS_COLLECTED
629623

630624

631625
class TestConfigAPI:
@@ -2121,7 +2115,7 @@ def pytest_addoption(parser):
21212115

21222116
result = pytester.runpytest("--version")
21232117
result.stdout.fnmatch_lines([f"pytest {pytest.__version__}"])
2124-
assert result.ret == ExitCode.USAGE_ERROR
2118+
assert result.ret == ExitCode.OK
21252119

21262120

21272121
def test_help_formatter_uses_py_get_terminal_width(monkeypatch: MonkeyPatch) -> None:

testing/test_helpconfig.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,29 @@ def test_version_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
1010
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
1111
monkeypatch.delenv("PYTEST_PLUGINS", raising=False)
1212
result = pytester.runpytest("--version", "--version")
13-
assert result.ret == 0
13+
assert result.ret == ExitCode.OK
1414
result.stdout.fnmatch_lines([f"*pytest*{pytest.__version__}*imported from*"])
1515
if pytestconfig.pluginmanager.list_plugin_distinfo():
1616
result.stdout.fnmatch_lines(["*registered third-party plugins:", "*at*"])
1717

1818

19-
def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None:
20-
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
21-
monkeypatch.delenv("PYTEST_PLUGINS", raising=False)
22-
result = pytester.runpytest("--version")
23-
assert result.ret == 0
24-
result.stdout.fnmatch_lines([f"pytest {pytest.__version__}"])
19+
def test_version_less_verbose(pytester: Pytester) -> None:
20+
"""Single ``--version`` parameter should display only the pytest version, without loading plugins (#13574)."""
21+
pytester.makeconftest("print('This should not be printed')")
22+
result = pytester.runpytest_subprocess("--version")
23+
assert result.ret == ExitCode.OK
24+
assert result.stdout.str().strip() == f"pytest {pytest.__version__}"
2525

2626

27-
def test_versions():
27+
def test_versions() -> None:
2828
"""Regression check for the public version attributes in pytest."""
2929
assert isinstance(pytest.__version__, str)
3030
assert isinstance(pytest.version_tuple, tuple)
3131

3232

3333
def test_help(pytester: Pytester) -> None:
3434
result = pytester.runpytest("--help")
35-
assert result.ret == 0
35+
assert result.ret == ExitCode.OK
3636
result.stdout.fnmatch_lines(
3737
"""
3838
-m MARKEXPR Only run tests matching given mark expression. For
@@ -73,7 +73,7 @@ def pytest_addoption(parser):
7373
"""
7474
)
7575
result = pytester.runpytest("--help")
76-
assert result.ret == 0
76+
assert result.ret == ExitCode.OK
7777
lines = [
7878
" required_plugins (args):",
7979
" Plugins that must be present for pytest to run*",
@@ -91,7 +91,7 @@ def pytest_hello(xyz):
9191
"""
9292
)
9393
result = pytester.runpytest()
94-
assert result.ret != 0
94+
assert result.ret != ExitCode.OK
9595
result.stdout.fnmatch_lines(["*unknown hook*pytest_hello*"])
9696

9797

0 commit comments

Comments
 (0)