diff --git a/AUTHORS b/AUTHORS index 33ee644ea68..e98670e92b6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -295,6 +295,7 @@ Mandeep Bhutani Manuel Krebber Marc Mueller Marc Schlaich +Marcel Telka Marcelo Duarte Trevisani Marcin Augustynów Marcin Bachry diff --git a/changelog/12624.improvement.rst b/changelog/12624.improvement.rst new file mode 100644 index 00000000000..df87da9aa14 --- /dev/null +++ b/changelog/12624.improvement.rst @@ -0,0 +1,5 @@ +The :globalvar:`pytest_plugins` variable and the :envvar:`PYTEST_PLUGINS` environment variable now also accept entry point names of installed plugins (e.g. ``xdist``), in addition to importable module names, matching the behavior of the :option:`-p` command-line option. + +As part of this change, plugins specified by entry point name through these mechanisms are now also listed in the ``plugins:`` line of the terminal header (:issue:`12615`). + +Based on the original implementation by Marcel Telka in :pr:`12616`. diff --git a/doc/en/how-to/plugins.rst b/doc/en/how-to/plugins.rst index f3d4b0040b9..41d65bea7a4 100644 --- a/doc/en/how-to/plugins.rst +++ b/doc/en/how-to/plugins.rst @@ -188,9 +188,9 @@ manually specify each plugin with :option:`-p` or :envvar:`PYTEST_PLUGINS`, you * :option:`-p` loads (or disables with ``-p no:``) a plugin by name or entry point for a specific pytest invocation, and is processed early during startup. - * :envvar:`PYTEST_PLUGINS` is a comma-separated list of Python modules that are imported - and registered as plugins during startup. This mechanism is commonly used by test - suites, for example when testing a plugin. + * :envvar:`PYTEST_PLUGINS` is a comma-separated list of Python modules or plugin + entry point names that are loaded and registered as plugins during startup. + This mechanism is commonly used by test suites, for example when testing a plugin. When explicitly controlling plugin loading (especially with :envvar:`PYTEST_DISABLE_PLUGIN_AUTOLOAD` or :option:`--disable-plugin-autoload`), diff --git a/doc/en/how-to/writing_plugins.rst b/doc/en/how-to/writing_plugins.rst index 56043a14f97..aba3f1a8263 100644 --- a/doc/en/how-to/writing_plugins.rst +++ b/doc/en/how-to/writing_plugins.rst @@ -251,6 +251,13 @@ application modules: pytest_plugins = "myapp.testsupport.myplugin" +The entry point name of an installed plugin can also be used, just like with +the :option:`-p` command-line option: + +.. code-block:: python + + pytest_plugins = ("xdist",) + :globalvar:`pytest_plugins` are processed recursively, so note that in the example above if ``myapp.testsupport.myplugin`` also declares :globalvar:`pytest_plugins`, the contents of the variable will also be loaded as plugins, and so on. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 1eec97a5366..078ce608ec7 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1151,6 +1151,7 @@ contain glob patterns. Can be declared at the **global** level in *test modules* and *conftest.py files* to register additional plugins. Can be either a ``str`` or ``Sequence[str]``. +Each entry can be the name of an importable module or the entry point name of an installed plugin. .. code-block:: python @@ -1160,6 +1161,10 @@ Can be either a ``str`` or ``Sequence[str]``. pytest_plugins = ("myapp.testsupport.tools", "myapp.testsupport.regression") +.. versionchanged:: 9.2 + Entry point names of installed plugins are now also accepted, in addition + to importable module names. + .. globalvar:: pytestmark @@ -1228,7 +1233,8 @@ Environment variables that can be used to change pytest's behavior. .. envvar:: PYTEST_PLUGINS - Contains comma-separated list of modules that should be loaded as plugins: + Contains comma-separated list of modules or plugin entry point names that + should be loaded as plugins: .. code-block:: bash @@ -1236,6 +1242,10 @@ Environment variables that can be used to change pytest's behavior. See also :option:`-p`. + .. versionchanged:: 9.2 + Entry point names of installed plugins are now also accepted, in + addition to importable module names. + .. envvar:: PYTEST_THEME Sets a `pygment style `_ to use for the code output. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 96b3dd6a86a..3a10c3aa8bb 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -882,7 +882,7 @@ def _import_plugin_specs( ) -> None: plugins = _get_plugin_specs_as_list(spec) for import_spec in plugins: - self.import_plugin(import_spec) + self.import_plugin(import_spec, consider_entry_points=True) def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None: """Import a plugin with ``modname``. diff --git a/testing/test_config.py b/testing/test_config.py index 9583c9131fa..14946dde164 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -6,7 +6,6 @@ import importlib.metadata import os from pathlib import Path -import platform import re import sys import textwrap @@ -1711,17 +1710,11 @@ def distributions(): and not (enable_plugin_method in ("env_var", "") and not disable_plugin_method) ) - # __spec__ is accessed in AssertionRewritingHook.exec_module, which would be - # eventually called if we did a full pytest run; but it's only accessed with - # enable_plugin_method=="env_var" because that will early-load it. - # Except when autoloads aren't disabled, in which case PytestPluginManager.import_plugin - # bails out before importing it.. because it knows it'll be loaded later? - # The above seems a bit weird, but I *think* it's true. - if platform.python_implementation() != "PyPy": - assert ("__spec__" in PseudoPlugin.attrs_used) == bool( - enable_plugin_method == "env_var" and disable_plugin_method - ) - # __spec__ is present when testing locally on pypy, but not in CI ???? + # __spec__ is accessed in AssertionRewritingHook.exec_module, which is never + # reached here: since PYTEST_PLUGINS also considers entry points (#12624), + # the plugin is loaded through its entry point (like with -p) instead of + # being imported through the rewrite hook. + assert "__spec__" not in PseudoPlugin.attrs_used def test_plugin_loading_order(pytester: Pytester) -> None: diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 385d907d481..70c1cde2821 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs from __future__ import annotations +import importlib.metadata import os import shutil import sys @@ -324,6 +325,85 @@ def test_consider_env_fails_to_import( with pytest.raises(ImportError): pytestpm.consider_env() + def test_consider_env_entry_point_name( + self, monkeypatch: MonkeyPatch, pytestpm: PytestPluginManager + ) -> None: + """PYTEST_PLUGINS accepts entry point names of installed plugins, + in addition to importable module names (#12624).""" + plugin = types.ModuleType("mytestplugin_module") + + class DummyEntryPoint: + name = "mytestplugin" + group = "pytest11" + + def load(self): + return plugin + + class DummyDist: + version = "1.0" + files = () + metadata = {"name": "mytestplugin"} + entry_points = (DummyEntryPoint(),) + + monkeypatch.setattr(importlib.metadata, "distributions", lambda: (DummyDist(),)) + monkeypatch.setenv("PYTEST_PLUGINS", "mytestplugin") + pytestpm.consider_env() + assert pytestpm.get_plugin("mytestplugin") is plugin + + def test_consider_module_entry_point_name( + self, monkeypatch: MonkeyPatch, pytestpm: PytestPluginManager + ) -> None: + """pytest_plugins accepts entry point names of installed plugins, + in addition to importable module names (#12624).""" + plugin = types.ModuleType("mytestplugin_module") + + class DummyEntryPoint: + name = "mytestplugin" + group = "pytest11" + + def load(self): + return plugin + + class DummyDist: + version = "1.0" + files = () + metadata = {"name": "mytestplugin"} + entry_points = (DummyEntryPoint(),) + + monkeypatch.setattr(importlib.metadata, "distributions", lambda: (DummyDist(),)) + mod = types.ModuleType("temp") + mod.__dict__["pytest_plugins"] = ["mytestplugin"] + pytestpm.consider_module(mod) + assert pytestpm.get_plugin("mytestplugin") is plugin + + def test_consider_env_entry_point_reported_in_header( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: + """Plugins loaded by entry point name via PYTEST_PLUGINS are shown in + the terminal header, like with -p (#12615).""" + plugin = types.ModuleType("mytestplugin_module") + + class DummyEntryPoint: + name = "mytestplugin" + group = "pytest11" + + def load(self): + return plugin + + class DummyDist: + version = "1.2.3" + files = () + metadata = {"name": "mytestplugin"} + entry_points = (DummyEntryPoint(),) + + monkeypatch.setattr(importlib.metadata, "distributions", lambda: (DummyDist(),)) + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + monkeypatch.setenv("PYTEST_PLUGINS", "mytestplugin") + pytester.makepyfile("def test_ok(): pass") + result = pytester.runpytest_inprocess() + assert result.ret == ExitCode.OK + result.stdout.fnmatch_lines(["plugins: *mytestplugin-1.2.3*"]) + @pytest.mark.filterwarnings("always") def test_plugin_skip(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: p = pytester.makepyfile(