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]
|
[pytest]
|
||||||
pythonpath = src1 src2
|
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
|
.. confval:: required_plugins
|
||||||
|
|
||||||
|
|
|
@ -268,7 +268,6 @@ default_plugins = (
|
||||||
"warnings",
|
"warnings",
|
||||||
"logging",
|
"logging",
|
||||||
"reports",
|
"reports",
|
||||||
"python_path",
|
|
||||||
"unraisableexception",
|
"unraisableexception",
|
||||||
"threadexception",
|
"threadexception",
|
||||||
"faulthandler",
|
"faulthandler",
|
||||||
|
@ -1245,6 +1244,9 @@ class Config:
|
||||||
self._parser.extra_info["inifile"] = str(self.inipath)
|
self._parser.extra_info["inifile"] = str(self.inipath)
|
||||||
self._parser.addini("addopts", "Extra command line options", "args")
|
self._parser.addini("addopts", "Extra command line options", "args")
|
||||||
self._parser.addini("minversion", "Minimally required pytest version")
|
self._parser.addini("minversion", "Minimally required pytest version")
|
||||||
|
self._parser.addini(
|
||||||
|
"pythonpath", type="paths", help="Add paths to sys.path", default=[]
|
||||||
|
)
|
||||||
self._parser.addini(
|
self._parser.addini(
|
||||||
"required_plugins",
|
"required_plugins",
|
||||||
"Plugins that must be present for pytest to run",
|
"Plugins that must be present for pytest to run",
|
||||||
|
@ -1294,6 +1296,18 @@ class Config:
|
||||||
for name in _iter_rewritable_modules(package_files):
|
for name in _iter_rewritable_modules(package_files):
|
||||||
hook.mark_rewrite(name)
|
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]:
|
def _validate_args(self, args: list[str], via: str) -> list[str]:
|
||||||
"""Validate known args."""
|
"""Validate known args."""
|
||||||
self._parser._config_source_hint = via # type: ignore
|
self._parser._config_source_hint = via # type: ignore
|
||||||
|
@ -1370,6 +1384,7 @@ class Config:
|
||||||
)
|
)
|
||||||
self._checkversion()
|
self._checkversion()
|
||||||
self._consider_importhook(args)
|
self._consider_importhook(args)
|
||||||
|
self._configure_python_path()
|
||||||
self.pluginmanager.consider_preparse(args, exclude_only=False)
|
self.pluginmanager.consider_preparse(args, exclude_only=False)
|
||||||
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||||
# Don't autoload from distribution package entry point. Only
|
# 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"),
|
("_pytest.config", "nonwrapper"),
|
||||||
(m.__module__, "nonwrapper"),
|
(m.__module__, "nonwrapper"),
|
||||||
("_pytest.legacypath", "nonwrapper"),
|
("_pytest.legacypath", "nonwrapper"),
|
||||||
("_pytest.python_path", "nonwrapper"),
|
|
||||||
("_pytest.capture", "wrapper"),
|
("_pytest.capture", "wrapper"),
|
||||||
("_pytest.warnings", "wrapper"),
|
("_pytest.warnings", "wrapper"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -62,6 +61,26 @@ def test_two_dirs(pytester: Pytester, file_structure) -> None:
|
||||||
result.assert_outcomes(passed=2)
|
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:
|
def test_module_not_found(pytester: Pytester, file_structure) -> None:
|
||||||
"""Without the pythonpath setting, the module should not be found."""
|
"""Without the pythonpath setting, the module should not be found."""
|
||||||
pytester.makefile(".ini", pytest="[pytest]\n")
|
pytester.makefile(".ini", pytest="[pytest]\n")
|
||||||
|
@ -95,16 +114,13 @@ def test_clean_up(pytester: Pytester) -> None:
|
||||||
after: list[str] | None = None
|
after: list[str] | None = None
|
||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
@pytest.hookimpl(tryfirst=True)
|
||||||
def pytest_unconfigure(self) -> Generator[None, None, None]:
|
def pytest_unconfigure(self) -> None:
|
||||||
nonlocal before, after
|
nonlocal before
|
||||||
before = sys.path.copy()
|
before = sys.path.copy()
|
||||||
try:
|
|
||||||
return (yield)
|
|
||||||
finally:
|
|
||||||
after = sys.path.copy()
|
|
||||||
|
|
||||||
result = pytester.runpytest_inprocess(plugins=[Plugin()])
|
result = pytester.runpytest_inprocess(plugins=[Plugin()])
|
||||||
|
after = sys.path.copy()
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
assert before is not None
|
assert before is not None
|
||||||
|
|
Loading…
Reference in New Issue