diff --git a/changelog/9396.bugfix.rst b/changelog/9396.bugfix.rst new file mode 100644 index 00000000000..dcb83bbc14f --- /dev/null +++ b/changelog/9396.bugfix.rst @@ -0,0 +1 @@ +Ensure :attr:`pytest.Config.inifile` is available during the :func:`pytest_cmdline_main <_pytest.hookspec.pytest_cmdline_main>` hook (regression during ``7.0.0rc1``). diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 4c2db941257..37e8c24220e 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -403,37 +403,17 @@ def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None: self.path = Path(value) -@hookimpl -def pytest_configure(config: Config) -> None: - import pytest - - mp = pytest.MonkeyPatch() - config.add_cleanup(mp.undo) - - if config.pluginmanager.has_plugin("tmpdir"): - # Create TmpdirFactory and attach it to the config object. - # - # This is to comply with existing plugins which expect the handler to be - # available at pytest_configure time, but ideally should be moved entirely - # to the tmpdir_factory session fixture. - try: - tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined] - except AttributeError: - # tmpdir plugin is blocked. - pass - else: - _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) - mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) - - config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir") +@hookimpl(tryfirst=True) +def pytest_load_initial_conftests(early_config: Config) -> None: + """Monkeypatch legacy path attributes in several classes, as early as possible.""" + mp = MonkeyPatch() + early_config.add_cleanup(mp.undo) # Add Cache.makedir(). - mp.setattr(pytest.Cache, "makedir", Cache_makedir, raising=False) + mp.setattr(Cache, "makedir", Cache_makedir, raising=False) # Add FixtureRequest.fspath property. - mp.setattr( - pytest.FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False - ) + mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False) # Add TerminalReporter.startdir property. mp.setattr( @@ -441,22 +421,43 @@ def pytest_configure(config: Config) -> None: ) # Add Config.{invocation_dir,rootdir,inifile} properties. - mp.setattr( - pytest.Config, "invocation_dir", property(Config_invocation_dir), raising=False - ) - mp.setattr(pytest.Config, "rootdir", property(Config_rootdir), raising=False) - mp.setattr(pytest.Config, "inifile", property(Config_inifile), raising=False) + mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False) + mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False) + mp.setattr(Config, "inifile", property(Config_inifile), raising=False) # Add Session.startdir property. - mp.setattr(pytest.Session, "startdir", property(Session_stardir), raising=False) + mp.setattr(Session, "startdir", property(Session_stardir), raising=False) # Add pathlist configuration type. - mp.setattr(pytest.Config, "_getini_unknown_type", Config__getini_unknown_type) + mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type) # Add Node.fspath property. mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False) +@hookimpl +def pytest_configure(config: Config) -> None: + """Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed.""" + if config.pluginmanager.has_plugin("tmpdir"): + mp = MonkeyPatch() + config.add_cleanup(mp.undo) + # Create TmpdirFactory and attach it to the config object. + # + # This is to comply with existing plugins which expect the handler to be + # available at pytest_configure time, but ideally should be moved entirely + # to the tmpdir_factory session fixture. + try: + tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined] + except AttributeError: + # tmpdir plugin is blocked. + pass + else: + _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) + mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) + + config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir") + + @hookimpl def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None: # pytester is not loaded by default and is commonly loaded from a conftest, diff --git a/testing/test_config.py b/testing/test_config.py index 9a57b919dd9..f691d3ed5bb 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1268,6 +1268,7 @@ def pytest_load_initial_conftests(self): expected = [ "_pytest.config", m.__module__, + "_pytest.legacypath", "_pytest.pythonpath", "_pytest.capture", "_pytest.warnings", diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 1d3fdb4bd9c..fbfd88b7384 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -161,3 +161,20 @@ def test_overriden(pytestconfig): ) result = pytester.runpytest("--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s") result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"]) + + +def test_inifile_from_cmdline_main_hook(pytester: pytest.Pytester) -> None: + """Ensure Config.inifile is available during pytest_cmdline_main (#9396).""" + p = pytester.makeini( + """ + [pytest] + """ + ) + pytester.makeconftest( + """ + def pytest_cmdline_main(config): + print("pytest_cmdline_main inifile =", config.inifile) + """ + ) + result = pytester.runpytest_subprocess("-s") + result.stdout.fnmatch_lines(f"*pytest_cmdline_main inifile = {p}")