Merge 2b319c15e6
into ac41898755
This commit is contained in:
commit
2b75475017
|
@ -0,0 +1,3 @@
|
|||
Allow plugins to be loaded with `-p` from paths specified in `pythonpath`.
|
||||
|
||||
-- by :user:`millerdev`
|
|
@ -1789,11 +1789,6 @@ passed multiple times. The expected format is ``name=value``. For example::
|
|||
[pytest]
|
||||
pythonpath = src1 src2
|
||||
|
||||
.. note::
|
||||
|
||||
``pythonpath`` does not affect some imports that happen very early,
|
||||
most notably plugins loaded using the ``-p`` command line option.
|
||||
|
||||
|
||||
.. confval:: required_plugins
|
||||
|
||||
|
|
|
@ -268,7 +268,6 @@ default_plugins = (
|
|||
"warnings",
|
||||
"logging",
|
||||
"reports",
|
||||
"python_path",
|
||||
"unraisableexception",
|
||||
"threadexception",
|
||||
"faulthandler",
|
||||
|
@ -1245,6 +1244,9 @@ class Config:
|
|||
self._parser.extra_info["inifile"] = str(self.inipath)
|
||||
self._parser.addini("addopts", "Extra command line options", "args")
|
||||
self._parser.addini("minversion", "Minimally required pytest version")
|
||||
self._parser.addini(
|
||||
"pythonpath", type="paths", help="Add paths to sys.path", default=[]
|
||||
)
|
||||
self._parser.addini(
|
||||
"required_plugins",
|
||||
"Plugins that must be present for pytest to run",
|
||||
|
@ -1294,6 +1296,18 @@ class Config:
|
|||
for name in _iter_rewritable_modules(package_files):
|
||||
hook.mark_rewrite(name)
|
||||
|
||||
def _configure_python_path(self):
|
||||
# `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`
|
||||
for path in reversed(self.getini("pythonpath")):
|
||||
sys.path.insert(0, str(path))
|
||||
self.add_cleanup(self._unconfigure_python_path)
|
||||
|
||||
def _unconfigure_python_path(self):
|
||||
for path in self.getini("pythonpath"):
|
||||
path_str = str(path)
|
||||
if path_str in sys.path:
|
||||
sys.path.remove(path_str)
|
||||
|
||||
def _validate_args(self, args: list[str], via: str) -> list[str]:
|
||||
"""Validate known args."""
|
||||
self._parser._config_source_hint = via # type: ignore
|
||||
|
@ -1370,6 +1384,7 @@ class Config:
|
|||
)
|
||||
self._checkversion()
|
||||
self._consider_importhook(args)
|
||||
self._configure_python_path()
|
||||
self.pluginmanager.consider_preparse(args, exclude_only=False)
|
||||
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||
# Don't autoload from distribution package entry point. Only
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from pytest import Config
|
||||
from pytest import Parser
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
parser.addini("pythonpath", type="paths", help="Add paths to sys.path", default=[])
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_load_initial_conftests(early_config: Config) -> None:
|
||||
# `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`
|
||||
for path in reversed(early_config.getini("pythonpath")):
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_unconfigure(config: Config) -> None:
|
||||
for path in config.getini("pythonpath"):
|
||||
path_str = str(path)
|
||||
if path_str in sys.path:
|
||||
sys.path.remove(path_str)
|
|
@ -1409,7 +1409,6 @@ def test_load_initial_conftest_last_ordering(_config_for_test):
|
|||
("_pytest.config", "nonwrapper"),
|
||||
(m.__module__, "nonwrapper"),
|
||||
("_pytest.legacypath", "nonwrapper"),
|
||||
("_pytest.python_path", "nonwrapper"),
|
||||
("_pytest.capture", "wrapper"),
|
||||
("_pytest.warnings", "wrapper"),
|
||||
]
|
||||
|
|
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
|||
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
from typing import Generator
|
||||
|
||||
from _pytest.pytester import Pytester
|
||||
import pytest
|
||||
|
@ -62,6 +61,26 @@ def test_two_dirs(pytester: Pytester, file_structure) -> None:
|
|||
result.assert_outcomes(passed=2)
|
||||
|
||||
|
||||
def test_local_plugin(pytester: Pytester, file_structure) -> None:
|
||||
localplugin_py = pytester.path / "sub" / "localplugin.py"
|
||||
content = dedent(
|
||||
"""
|
||||
def pytest_load_initial_conftests():
|
||||
print("local plugin load")
|
||||
|
||||
def pytest_unconfigure():
|
||||
print("local plugin unconfig")
|
||||
"""
|
||||
)
|
||||
localplugin_py.write_text(content, encoding="utf-8")
|
||||
|
||||
pytester.makefile(".ini", pytest="[pytest]\npythonpath=sub\n")
|
||||
result = pytester.runpytest("-plocalplugin", "-s", "test_foo.py")
|
||||
result.stdout.fnmatch_lines(["local plugin load", "local plugin unconfig"])
|
||||
assert result.ret == 0
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
|
||||
def test_module_not_found(pytester: Pytester, file_structure) -> None:
|
||||
"""Without the pythonpath setting, the module should not be found."""
|
||||
pytester.makefile(".ini", pytest="[pytest]\n")
|
||||
|
@ -95,16 +114,13 @@ def test_clean_up(pytester: Pytester) -> None:
|
|||
after: list[str] | None = None
|
||||
|
||||
class Plugin:
|
||||
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_unconfigure(self) -> Generator[None, None, None]:
|
||||
nonlocal before, after
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_unconfigure(self) -> None:
|
||||
nonlocal before
|
||||
before = sys.path.copy()
|
||||
try:
|
||||
return (yield)
|
||||
finally:
|
||||
after = sys.path.copy()
|
||||
|
||||
result = pytester.runpytest_inprocess(plugins=[Plugin()])
|
||||
after = sys.path.copy()
|
||||
assert result.ret == 0
|
||||
|
||||
assert before is not None
|
||||
|
|
Loading…
Reference in New Issue