Skip to content

Commit 5fed31a

Browse files
committed
fix loading of dotted names such as x.y.z.py as main modules
1 parent 55bb1cd commit 5fed31a

2 files changed

Lines changed: 27 additions & 7 deletions

File tree

python/code/wypp/instrument.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import importlib
55
import importlib.abc
66
from importlib.machinery import ModuleSpec, SourceFileLoader
7-
from importlib.util import decode_source
7+
import importlib.machinery
8+
from importlib.util import decode_source, spec_from_file_location
89
from collections.abc import Buffer
910
import types
1011
from os import PathLike
@@ -126,8 +127,9 @@ def source_to_code(
126127
return code
127128

128129
class InstrumentingFinder(importlib.abc.MetaPathFinder):
129-
def __init__(self, finder, modDir: str, extraDirs: list[str]):
130+
def __init__(self, finder, modDir: str, modName: str, extraDirs: list[str]):
130131
self._origFinder = finder
132+
self.mainModName = modName
131133
self.modDir = os.path.realpath(modDir) + '/'
132134
self.extraDirs = [os.path.realpath(d) for d in extraDirs]
133135

@@ -137,9 +139,27 @@ def find_spec(
137139
path: Sequence[str] | None,
138140
target: types.ModuleType | None = None,
139141
) -> ModuleSpec | None:
142+
143+
# 1) The fullname is the name of the main module. This might be a dotted name such as x.y.z.py
144+
# so we have special logic here
145+
fp = os.path.join(self.modDir, f"{fullname}.py")
146+
if self.mainModName == fullname and os.path.isfile(fp):
147+
loader = InstrumentingLoader(fullname, fp)
148+
return spec_from_file_location(fullname, fp, loader=loader)
149+
# 2) The fullname is a prefix of the main module. We want to load main modules with
150+
# dotted names such as x.y.z.py, hence we synthesize a namespace pkg
151+
# e.g. if 'x.y.z.py' exists and we're asked for 'x', return a package spec.
152+
elif self.mainModName.startswith(fullname + '.'):
153+
spec = importlib.machinery.ModuleSpec(fullname, loader=None, is_package=True)
154+
# Namespace package marker (PEP 451)
155+
spec.submodule_search_locations = []
156+
return spec
157+
# 3) Fallback: use the original PathFinder
140158
spec = self._origFinder.find_spec(fullname, path, target)
159+
debug(f'spec for {fullname}: {spec}')
141160
if spec is None:
142-
return None
161+
return spec
162+
143163
origin = os.path.realpath(spec.origin)
144164
dirs = [self.modDir] + self.extraDirs
145165
isLocalModule = False
@@ -153,7 +173,7 @@ def find_spec(
153173
return spec
154174

155175
@contextmanager
156-
def setupFinder(modDir: str, extraDirs: list[str], typechecking: bool):
176+
def setupFinder(modDir: str, modName: str, extraDirs: list[str], typechecking: bool):
157177
if not typechecking:
158178
yield
159179
else:
@@ -169,7 +189,7 @@ def setupFinder(modDir: str, extraDirs: list[str], typechecking: bool):
169189
raise RuntimeError("Cannot find a PathFinder in sys.meta_path")
170190

171191
# Create and install our custom finder
172-
instrumenting_finder = InstrumentingFinder(finder, modDir, extraDirs)
192+
instrumenting_finder = InstrumentingFinder(finder, modDir, modName, extraDirs)
173193
sys.meta_path.insert(0, instrumenting_finder)
174194

175195
try:

python/code/wypp/runCode.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ def prepareLib(onlyCheckRunnable, enableTypeChecking):
6767
def runCode(fileToRun, globals, doTypecheck=True, extraDirs=None) -> dict:
6868
if not extraDirs:
6969
extraDirs = []
70-
with instrument.setupFinder(os.path.dirname(fileToRun), extraDirs, doTypecheck):
71-
modName = os.path.basename(os.path.splitext(fileToRun)[0])
70+
modName = os.path.basename(os.path.splitext(fileToRun)[0])
71+
with instrument.setupFinder(os.path.dirname(fileToRun), modName, extraDirs, doTypecheck):
7272
sys.dont_write_bytecode = True
7373
res = runpy.run_module(modName, init_globals=globals, run_name='__wypp__', alter_sys=False)
7474
return res

0 commit comments

Comments
 (0)