Skip to content

Commit 6be496f

Browse files
committed
fix(python-capture): preserve __file__ on instrumented modules
Why: FlowtraceFinder rebuilt ModuleSpec from scratch, losing spec.has_location=True (default False on bare ModuleSpec). CPython only assigns module.__file__ when has_location is True, so user code relying on Path(__file__).parent or pkg.__file__ broke under instrumentation. Mutate the spec PathFinder returned instead — it already has the correct flags. Adds regression test that imports an instrumented module and asserts __file__ is set at top-level and inside function scope.
1 parent ab881d3 commit 6be496f

2 files changed

Lines changed: 63 additions & 9 deletions

File tree

capture/python/flowtrace_runtime/finder.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,8 @@ def find_spec(
7878
return None
7979

8080
loader = FlowtraceSourceLoader(name, origin)
81-
new_spec = importlib.machinery.ModuleSpec(
82-
name=name,
83-
loader=loader,
84-
origin=origin,
85-
is_package=spec.submodule_search_locations is not None,
86-
)
87-
if spec.submodule_search_locations is not None:
88-
new_spec.submodule_search_locations = list(spec.submodule_search_locations)
89-
return new_spec
81+
# Mutate the spec PathFinder gave us instead of rebuilding it.
82+
# Rebuilding loses has_location=True (default False on bare ModuleSpec),
83+
# which makes CPython skip setting __file__ on the module.
84+
spec.loader = loader
85+
return spec

capture/python/tests/test_integration.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,61 @@ def test_source_not_modified():
105105
h1 = _original_sha256()
106106
h2 = _original_sha256()
107107
assert h1 == h2
108+
109+
110+
def test_instrumented_module_preserves_dunder_file():
111+
"""Regression: FlowtraceFinder must preserve spec.has_location so __file__ is set.
112+
113+
Reason: a previous implementation rebuilt ModuleSpec without copying
114+
has_location=True, which made CPython skip assigning module.__file__.
115+
User code relying on Path(__file__).parent or pkg.__file__ then broke.
116+
"""
117+
with tempfile.TemporaryDirectory() as tmpdir:
118+
pkg_dir = Path(tmpdir) / "ft_pkg_dunder"
119+
pkg_dir.mkdir()
120+
(pkg_dir / "__init__.py").write_text("", encoding="utf-8")
121+
mod_path = pkg_dir / "leaf.py"
122+
mod_path.write_text(
123+
"from pathlib import Path\n"
124+
"MY_FILE = __file__\n"
125+
"MY_PARENT = str(Path(__file__).parent)\n"
126+
"def hello():\n"
127+
" return MY_FILE\n",
128+
encoding="utf-8",
129+
)
130+
131+
probe = (
132+
"import sys, json\n"
133+
f"sys.path.insert(0, {str(tmpdir)!r})\n"
134+
"import ft_pkg_dunder.leaf as m\n"
135+
"print(json.dumps({'file': m.__file__, 'my_file': m.MY_FILE, 'parent': m.MY_PARENT, 'hello': m.hello()}))\n"
136+
)
137+
138+
existing_pythonpath = os.environ.get("PYTHONPATH", "")
139+
pythonpath = os.pathsep.join(
140+
[str(STUB_DIR), str(CAPTURE_PKG)] + ([existing_pythonpath] if existing_pythonpath else [])
141+
)
142+
143+
env = {
144+
**os.environ,
145+
"PYTHONPATH": pythonpath,
146+
"FLOWTRACE_ENABLE": "1",
147+
"FLOWTRACE_PACKAGE_PREFIX": "ft_pkg_dunder",
148+
}
149+
env.pop("FLOWTRACE_OUTPUT", None)
150+
151+
result = subprocess.run(
152+
[sys.executable, "-c", probe],
153+
env=env,
154+
capture_output=True,
155+
text=True,
156+
timeout=15,
157+
)
158+
assert result.returncode == 0, f"probe failed: {result.stderr}"
159+
payload = json.loads(result.stdout.strip().splitlines()[-1])
160+
161+
expected_file = str(mod_path)
162+
assert payload["file"] == expected_file, f"module.__file__ mismatch: {payload}"
163+
assert payload["my_file"] == expected_file, f"top-level __file__ broken: {payload}"
164+
assert payload["parent"] == str(pkg_dir), f"Path(__file__).parent broken: {payload}"
165+
assert payload["hello"] == expected_file, f"function-scope __file__ broken: {payload}"

0 commit comments

Comments
 (0)