Repository: pytest-dev/pytest-xdist
Pull request #1030 (2024-02-28 23:19:13)

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.

This is a proposed fix for #1028 .

Checklist:
> - [ ] Make sure to include reasonable tests for your change if necessary

I couldn't figure out how to test this specific behavior, but happy to add one given guidance on how to modify `sys.path` in the test environment.

> - [x] We use [towncrier](https://pypi.python.org/pypi/towncrier) for changelog management, so please add a *news* file

Added.




Patch:
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,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


Test patch:
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: