From 9ad8b9fc36f4f62e085b1ad9a8b8bdf041cd8fa9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 9 Jan 2022 23:17:48 +0200 Subject: [PATCH 1/8] hookspec: remove explicit `:param` types Duplicates info in the type annotations which sphinx understands. --- src/_pytest/hookspec.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index c1963b4d7..a5c9aff70 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -54,7 +54,7 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: """Called at plugin registration time to allow adding new hooks via a call to :func:`pluginmanager.add_hookspecs(module_or_class, prefix) `. - :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager. + :param pluginmanager: The pytest plugin manager. .. note:: This hook is incompatible with hook wrappers. @@ -68,7 +68,7 @@ def pytest_plugin_registered( """A new pytest plugin got registered. :param plugin: The plugin module or instance. - :param pytest.PytestPluginManager manager: pytest plugin manager. + :param manager: pytest plugin manager. .. note:: This hook is incompatible with hook wrappers. @@ -86,13 +86,13 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> files situated at the tests root directory due to how pytest :ref:`discovers plugins during startup `. - :param pytest.Parser parser: + :param parser: To add command line options, call :py:func:`parser.addoption(...) `. To add ini-file values call :py:func:`parser.addini(...) `. - :param pytest.PytestPluginManager pluginmanager: + :param pluginmanager: The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks to change how command line options are added. @@ -127,7 +127,7 @@ def pytest_configure(config: "Config") -> None: .. note:: This hook is incompatible with hook wrappers. - :param pytest.Config config: The pytest config object. + :param config: The pytest config object. """ @@ -435,7 +435,7 @@ def pytest_make_parametrize_id( :param config: The pytest config object. :param val: The parametrized value. - :param str argname: The automatic parameter name produced by pytest. + :param argname: The automatic parameter name produced by pytest. """ From dd1447cfe509fac989258e078a73609e0d122a73 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 10 Jan 2022 00:04:20 +0200 Subject: [PATCH 2/8] hookspec: move pytest_load_initial_conftests up Reflect the order in which the plugins are called. --- src/_pytest/hookspec.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index a5c9aff70..f0c579c7b 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -156,18 +156,6 @@ def pytest_cmdline_parse( """ -@hookspec(firstresult=True) -def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]: - """Called for performing the main command line action. The default - implementation will invoke the configure hooks and runtest_mainloop. - - Stops at first non-None result, see :ref:`firstresult`. - - :param config: The pytest config object. - :returns: The exit code. - """ - - def pytest_load_initial_conftests( early_config: "Config", parser: "Parser", args: List[str] ) -> None: @@ -183,6 +171,18 @@ def pytest_load_initial_conftests( """ +@hookspec(firstresult=True) +def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]: + """Called for performing the main command line action. The default + implementation will invoke the configure hooks and runtest_mainloop. + + Stops at first non-None result, see :ref:`firstresult`. + + :param config: The pytest config object. + :returns: The exit code. + """ + + # ------------------------------------------------------------------------- # collection hooks # ------------------------------------------------------------------------- From c973ccb622da11181bbb820fa5861e825982f271 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 15 Jan 2024 23:46:59 +0200 Subject: [PATCH 3/8] hookspec: modernize a reference --- src/_pytest/hookspec.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index f0c579c7b..404eb4e96 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -173,8 +173,10 @@ def pytest_load_initial_conftests( @hookspec(firstresult=True) def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]: - """Called for performing the main command line action. The default - implementation will invoke the configure hooks and runtest_mainloop. + """Called for performing the main command line action. + + The default implementation will invoke the configure hooks and + :hook:`pytest_runtestloop`. Stops at first non-None result, see :ref:`firstresult`. From e895c9d38cf4e66912c60c9c8307efcb9f82df4c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jan 2024 14:53:10 +0100 Subject: [PATCH 4/8] doc: Remove sold out training (#11823) --- doc/en/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index 3fb095a34..bc0bd56bf 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -3,7 +3,6 @@ .. sidebar:: Next Open Trainings and Events - `Professional Testing with Python `_, via `Python Academy `_ (3 day in-depth training): - * **March 5th to 7th 2024**, Leipzig, Germany / Remote * **June 11th to 13th 2024**, Remote * **March 4th to 6th 2025**, Leipzig, Germany / Remote - `pytest development sprint `_, June 2024 (`date poll `_) From 6e9f566d7919b52be266c365ea65cbab79108469 Mon Sep 17 00:00:00 2001 From: woutdenolf Date: Tue, 16 Jan 2024 15:41:01 +0100 Subject: [PATCH 5/8] avoid using __file__ in pytest_plugin_registered as can be wrong on Windows --- src/_pytest/fixtures.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c294ec586..46b201184 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1485,23 +1485,25 @@ class FixtureManager: def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: nodeid = None - try: - p = absolutepath(plugin.__file__) # type: ignore[attr-defined] - except AttributeError: - pass - else: - # Construct the base nodeid which is later used to check - # what fixtures are visible for particular tests (as denoted - # by their test id). - if p.name == "conftest.py": - try: - nodeid = str(p.parent.relative_to(self.config.rootpath)) - except ValueError: - nodeid = "" - if nodeid == ".": - nodeid = "" - if os.sep != nodes.SEP: - nodeid = nodeid.replace(os.sep, nodes.SEP) + plugin_name = self.config.pluginmanager.get_name(plugin) + + # Construct the base nodeid which is later used to check + # what fixtures are visible for particular tests (as denoted + # by their test id). + if plugin_name and plugin_name.endswith("conftest.py"): + # The plugin name is assumed to be equal to plugin.__file__ + # for conftest plugins. The difference is that plugin_name + # has the correct capitalization on capital-insensitive + # systems (Windows). + p = absolutepath(plugin_name) + try: + nodeid = str(p.parent.relative_to(self.config.rootpath)) + except ValueError: + nodeid = "" + if nodeid == ".": + nodeid = "" + if os.sep != nodes.SEP: + nodeid = nodeid.replace(os.sep, nodes.SEP) self.parsefactories(plugin, nodeid) From 6b9bba2edb12a1b6679476aefe20c85dfb652ea0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 17 Jan 2024 15:00:09 +0200 Subject: [PATCH 6/8] pre-commit: add pluggy to mypy deps Otherwise mypy doesn't fully recognize pluggy's typing for some reason or another. --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be0b4315b..fe6ed99ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,6 +64,7 @@ repos: additional_dependencies: - iniconfig>=1.1.0 - attrs>=19.2.0 + - pluggy - packaging - tomli - types-pkg_resources From 0f5ecd83c432bc56e39549fe7353d06a92455a2a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 17 Jan 2024 14:47:07 +0200 Subject: [PATCH 7/8] hookspecs: add `plugin_name` parameter to the `pytest_plugin_registered` hook We have a use case for this in the next commit. The name can be obtained by using `manager.get_name(plugin)`, however this is currently O(num plugins) in pluggy, which would be good to avoid. Besides, it seems generally useful. --- changelog/11825.improvement.rst | 1 + src/_pytest/config/__init__.py | 12 ++++++++---- src/_pytest/hookspec.py | 7 +++++-- 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 changelog/11825.improvement.rst diff --git a/changelog/11825.improvement.rst b/changelog/11825.improvement.rst new file mode 100644 index 000000000..afd85a041 --- /dev/null +++ b/changelog/11825.improvement.rst @@ -0,0 +1 @@ +The :hook:`pytest_plugin_registered` hook has a new ``plugin_name`` parameter containing the name by which ``plugin`` is registered. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 698616664..157c36490 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -490,15 +490,19 @@ class PytestPluginManager(PluginManager): ) ) return None - ret: Optional[str] = super().register(plugin, name) - if ret: + plugin_name = super().register(plugin, name) + if plugin_name is not None: self.hook.pytest_plugin_registered.call_historic( - kwargs=dict(plugin=plugin, manager=self) + kwargs=dict( + plugin=plugin, + plugin_name=plugin_name, + manager=self, + ) ) if isinstance(plugin, types.ModuleType): self.consider_module(plugin) - return ret + return plugin_name def getplugin(self, name: str): # Support deprecated naming because plugins (xdist e.g.) use it. diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 404eb4e96..c4cce2d83 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -63,12 +63,15 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: @hookspec(historic=True) def pytest_plugin_registered( - plugin: "_PluggyPlugin", manager: "PytestPluginManager" + plugin: "_PluggyPlugin", + plugin_name: str, + manager: "PytestPluginManager", ) -> None: """A new pytest plugin got registered. :param plugin: The plugin module or instance. - :param manager: pytest plugin manager. + :param plugin_name: The name by which the plugin is registered. + :param manager: The pytest plugin manager. .. note:: This hook is incompatible with hook wrappers. From 9ea2e0a79f8d45b801b4bff8e4ad0aefd09fab21 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 17 Jan 2024 14:49:17 +0200 Subject: [PATCH 8/8] fixtures: avoid slow `pm.get_name(plugin)` call by using the new `plugin_name` hook parameter --- src/_pytest/fixtures.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 46b201184..51c573575 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1483,27 +1483,27 @@ class FixtureManager: return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) - def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: - nodeid = None - plugin_name = self.config.pluginmanager.get_name(plugin) - - # Construct the base nodeid which is later used to check - # what fixtures are visible for particular tests (as denoted - # by their test id). + def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None: + # Fixtures defined in conftest plugins are only visible to within the + # conftest's directory. This is unlike fixtures in non-conftest plugins + # which have global visibility. So for conftests, construct the base + # nodeid from the plugin name (which is the conftest path). if plugin_name and plugin_name.endswith("conftest.py"): - # The plugin name is assumed to be equal to plugin.__file__ - # for conftest plugins. The difference is that plugin_name - # has the correct capitalization on capital-insensitive - # systems (Windows). - p = absolutepath(plugin_name) + # Note: we explicitly do *not* use `plugin.__file__` here -- The + # difference is that plugin_name has the correct capitalization on + # case-insensitive systems (Windows) and other normalization issues + # (issue #11816). + conftestpath = absolutepath(plugin_name) try: - nodeid = str(p.parent.relative_to(self.config.rootpath)) + nodeid = str(conftestpath.parent.relative_to(self.config.rootpath)) except ValueError: nodeid = "" if nodeid == ".": nodeid = "" if os.sep != nodes.SEP: nodeid = nodeid.replace(os.sep, nodes.SEP) + else: + nodeid = None self.parsefactories(plugin, nodeid)