Skip to content

Commit 64b0861

Browse files
committed
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.
1 parent 91c5ab3 commit 64b0861

2 files changed

Lines changed: 38 additions & 5 deletions

File tree

‎Lib/profiling/sampling/_sync_coordinator.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,14 @@ def _execute_script(script_path: str, script_args: List[str], cwd: str) -> None:
168168
if not os.path.isfile(script_path):
169169
raise TargetError(f"Script not found: {script_path}")
170170

171-
# Put directory containing the script (not the current working directory) on sys.path so that modules next to it can be imported. Similar to ``python script.py``
172-
script_dir = os.path.dirname(script_path)
173-
if script_dir not in sys.path:
174-
sys.path.insert(0, script_dir)
171+
# Put the directory containing the script (not the current working
172+
# directory) on sys.path so that modules next to it can be imported,
173+
# like ``python script.py`` does. Resolve symlinks first so that
174+
# modules next to the real script (not next to the link) are found.
175+
script_dir = os.path.dirname(os.path.realpath(script_path))
176+
if script_dir in sys.path:
177+
sys.path.remove(script_dir)
178+
sys.path.insert(0, script_dir)
175179

176180
# Replace sys.argv to match original script call
177181
sys.argv = [script_path] + script_args

‎Lib/test/test_profiling/test_sampling_profiler/test_sync_coordinator.py‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import tempfile
88
import unittest
99

10-
from test.support import SHORT_TIMEOUT, requires_subprocess
10+
from test.support import SHORT_TIMEOUT, os_helper, requires_subprocess
1111

1212

1313
@requires_subprocess()
@@ -79,6 +79,35 @@ def test_script_directory_on_sys_path(self):
7979
self.assertIn("HELPER: helper imported", stdout)
8080
self.assertIn(f"PATH0: {os.path.realpath(script_dir)}", stdout)
8181

82+
@os_helper.skip_unless_symlink
83+
def test_symlinked_script_uses_real_directory(self):
84+
# gh-158540: ``python script.py`` resolves symlinks when computing
85+
# sys.path[0], so a symlinked script must import modules next to the
86+
# real script, not next to the link.
87+
with tempfile.TemporaryDirectory() as tmpdir:
88+
script_dir = os.path.join(tmpdir, "sub")
89+
os.mkdir(script_dir)
90+
with open(os.path.join(script_dir, "helper.py"), "w") as f:
91+
f.write("message = 'helper imported'\n")
92+
with open(os.path.join(script_dir, "where.py"), "w") as f:
93+
f.write(
94+
"import os\n"
95+
"import sys\n"
96+
"print('PATH0:', os.path.realpath(sys.path[0]))\n"
97+
"import helper\n"
98+
"print('HELPER:', helper.message)\n"
99+
)
100+
os.symlink(
101+
os.path.join("sub", "where.py"),
102+
os.path.join(tmpdir, "link.py"),
103+
)
104+
105+
stdout, stderr = self.run_coordinator(tmpdir, "link.py")
106+
107+
self.assertNotIn("ModuleNotFoundError", stderr)
108+
self.assertIn("HELPER: helper imported", stdout)
109+
self.assertIn(f"PATH0: {os.path.realpath(script_dir)}", stdout)
110+
82111

83112
if __name__ == "__main__":
84113
unittest.main()

0 commit comments

Comments
 (0)