Skip to content

Commit 359c709

Browse files
committed
Fix side-effects to importing hooks.
1 parent 77665e7 commit 359c709

1 file changed

Lines changed: 27 additions & 14 deletions

File tree

src/_pytask/path.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -210,26 +210,22 @@ def _resolve_pkg_root_and_module_name(path: Path) -> tuple[Path, str]:
210210
(missing any __init__.py files) and no valid namespace package root is found.
211211
212212
"""
213-
pkg_root: Path | None = None
213+
# First, try to find a regular package (with __init__.py files).
214214
pkg_path = _resolve_package_path(path)
215215
if pkg_path is not None:
216216
pkg_root = pkg_path.parent
217+
module_name = _compute_module_name(pkg_root, path)
218+
if module_name:
219+
return pkg_root, module_name
217220

218-
# Check for namespace packages by walking up the directory tree.
219-
# For each candidate root, compute the module name and verify that Python's
220-
# import system would resolve that name to this file.
221-
start = pkg_root if pkg_root is not None else path.parent
222-
for candidate in (start, *start.parents):
221+
# No regular package found. Check for namespace packages by walking up the
222+
# directory tree and verifying that Python's import system would resolve
223+
# the computed module name to this file.
224+
for candidate in (path.parent, *path.parent.parents):
223225
module_name = _compute_module_name(candidate, path)
224226
if module_name and _is_importable(module_name, path):
225227
# Found a root where Python's import system agrees with our module name.
226-
pkg_root = candidate
227-
break
228-
229-
if pkg_root is not None:
230-
module_name = _compute_module_name(pkg_root, path)
231-
if module_name:
232-
return pkg_root, module_name
228+
return candidate, module_name
233229

234230
msg = f"Could not resolve for {path}"
235231
raise CouldNotResolvePathError(msg)
@@ -270,11 +266,23 @@ def _is_importable(module_name: str, module_path: Path) -> bool:
270266
This verifies that importing `module_name` via Python's standard import mechanism
271267
(as if typed in the REPL) would load the file at `module_path`.
272268
269+
Note: find_spec() has a side effect of creating parent namespace packages in
270+
sys.modules. We clean these up to avoid polluting the module namespace.
273271
"""
272+
# Track modules before the call to clean up side effects
273+
modules_before = set(sys.modules.keys())
274+
274275
try:
275276
spec = importlib.util.find_spec(module_name)
276277
except (ImportError, ValueError, ImportWarning):
277278
return False
279+
finally:
280+
# Clean up any modules that were added as side effects.
281+
# find_spec() can create parent namespace packages in sys.modules.
282+
modules_added = set(sys.modules.keys()) - modules_before
283+
for mod_name in modules_added:
284+
sys.modules.pop(mod_name, None)
285+
278286
return _spec_matches_module_path(spec, module_path)
279287

280288

@@ -314,7 +322,11 @@ def _import_module_using_spec(
314322
# Checking with sys.meta_path first in case one of its hooks can import this module,
315323
# such as our own assertion-rewrite hook.
316324
for meta_importer in sys.meta_path:
317-
spec = meta_importer.find_spec(module_name, [str(module_location)])
325+
try:
326+
spec = meta_importer.find_spec(module_name, [str(module_location)])
327+
except (ImportError, KeyError, ValueError):
328+
# Some meta_path finders raise exceptions when parent modules don't exist.
329+
continue
318330
if spec is not None:
319331
break
320332
else:
@@ -323,6 +335,7 @@ def _import_module_using_spec(
323335
mod = importlib.util.module_from_spec(spec)
324336
sys.modules[module_name] = mod
325337
spec.loader.exec_module(mod) # type: ignore[union-attr]
338+
_insert_missing_modules(sys.modules, module_name)
326339
return mod
327340

328341
return None

0 commit comments

Comments
 (0)