@@ -104,9 +104,9 @@ def __dir__():
104104
105105
106106class DelayedImportErrorModule (types .ModuleType ):
107- def __init__ (self , frame_data , * args , message = None , ** kwargs ):
107+ def __init__ (self , frame_data , * args , message , ** kwargs ):
108108 self .__frame_data = frame_data
109- self .__message = message or f"No module named ' { frame_data [ 'spec' ] } '"
109+ self .__message = message
110110 super ().__init__ (* args , ** kwargs )
111111
112112 def __getattr__ (self , x ):
@@ -122,7 +122,7 @@ def __getattr__(self, x):
122122 )
123123
124124
125- def load (fullname , error_on_import = False ):
125+ def load (fullname , * , require = None , error_on_import = False ):
126126 """Return a lazily imported proxy for a module.
127127
128128 We often see the following pattern::
@@ -177,10 +177,12 @@ def myfunc():
177177 Actual loading of the module occurs upon first attribute request.
178178
179179 """
180- try :
181- return sys .modules [fullname ]
182- except KeyError :
183- pass
180+ module = sys .modules .get (fullname )
181+ have_module = module is not None
182+
183+ # Most common, short-circuit
184+ if have_module and require is None :
185+ return module
184186
185187 if "." in fullname :
186188 msg = (
@@ -190,46 +192,65 @@ def myfunc():
190192 )
191193 warnings .warn (msg , RuntimeWarning )
192194
193- spec = importlib .util .find_spec (fullname )
194- return _module_from_spec (
195- spec ,
196- fullname ,
197- f"No module named '{ fullname } '" ,
198- error_on_import ,
199- )
195+ spec = None
196+ if not have_module :
197+ spec = importlib .util .find_spec (fullname )
198+ have_module = spec is not None
199+
200+ if not have_module :
201+ not_found_message = f"No module named '{ fullname } '"
202+ elif require is not None :
203+ # Old style lazy loading to avoid polluting sys.modules
204+ import packaging .requirements
205+
206+ req = packaging .requirements .Requirement (require )
207+ try :
208+ have_module = req .specifier .contains (
209+ importlib_metadata .version (req .name ),
210+ prereleases = True ,
211+ )
212+ except importlib_metadata .PackageNotFoundError as e :
213+ raise ValueError (
214+ f"Found module '{ fullname } ' but cannot test requirement '{ require } '. "
215+ "Requirements must match distribution name, not module name."
216+ ) from e
200217
218+ if not have_module :
219+ not_found_message = f"No distribution can be found matching '{ require } '"
201220
202- def _module_from_spec (spec , fullname , failure_message , error_on_import ):
203- """Return lazy module, DelayedImportErrorModule, or raise error"""
204- if spec is None :
221+ if not have_module :
205222 if error_on_import :
206- raise ModuleNotFoundError (failure_message )
207- else :
208- try :
209- parent = inspect . stack ()[ 2 ]
210- frame_data = {
211- "filename " : parent .filename ,
212- "lineno " : parent .lineno ,
213- "function " : parent .function ,
214- "code_context" : parent . code_context ,
215- }
216- return DelayedImportErrorModule (
217- frame_data ,
218- "DelayedImportErrorModule" ,
219- message = failure_message ,
220- )
221- finally :
222- del parent
223-
224- module = importlib .util .module_from_spec (spec )
225- sys .modules [fullname ] = module
226-
227- loader = importlib .util .LazyLoader (spec .loader )
228- loader .exec_module (module )
223+ raise ModuleNotFoundError (not_found_message )
224+ try :
225+ parent = inspect . stack ()[ 1 ]
226+ frame_data = {
227+ "filename" : parent . filename ,
228+ "lineno " : parent .lineno ,
229+ "function " : parent .function ,
230+ "code_context " : parent .code_context ,
231+ }
232+ return DelayedImportErrorModule (
233+ frame_data ,
234+ "DelayedImportErrorModule" ,
235+ message = not_found_message ,
236+ )
237+ finally :
238+ del parent
239+
240+ if spec is not None :
241+ module = importlib .util .module_from_spec (spec )
242+ sys .modules [fullname ] = module
243+
244+ loader = importlib .util .LazyLoader (spec .loader )
245+ loader .exec_module (module )
229246
230247 return module
231248
232249
250+ def have_module (module_like : types .ModuleType ) -> bool :
251+ return not isinstance (module_like , DelayedImportErrorModule )
252+
253+
233254class _StubVisitor (ast .NodeVisitor ):
234255 """AST visitor to parse a stub file for submodules and submod_attrs."""
235256
@@ -288,40 +309,3 @@ def attach_stub(package_name: str, filename: str):
288309 visitor = _StubVisitor ()
289310 visitor .visit (stub_node )
290311 return attach (package_name , visitor ._submodules , visitor ._submod_attrs )
291-
292-
293- def load_requirement (requirement , fullname = None , error_on_import = False ):
294- # Old style lazy loading to avoid polluting sys.modules
295- import packaging .requirements
296-
297- req = packaging .requirements .Requirement (requirement )
298-
299- if fullname is None :
300- fullname = req .name
301-
302- not_found_msg = f"No module named '{ fullname } '"
303-
304- module = sys .modules .get (fullname )
305- have_mod = module is not None
306- if not have_mod :
307- spec = importlib .util .find_spec (fullname )
308- have_mod = spec is not None
309-
310- if have_mod and req .specifier :
311- # Note: req.name is the distribution name, not the module name
312- try :
313- version = importlib_metadata .version (req .name )
314- except importlib_metadata .PackageNotFoundError as e :
315- raise ValueError (
316- f"Found module '{ fullname } ' but cannot test requirement '{ req } '. "
317- "Requirements must match distribution name, not module name."
318- ) from e
319- have_mod = any (req .specifier .filter ((version ,)))
320- if not have_mod :
321- spec = None
322- not_found_msg = f"No distribution can be found matching '{ req } '"
323-
324- if have_mod and module is not None :
325- return module , have_mod
326-
327- return _module_from_spec (spec , fullname , not_found_msg , error_on_import ), have_mod
0 commit comments