Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a pythonpath setting to allow paths to be added to sys.path. #9134

Merged
merged 13 commits into from Oct 5, 2021
1 change: 1 addition & 0 deletions changelog/9114.feature.rst
@@ -0,0 +1 @@
Added :ref:`pythonpath` setting that adds listed paths to `sys.path`.
okken marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions src/_pytest/config/__init__.py
Expand Up @@ -256,6 +256,7 @@ def directory_arg(path: str, optname: str) -> str:
"reports",
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
"faulthandler",
"pythonpath",
bluetech marked this conversation as resolved.
Show resolved Hide resolved
)

builtin_plugins = set(default_plugins)
Expand Down
19 changes: 19 additions & 0 deletions src/_pytest/pythonpath.py
@@ -0,0 +1,19 @@
import sys
from typing import List

import pytest
okken marked this conversation as resolved.
Show resolved Hide resolved
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, parser: Parser, args: List[str]
okken marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
"""`pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`"""
okken marked this conversation as resolved.
Show resolved Hide resolved
for path in reversed(early_config.getini("pythonpath")):
sys.path.insert(0, str(path))
8 changes: 7 additions & 1 deletion testing/test_config.py
Expand Up @@ -1268,7 +1268,13 @@ def pytest_load_initial_conftests(self):
pm.register(m)
hc = pm.hook.pytest_load_initial_conftests
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


Expand Down
81 changes: 81 additions & 0 deletions testing/test_pythonpath.py
@@ -0,0 +1,81 @@
import sys
from textwrap import dedent

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")
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")
result.assert_outcomes(passed=2)


def test_module_not_found(pytester: Pytester, file_structure) -> None:
"""If pythonpath setting not there, test should error."""
okken marked this conversation as resolved.
Show resolved Hide resolved
pytester.makefile(".ini", pytest="[pytest]\n")
result = pytester.runpytest("test_foo.py")
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")
result.assert_outcomes(errors=1)
expected_error = "E ModuleNotFoundError: No module named 'foo'"
result.stdout.fnmatch_lines([expected_error])


def test_path_removal(pytester: Pytester, file_structure) -> None:
"""Make sure sys.path is cleaned up after testing."""
original = sys.path.copy()
pytester.makefile(".ini", pytest="[pytest]\npythonpath=sub sub2\n")
pytester.runpytest()
assert sys.path == original
okken marked this conversation as resolved.
Show resolved Hide resolved