7
7
import ast
8
8
import importlib
9
9
import importlib .util
10
- import inspect
11
10
import os
12
11
import sys
13
12
import types
@@ -99,24 +98,25 @@ def __dir__():
99
98
100
99
101
100
class DelayedImportErrorModule (types .ModuleType ):
102
- def __init__ (self , frame_data , * args , ** kwargs ):
101
+ def __init__ (self , frame_data , * args , message , ** kwargs ):
103
102
self .__frame_data = frame_data
103
+ self .__message = message
104
104
super ().__init__ (* args , ** kwargs )
105
105
106
106
def __getattr__ (self , x ):
107
- if x in ("__class__" , "__file__" , "__frame_data" ):
107
+ if x in ("__class__" , "__file__" , "__frame_data" , "__message" ):
108
108
super ().__getattr__ (x )
109
109
else :
110
110
fd = self .__frame_data
111
111
raise ModuleNotFoundError (
112
- f"No module named ' { fd [ 'spec' ] } ' \n \n "
112
+ f"{ self . __message } \n \n "
113
113
"This error is lazily reported, having originally occured in\n "
114
114
f' File { fd ["filename" ]} , line { fd ["lineno" ]} , in { fd ["function" ]} \n \n '
115
115
f'----> { "" .join (fd ["code_context" ] or "" ).strip ()} '
116
116
)
117
117
118
118
119
- def load (fullname , error_on_import = False ):
119
+ def load (fullname , * , require = None , error_on_import = False ):
120
120
"""Return a lazily imported proxy for a module.
121
121
122
122
We often see the following pattern::
@@ -160,6 +160,14 @@ def myfunc():
160
160
161
161
sp = lazy.load('scipy') # import scipy as sp
162
162
163
+ require : str
164
+ A dependency requirement as defined in PEP-508. For example::
165
+
166
+ "numpy >=1.24"
167
+
168
+ If defined, the proxy module will raise an error if the installed
169
+ version does not satisfy the requirement.
170
+
163
171
error_on_import : bool
164
172
Whether to postpone raising import errors until the module is accessed.
165
173
If set to `True`, import errors are raised as soon as `load` is called.
@@ -171,10 +179,12 @@ def myfunc():
171
179
Actual loading of the module occurs upon first attribute request.
172
180
173
181
"""
174
- try :
175
- return sys .modules [fullname ]
176
- except KeyError :
177
- pass
182
+ module = sys .modules .get (fullname )
183
+ have_module = module is not None
184
+
185
+ # Most common, short-circuit
186
+ if have_module and require is None :
187
+ return module
178
188
179
189
if "." in fullname :
180
190
msg = (
@@ -184,33 +194,86 @@ def myfunc():
184
194
)
185
195
warnings .warn (msg , RuntimeWarning )
186
196
187
- spec = importlib .util .find_spec (fullname )
188
- if spec is None :
197
+ spec = None
198
+ if not have_module :
199
+ spec = importlib .util .find_spec (fullname )
200
+ have_module = spec is not None
201
+
202
+ if not have_module :
203
+ not_found_message = f"No module named '{ fullname } '"
204
+ elif require is not None :
205
+ try :
206
+ have_module = _check_requirement (require )
207
+ except ModuleNotFoundError as e :
208
+ raise ValueError (
209
+ f"Found module '{ fullname } ' but cannot test requirement '{ require } '. "
210
+ "Requirements must match distribution name, not module name."
211
+ ) from e
212
+
213
+ not_found_message = f"No distribution can be found matching '{ require } '"
214
+
215
+ if not have_module :
189
216
if error_on_import :
190
- raise ModuleNotFoundError (f"No module named '{ fullname } '" )
191
- else :
192
- try :
193
- parent = inspect .stack ()[1 ]
194
- frame_data = {
195
- "spec" : fullname ,
196
- "filename" : parent .filename ,
197
- "lineno" : parent .lineno ,
198
- "function" : parent .function ,
199
- "code_context" : parent .code_context ,
200
- }
201
- return DelayedImportErrorModule (frame_data , "DelayedImportErrorModule" )
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 )
217
+ raise ModuleNotFoundError (not_found_message )
218
+ import inspect
219
+
220
+ try :
221
+ parent = inspect .stack ()[1 ]
222
+ frame_data = {
223
+ "filename" : parent .filename ,
224
+ "lineno" : parent .lineno ,
225
+ "function" : parent .function ,
226
+ "code_context" : parent .code_context ,
227
+ }
228
+ return DelayedImportErrorModule (
229
+ frame_data ,
230
+ "DelayedImportErrorModule" ,
231
+ message = not_found_message ,
232
+ )
233
+ finally :
234
+ del parent
235
+
236
+ if spec is not None :
237
+ module = importlib .util .module_from_spec (spec )
238
+ sys .modules [fullname ] = module
239
+
240
+ loader = importlib .util .LazyLoader (spec .loader )
241
+ loader .exec_module (module )
210
242
211
243
return module
212
244
213
245
246
+ def _check_requirement (require : str ) -> bool :
247
+ """Verify that a package requirement is satisfied
248
+
249
+ If the package is required, a ``ModuleNotFoundError`` is raised
250
+ by ``importlib.metadata``.
251
+
252
+ Parameters
253
+ ----------
254
+ require : str
255
+ A dependency requirement as defined in PEP-508
256
+
257
+ Returns
258
+ -------
259
+ satisfied : bool
260
+ True if the installed version of the dependency matches
261
+ the specified version, False otherwise.
262
+ """
263
+ import packaging .requirements
264
+
265
+ try :
266
+ import importlib .metadata as importlib_metadata
267
+ except ImportError : # PY37
268
+ import importlib_metadata
269
+
270
+ req = packaging .requirements .Requirement (require )
271
+ return req .specifier .contains (
272
+ importlib_metadata .version (req .name ),
273
+ prereleases = True ,
274
+ )
275
+
276
+
214
277
class _StubVisitor (ast .NodeVisitor ):
215
278
"""AST visitor to parse a stub file for submodules and submod_attrs."""
216
279
0 commit comments