diff --git a/changelog/1028.bugfix b/changelog/1028.bugfix new file mode 100644 index 0000000..39bc7a9 --- /dev/null +++ b/changelog/1028.bugfix @@ -0,0 +1 @@ +Fix compatiblity issue between `looponfail` and editable installs. diff --git a/src/xdist/looponfail.py b/src/xdist/looponfail.py index 7ef24c1..370cb8b 100644 --- a/src/xdist/looponfail.py +++ b/src/xdist/looponfail.py @@ -160,7 +160,8 @@ def init_worker_session(channel, args, option_dict): newpaths = [] for p in sys.path: if p: - if not os.path.isabs(p): + # Ignore path placeholders created for editable installs + if not os.path.isabs(p) and not p.endswith(".__path_hook__"): p = os.path.abspath(p) newpaths.append(p) sys.path[:] = newpaths diff --git a/testing/test_looponfail.py b/testing/test_looponfail.py index 92d4384..65a89fb 100644 --- a/testing/test_looponfail.py +++ b/testing/test_looponfail.py @@ -1,3 +1,5 @@ +import pathlib +import tempfile import unittest.mock from typing import List @@ -191,6 +193,43 @@ class TestRemoteControl: control.loop_once() assert control.failures + def test_ignore_sys_path_hook_entry( + self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + # Modifying sys.path as seen by the worker process is a bit tricky, + # because any changes made in the current process do not carry over. + # However, we can leverage the `sitecustomize` behavior to run arbitrary + # code when the subprocess interpreter is starting up. We just need to + # install our module in the search path, which we can accomplish by + # adding a temporary directory to PYTHONPATH. + tmpdir = tempfile.TemporaryDirectory() + with open(pathlib.Path(tmpdir.name) / "sitecustomize.py", "w") as custom: + print( + textwrap.dedent( + """ + import sys + sys.path.append('dummy.__path_hook__') + """ + ), + file=custom, + ) + + monkeypatch.setenv("PYTHONPATH", tmpdir.name, prepend=":") + + item = pytester.getitem( + textwrap.dedent( + """ + def test_func(): + import sys + assert "dummy.__path_hook__" in sys.path + """ + ) + ) + control = RemoteControl(item.config) + control.setup() + topdir, failures = control.runsession()[:2] + assert not failures + class TestLooponFailing: def test_looponfail_from_fail_to_ok(self, pytester: pytest.Pytester) -> None: