https://github.com/python/cpython/commit/3f02aab9faa338853425da23541e44e4bd24d478
commit: 3f02aab9faa338853425da23541e44e4bd24d478
branch: main
author: he_tao <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-10-05T14:51:20+01:00
summary:

gh-158540: Add the profiled script's directory to sys.path (#158548)

* gh-158540: Add the profiled script's directory to sys.path

When a script is profiled with ``python -m profiling.sampling run
script.py`` from another directory, the script cannot import modules
placed next to it, because ``_sync_coordinator._execute_script()``
executes it with the working directory (added by
``_setup_environment()`` for the module case) as ``sys.path[0]``
instead of the script's own directory.

Make the script's directory importable in ``_execute_script()``,
matching the behavior of ``python script.py``.

Add a regression test that runs the coordinator on a script importing
a sibling module.

* Update _sync_coordinator.py Comment simplified

Co-authored-by: Eduardo Villalpando Mello <[email protected]>

* gh-158540: Resolve symlinks when adding the script directory to sys.path

``python script.py`` resolves symlinks when computing ``sys.path[0]``, so
a script reached through a symlink (``link.py -> sub/where.py``) imports
modules from the real script's directory.  Apply ``os.path.realpath()``
before taking the directory name, and make sure the result is placed at
the front of ``sys.path`` even if it was already listed.

Add a regression test for a symlinked script.

---------

Co-authored-by: Eduardo Villalpando Mello <[email protected]>

files:
A Lib/test/test_profiling/test_sampling_profiler/test_sync_coordinator.py
A Misc/NEWS.d/next/Library/2026-09-30-22-10-00.gh-issue-158540.kQ9vXm.rst
M Lib/profiling/sampling/_sync_coordinator.py

diff --git a/Lib/profiling/sampling/_sync_coordinator.py 
b/Lib/profiling/sampling/_sync_coordinator.py
index a1cce314b33b19..bb0ee5b5317d69 100644
--- a/Lib/profiling/sampling/_sync_coordinator.py
+++ b/Lib/profiling/sampling/_sync_coordinator.py
@@ -168,6 +168,11 @@ def _execute_script(script_path: str, script_args: 
List[str], cwd: str) -> None:
     if not os.path.isfile(script_path):
         raise TargetError(f"Script not found: {script_path}")
 
+    script_dir = os.path.dirname(os.path.realpath(script_path))
+    if script_dir in sys.path:
+        sys.path.remove(script_dir)
+    sys.path.insert(0, script_dir)
+
     # Replace sys.argv to match original script call
     sys.argv = [script_path] + script_args
 
diff --git 
a/Lib/test/test_profiling/test_sampling_profiler/test_sync_coordinator.py 
b/Lib/test/test_profiling/test_sampling_profiler/test_sync_coordinator.py
new file mode 100644
index 00000000000000..936c01e206f9fb
--- /dev/null
+++ b/Lib/test/test_profiling/test_sampling_profiler/test_sync_coordinator.py
@@ -0,0 +1,113 @@
+"""Tests for the sampling profiler's synchronization coordinator."""
+
+import os
+import socket
+import subprocess
+import sys
+import tempfile
+import unittest
+
+from test.support import SHORT_TIMEOUT, os_helper, requires_subprocess
+
+
+@requires_subprocess()
+class TestSyncCoordinatorScriptExecution(unittest.TestCase):
+    """Tests for how the coordinator executes a target script."""
+
+    def run_coordinator(self, cwd, target, *target_args):
+        """Execute *target* from *cwd* with the sync coordinator.
+
+        Returns the ``(stdout, stderr)`` captured from the coordinator.
+        """
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
+            server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            server.bind(("127.0.0.1", 0))
+            server.listen(1)
+            server.settimeout(SHORT_TIMEOUT)
+            port = server.getsockname()[1]
+
+            cmd = (
+                sys.executable,
+                "-m",
+                "profiling.sampling._sync_coordinator",
+                str(port),
+                cwd,
+                target,
+            ) + tuple(target_args)
+            process = subprocess.Popen(
+                cmd,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                text=True,
+            )
+            try:
+                conn, _ = server.accept()
+                with conn:
+                    # _signal_readiness() sends b"ready" before running target.
+                    self.assertEqual(conn.recv(64), b"ready")
+                stdout, stderr = process.communicate(timeout=SHORT_TIMEOUT)
+            finally:
+                if process.poll() is None:
+                    process.kill()
+                    process.wait()
+        return stdout, stderr
+
+    def test_script_directory_on_sys_path(self):
+        # gh-158540: The coordinator must put the directory containing the
+        # script (not the working directory) on sys.path, matching
+        # ``python script.py``, so that modules next to the script can be
+        # imported even when the coordinator runs from another directory.
+        with tempfile.TemporaryDirectory() as tmpdir:
+            script_dir = os.path.join(tmpdir, "sub")
+            os.mkdir(script_dir)
+            with open(os.path.join(script_dir, "helper.py"), "w") as f:
+                f.write("message = 'helper imported'\n")
+            with open(os.path.join(script_dir, "where.py"), "w") as f:
+                f.write(
+                    "import os\n"
+                    "import sys\n"
+                    "print('PATH0:', os.path.realpath(sys.path[0]))\n"
+                    "import helper\n"
+                    "print('HELPER:', helper.message)\n"
+                )
+
+            stdout, stderr = self.run_coordinator(
+                tmpdir, os.path.join("sub", "where.py")
+            )
+
+        self.assertNotIn("ModuleNotFoundError", stderr)
+        self.assertIn("HELPER: helper imported", stdout)
+        self.assertIn(f"PATH0: {os.path.realpath(script_dir)}", stdout)
+
+    @os_helper.skip_unless_symlink
+    def test_symlinked_script_uses_real_directory(self):
+        # gh-158540: ``python script.py`` resolves symlinks when computing
+        # sys.path[0], so a symlinked script must import modules next to the
+        # real script, not next to the link.
+        with tempfile.TemporaryDirectory() as tmpdir:
+            script_dir = os.path.join(tmpdir, "sub")
+            os.mkdir(script_dir)
+            with open(os.path.join(script_dir, "helper.py"), "w") as f:
+                f.write("message = 'helper imported'\n")
+            with open(os.path.join(script_dir, "where.py"), "w") as f:
+                f.write(
+                    "import os\n"
+                    "import sys\n"
+                    "print('PATH0:', os.path.realpath(sys.path[0]))\n"
+                    "import helper\n"
+                    "print('HELPER:', helper.message)\n"
+                )
+            os.symlink(
+                os.path.join("sub", "where.py"),
+                os.path.join(tmpdir, "link.py"),
+            )
+
+            stdout, stderr = self.run_coordinator(tmpdir, "link.py")
+
+        self.assertNotIn("ModuleNotFoundError", stderr)
+        self.assertIn("HELPER: helper imported", stdout)
+        self.assertIn(f"PATH0: {os.path.realpath(script_dir)}", stdout)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-30-22-10-00.gh-issue-158540.kQ9vXm.rst 
b/Misc/NEWS.d/next/Library/2026-09-30-22-10-00.gh-issue-158540.kQ9vXm.rst
new file mode 100644
index 00000000000000..9cbd698cf9a96e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-30-22-10-00.gh-issue-158540.kQ9vXm.rst
@@ -0,0 +1,4 @@
+Fix :mod:`profiling.sampling` not adding the directory containing the
+profiled script to :data:`sys.path`, which prevented the script from
+importing modules placed next to it.  The script's directory is now added,
+matching the behavior of ``python script.py``.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to