From 51ef94439c0b3c6b3c1804796bf6a4372a0160fe Mon Sep 17 00:00:00 2001 From: Jeremy Hiatt Date: Tue, 27 Feb 2024 20:31:00 -0800 Subject: [PATCH 1/2] Avoid modifying path placeholders created by editable installs The setuptools implementation of editable installs will insert a placeholder entry into sys.path as part of its magic to register its custom import mechanism. These are not real filesystem paths and as such should not be rewritten to absolute paths. --- changelog/1028.bugfix | 1 + src/xdist/looponfail.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog/1028.bugfix diff --git a/changelog/1028.bugfix b/changelog/1028.bugfix new file mode 100644 index 00000000..39bc7a95 --- /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 7ef24c15..370cb8bf 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 From 79fb6ff9c0c2bc0db97a40ce3a81806e376d4807 Mon Sep 17 00:00:00 2001 From: Jeremy Hiatt Date: Wed, 28 Feb 2024 15:11:07 -0800 Subject: [PATCH 2/2] testing: Add test to assert path hooks ignored in sys.path --- testing/test_looponfail.py | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/testing/test_looponfail.py b/testing/test_looponfail.py index 92d43848..65a89fbf 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 @@ def test_func(): 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: