@@ -103,9 +103,9 @@ def __dir__():
103
103
104
104
105
105
class DelayedImportErrorModule (types .ModuleType ):
106
- def __init__ (self , frame_data , * args , message = None , ** kwargs ):
106
+ def __init__ (self , frame_data , * args , message , ** kwargs ):
107
107
self .__frame_data = frame_data
108
- self .__message = message or f"No module named ' { frame_data [ 'spec' ] } '"
108
+ self .__message = message
109
109
super ().__init__ (* args , ** kwargs )
110
110
111
111
def __getattr__ (self , x ):
@@ -121,7 +121,7 @@ def __getattr__(self, x):
121
121
)
122
122
123
123
124
- def load (fullname , error_on_import = False ):
124
+ def load (fullname , * , require = None , error_on_import = False ):
125
125
"""Return a lazily imported proxy for a module.
126
126
127
127
We often see the following pattern::
@@ -166,51 +166,72 @@ def myfunc():
166
166
Actual loading of the module occurs upon first attribute request.
167
167
168
168
"""
169
- try :
170
- return sys .modules [fullname ]
171
- except KeyError :
172
- pass
173
-
174
- spec = importlib .util .find_spec (fullname )
175
- return _module_from_spec (
176
- spec ,
177
- fullname ,
178
- f"No module named '{ fullname } '" ,
179
- error_on_import ,
180
- )
169
+ module = sys .modules .get (fullname )
170
+ have_module = module is not None
171
+
172
+ # Most common, short-circuit
173
+ if have_module and require is None :
174
+ return module
175
+
176
+ spec = None
177
+ if not have_module :
178
+ spec = importlib .util .find_spec (fullname )
179
+ have_module = spec is not None
180
+
181
+ if not have_module :
182
+ not_found_message = f"No module named '{ fullname } '"
183
+ elif require is not None :
184
+ # Old style lazy loading to avoid polluting sys.modules
185
+ import packaging .requirements
186
+
187
+ req = packaging .requirements .Requirement (require )
188
+ try :
189
+ have_module = req .specifier .contains (
190
+ importlib_metadata .version (req .name ),
191
+ prereleases = True ,
192
+ )
193
+ except importlib_metadata .PackageNotFoundError as e :
194
+ raise ValueError (
195
+ f"Found module '{ fullname } ' but cannot test requirement '{ require } '. "
196
+ "Requirements must match distribution name, not module name."
197
+ ) from e
181
198
199
+ if not have_module :
200
+ not_found_message = f"No distribution can be found matching '{ require } '"
182
201
183
- def _module_from_spec (spec , fullname , failure_message , error_on_import ):
184
- """Return lazy module, DelayedImportErrorModule, or raise error"""
185
- if spec is None :
202
+ if not have_module :
186
203
if error_on_import :
187
- raise ModuleNotFoundError (failure_message )
188
- else :
189
- try :
190
- parent = inspect . stack ()[ 2 ]
191
- frame_data = {
192
- "filename " : parent .filename ,
193
- "lineno " : parent .lineno ,
194
- "function " : parent .function ,
195
- "code_context" : parent . code_context ,
196
- }
197
- return DelayedImportErrorModule (
198
- frame_data ,
199
- "DelayedImportErrorModule" ,
200
- message = failure_message ,
201
- )
202
- finally :
203
- del parent
204
-
205
- module = importlib .util .module_from_spec (spec )
206
- sys .modules [fullname ] = module
207
-
208
- loader = importlib .util .LazyLoader (spec .loader )
209
- loader .exec_module (module )
204
+ raise ModuleNotFoundError (not_found_message )
205
+ try :
206
+ parent = inspect . stack ()[ 1 ]
207
+ frame_data = {
208
+ "filename" : parent . filename ,
209
+ "lineno " : parent .lineno ,
210
+ "function " : parent .function ,
211
+ "code_context " : parent .code_context ,
212
+ }
213
+ return DelayedImportErrorModule (
214
+ frame_data ,
215
+ "DelayedImportErrorModule" ,
216
+ message = not_found_message ,
217
+ )
218
+ finally :
219
+ del parent
220
+
221
+ if spec is not None :
222
+ module = importlib .util .module_from_spec (spec )
223
+ sys .modules [fullname ] = module
224
+
225
+ loader = importlib .util .LazyLoader (spec .loader )
226
+ loader .exec_module (module )
210
227
211
228
return module
212
229
213
230
231
+ def have_module (module_like : types .ModuleType ) -> bool :
232
+ return not isinstance (module_like , DelayedImportErrorModule )
233
+
234
+
214
235
class _StubVisitor (ast .NodeVisitor ):
215
236
"""AST visitor to parse a stub file for submodules and submod_attrs."""
216
237
@@ -269,40 +290,3 @@ def attach_stub(package_name: str, filename: str):
269
290
visitor = _StubVisitor ()
270
291
visitor .visit (stub_node )
271
292
return attach (package_name , visitor ._submodules , visitor ._submod_attrs )
272
-
273
-
274
- def load_requirement (requirement , fullname = None , error_on_import = False ):
275
- # Old style lazy loading to avoid polluting sys.modules
276
- import packaging .requirements
277
-
278
- req = packaging .requirements .Requirement (requirement )
279
-
280
- if fullname is None :
281
- fullname = req .name
282
-
283
- not_found_msg = f"No module named '{ fullname } '"
284
-
285
- module = sys .modules .get (fullname )
286
- have_mod = module is not None
287
- if not have_mod :
288
- spec = importlib .util .find_spec (fullname )
289
- have_mod = spec is not None
290
-
291
- if have_mod and req .specifier :
292
- # Note: req.name is the distribution name, not the module name
293
- try :
294
- version = importlib_metadata .version (req .name )
295
- except importlib_metadata .PackageNotFoundError as e :
296
- raise ValueError (
297
- f"Found module '{ fullname } ' but cannot test requirement '{ req } '. "
298
- "Requirements must match distribution name, not module name."
299
- ) from e
300
- have_mod = any (req .specifier .filter ((version ,)))
301
- if not have_mod :
302
- spec = None
303
- not_found_msg = f"No distribution can be found matching '{ req } '"
304
-
305
- if have_mod and module is not None :
306
- return module , have_mod
307
-
308
- return _module_from_spec (spec , fullname , not_found_msg , error_on_import ), have_mod
0 commit comments