44import importlib
55import importlib .abc
66from 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
89from collections .abc import Buffer
910import types
1011from os import PathLike
@@ -126,8 +127,9 @@ def source_to_code(
126127 return code
127128
128129class 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 :
0 commit comments