Skip to content

Commit 6c04963

Browse files
committed
Load plugins from paths in 'pythonpath' option
1 parent f0a0436 commit 6c04963

File tree

6 files changed

+43
-41
lines changed

6 files changed

+43
-41
lines changed

changelog/11118.improvement.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Allow plugins to be loaded with `-p` from paths specified in `pythonpath`.
2+
3+
-- by :user:`millerdev`

doc/en/reference/reference.rst

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,11 +1796,6 @@ passed multiple times. The expected format is ``name=value``. For example::
17961796
[pytest]
17971797
pythonpath = src1 src2
17981798
1799-
.. note::
1800-
1801-
``pythonpath`` does not affect some imports that happen very early,
1802-
most notably plugins loaded using the ``-p`` command line option.
1803-
18041799
18051800
.. confval:: required_plugins
18061801

src/_pytest/config/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,6 @@ def directory_arg(path: str, optname: str) -> str:
268268
"warnings",
269269
"logging",
270270
"reports",
271-
"python_path",
272271
"unraisableexception",
273272
"threadexception",
274273
"faulthandler",
@@ -1245,6 +1244,9 @@ def _initini(self, args: Sequence[str]) -> None:
12451244
self._parser.extra_info["inifile"] = str(self.inipath)
12461245
self._parser.addini("addopts", "Extra command line options", "args")
12471246
self._parser.addini("minversion", "Minimally required pytest version")
1247+
self._parser.addini(
1248+
"pythonpath", type="paths", help="Add paths to sys.path", default=[]
1249+
)
12481250
self._parser.addini(
12491251
"required_plugins",
12501252
"Plugins that must be present for pytest to run",
@@ -1294,6 +1296,18 @@ def _mark_plugins_for_rewrite(self, hook) -> None:
12941296
for name in _iter_rewritable_modules(package_files):
12951297
hook.mark_rewrite(name)
12961298

1299+
def _configure_python_path(self) -> None:
1300+
# `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`
1301+
for path in reversed(self.getini("pythonpath")):
1302+
sys.path.insert(0, str(path))
1303+
self.add_cleanup(self._unconfigure_python_path)
1304+
1305+
def _unconfigure_python_path(self) -> None:
1306+
for path in self.getini("pythonpath"):
1307+
path_str = str(path)
1308+
if path_str in sys.path:
1309+
sys.path.remove(path_str)
1310+
12971311
def _validate_args(self, args: list[str], via: str) -> list[str]:
12981312
"""Validate known args."""
12991313
self._parser._config_source_hint = via # type: ignore
@@ -1370,6 +1384,7 @@ def _preparse(self, args: list[str], addopts: bool = True) -> None:
13701384
)
13711385
self._checkversion()
13721386
self._consider_importhook(args)
1387+
self._configure_python_path()
13731388
self.pluginmanager.consider_preparse(args, exclude_only=False)
13741389
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
13751390
# Don't autoload from distribution package entry point. Only

src/_pytest/python_path.py

Lines changed: 0 additions & 26 deletions
This file was deleted.

testing/test_config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1409,7 +1409,6 @@ def pytest_load_initial_conftests(self):
14091409
("_pytest.config", "nonwrapper"),
14101410
(m.__module__, "nonwrapper"),
14111411
("_pytest.legacypath", "nonwrapper"),
1412-
("_pytest.python_path", "nonwrapper"),
14131412
("_pytest.capture", "wrapper"),
14141413
("_pytest.warnings", "wrapper"),
14151414
]

testing/test_python_path.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import sys
55
from textwrap import dedent
6-
from typing import Generator
76

87
from _pytest.pytester import Pytester
98
import pytest
@@ -62,6 +61,26 @@ def test_two_dirs(pytester: Pytester, file_structure) -> None:
6261
result.assert_outcomes(passed=2)
6362

6463

64+
def test_local_plugin(pytester: Pytester, file_structure) -> None:
65+
localplugin_py = pytester.path / "sub" / "localplugin.py"
66+
content = dedent(
67+
"""
68+
def pytest_load_initial_conftests():
69+
print("local plugin load")
70+
71+
def pytest_unconfigure():
72+
print("local plugin unconfig")
73+
"""
74+
)
75+
localplugin_py.write_text(content, encoding="utf-8")
76+
77+
pytester.makefile(".ini", pytest="[pytest]\npythonpath=sub\n")
78+
result = pytester.runpytest("-plocalplugin", "-s", "test_foo.py")
79+
result.stdout.fnmatch_lines(["local plugin load", "local plugin unconfig"])
80+
assert result.ret == 0
81+
result.assert_outcomes(passed=1)
82+
83+
6584
def test_module_not_found(pytester: Pytester, file_structure) -> None:
6685
"""Without the pythonpath setting, the module should not be found."""
6786
pytester.makefile(".ini", pytest="[pytest]\n")
@@ -95,16 +114,13 @@ def test_clean_up(pytester: Pytester) -> None:
95114
after: list[str] | None = None
96115

97116
class Plugin:
98-
@pytest.hookimpl(wrapper=True, tryfirst=True)
99-
def pytest_unconfigure(self) -> Generator[None, None, None]:
100-
nonlocal before, after
117+
@pytest.hookimpl(tryfirst=True)
118+
def pytest_unconfigure(self) -> None:
119+
nonlocal before
101120
before = sys.path.copy()
102-
try:
103-
return (yield)
104-
finally:
105-
after = sys.path.copy()
106121

107122
result = pytester.runpytest_inprocess(plugins=[Plugin()])
123+
after = sys.path.copy()
108124
assert result.ret == 0
109125

110126
assert before is not None

0 commit comments

Comments
 (0)