Add a `pythonpath` setting to allow paths to be added to `sys.path`. (#9134)
This commit is contained in:
parent
05a97375fd
commit
c82bda259c
|
@ -0,0 +1 @@
|
||||||
|
Added :confval:`pythonpath` setting that adds listed paths to :data:`sys.path` for the duration of the test session. If you currently use the pytest-pythonpath or pytest-srcpaths plugins, you should be able to replace them with built-in `pythonpath` setting.
|
|
@ -1676,6 +1676,21 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
See :ref:`change naming conventions` for more detailed examples.
|
See :ref:`change naming conventions` for more detailed examples.
|
||||||
|
|
||||||
|
|
||||||
|
.. confval:: pythonpath
|
||||||
|
|
||||||
|
Sets list of directories that should be added to the python search path.
|
||||||
|
Directories will be added to the head of :data:`sys.path`.
|
||||||
|
Similar to the :envvar:`PYTHONPATH` environment variable, the directories will be
|
||||||
|
included in where Python will look for imported modules.
|
||||||
|
Paths are relative to the :ref:`rootdir <rootdir>` directory.
|
||||||
|
Directories remain in path for the duration of the test session.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
pythonpath = src1 src2
|
||||||
|
|
||||||
|
|
||||||
.. confval:: required_plugins
|
.. confval:: required_plugins
|
||||||
|
|
||||||
A space separated list of plugins that must be present for pytest to run.
|
A space separated list of plugins that must be present for pytest to run.
|
||||||
|
|
|
@ -254,6 +254,7 @@ default_plugins = essential_plugins + (
|
||||||
"warnings",
|
"warnings",
|
||||||
"logging",
|
"logging",
|
||||||
"reports",
|
"reports",
|
||||||
|
"pythonpath",
|
||||||
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
|
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
|
||||||
"faulthandler",
|
"faulthandler",
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
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)
|
|
@ -1268,7 +1268,13 @@ def test_load_initial_conftest_last_ordering(_config_for_test):
|
||||||
pm.register(m)
|
pm.register(m)
|
||||||
hc = pm.hook.pytest_load_initial_conftests
|
hc = pm.hook.pytest_load_initial_conftests
|
||||||
values = hc._nonwrappers + hc._wrappers
|
values = hc._nonwrappers + hc._wrappers
|
||||||
expected = ["_pytest.config", m.__module__, "_pytest.capture", "_pytest.warnings"]
|
expected = [
|
||||||
|
"_pytest.config",
|
||||||
|
m.__module__,
|
||||||
|
"_pytest.pythonpath",
|
||||||
|
"_pytest.capture",
|
||||||
|
"_pytest.warnings",
|
||||||
|
]
|
||||||
assert [x.function.__module__ for x in values] == expected
|
assert [x.function.__module__ for x in values] == expected
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
import sys
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import Generator
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from _pytest.pytester import Pytester
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def file_structure(pytester: Pytester) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
test_foo="""
|
||||||
|
from foo import foo
|
||||||
|
|
||||||
|
def test_foo():
|
||||||
|
assert foo() == 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
pytester.makepyfile(
|
||||||
|
test_bar="""
|
||||||
|
from bar import bar
|
||||||
|
|
||||||
|
def test_bar():
|
||||||
|
assert bar() == 2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
foo_py = pytester.mkdir("sub") / "foo.py"
|
||||||
|
content = dedent(
|
||||||
|
"""
|
||||||
|
def foo():
|
||||||
|
return 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
foo_py.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
bar_py = pytester.mkdir("sub2") / "bar.py"
|
||||||
|
content = dedent(
|
||||||
|
"""
|
||||||
|
def bar():
|
||||||
|
return 2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
bar_py.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_dir(pytester: Pytester, file_structure) -> None:
|
||||||
|
pytester.makefile(".ini", pytest="[pytest]\npythonpath=sub\n")
|
||||||
|
result = pytester.runpytest("test_foo.py")
|
||||||
|
assert result.ret == 0
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_two_dirs(pytester: Pytester, file_structure) -> None:
|
||||||
|
pytester.makefile(".ini", pytest="[pytest]\npythonpath=sub sub2\n")
|
||||||
|
result = pytester.runpytest("test_foo.py", "test_bar.py")
|
||||||
|
assert result.ret == 0
|
||||||
|
result.assert_outcomes(passed=2)
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
result = pytester.runpytest("test_foo.py")
|
||||||
|
assert result.ret == pytest.ExitCode.INTERRUPTED
|
||||||
|
result.assert_outcomes(errors=1)
|
||||||
|
expected_error = "E ModuleNotFoundError: No module named 'foo'"
|
||||||
|
result.stdout.fnmatch_lines([expected_error])
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_ini(pytester: Pytester, file_structure) -> None:
|
||||||
|
"""If no ini file, test should error."""
|
||||||
|
result = pytester.runpytest("test_foo.py")
|
||||||
|
assert result.ret == pytest.ExitCode.INTERRUPTED
|
||||||
|
result.assert_outcomes(errors=1)
|
||||||
|
expected_error = "E ModuleNotFoundError: No module named 'foo'"
|
||||||
|
result.stdout.fnmatch_lines([expected_error])
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_up(pytester: Pytester) -> None:
|
||||||
|
"""Test that the pythonpath plugin cleans up after itself."""
|
||||||
|
# This is tough to test behaviorly because the cleanup really runs last.
|
||||||
|
# So the test make several implementation assumptions:
|
||||||
|
# - Cleanup is done in pytest_unconfigure().
|
||||||
|
# - Not a hookwrapper.
|
||||||
|
# So we can add a hookwrapper ourselves to test what it does.
|
||||||
|
pytester.makefile(".ini", pytest="[pytest]\npythonpath=I_SHALL_BE_REMOVED\n")
|
||||||
|
pytester.makepyfile(test_foo="""def test_foo(): pass""")
|
||||||
|
|
||||||
|
before: Optional[List[str]] = None
|
||||||
|
after: Optional[List[str]] = None
|
||||||
|
|
||||||
|
class Plugin:
|
||||||
|
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||||
|
def pytest_unconfigure(self) -> Generator[None, None, None]:
|
||||||
|
nonlocal before, after
|
||||||
|
before = sys.path.copy()
|
||||||
|
yield
|
||||||
|
after = sys.path.copy()
|
||||||
|
|
||||||
|
result = pytester.runpytest_inprocess(plugins=[Plugin()])
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
assert before is not None
|
||||||
|
assert after is not None
|
||||||
|
assert any("I_SHALL_BE_REMOVED" in entry for entry in before)
|
||||||
|
assert not any("I_SHALL_BE_REMOVED" in entry for entry in after)
|
Loading…
Reference in New Issue