Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ Mandeep Bhutani
Manuel Krebber
Marc Mueller
Marc Schlaich
Marcel Telka
Marcelo Duarte Trevisani
Marcin Augustynów
Marcin Bachry
Expand Down
5 changes: 5 additions & 0 deletions changelog/12624.improvement.rst
Original file line number Diff line number Diff line change
@@ -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`.
6 changes: 3 additions & 3 deletions doc/en/how-to/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ manually specify each plugin with :option:`-p` or :envvar:`PYTEST_PLUGINS`, you

* :option:`-p` loads (or disables with ``-p no:<name>``) 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`),
Expand Down
7 changes: 7 additions & 0 deletions doc/en/how-to/writing_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -1228,14 +1233,19 @@ 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

export PYTEST_PLUGINS=mymodule.plugin,xdist

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 <https://pygments.org/docs/styles/>`_ to use for the code output.
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down
17 changes: 5 additions & 12 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import importlib.metadata
import os
from pathlib import Path
import platform
import re
import sys
import textwrap
Expand Down Expand Up @@ -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:
Expand Down
80 changes: 80 additions & 0 deletions testing/test_pluginmanager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# mypy: allow-untyped-defs
from __future__ import annotations

import importlib.metadata
import os
import shutil
import sys
Expand Down Expand Up @@ -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(
Expand Down