@@ -171,36 +171,49 @@ class _LazyModule(types.ModuleType):
171
171
172
172
def __getattribute__ (self , attr ):
173
173
"""Trigger the load of the module and return the attribute."""
174
- # All module metadata must be garnered from __spec__ in order to avoid
175
- # using mutated values.
176
- # Stop triggering this method.
177
- self .__class__ = types .ModuleType
178
- # Get the original name to make sure no object substitution occurred
179
- # in sys.modules.
180
- original_name = self .__spec__ .name
181
- # Figure out exactly what attributes were mutated between the creation
182
- # of the module and now.
183
- attrs_then = self .__spec__ .loader_state ['__dict__' ]
184
- attrs_now = self .__dict__
185
- attrs_updated = {}
186
- for key , value in attrs_now .items ():
187
- # Code that set the attribute may have kept a reference to the
188
- # assigned object, making identity more important than equality.
189
- if key not in attrs_then :
190
- attrs_updated [key ] = value
191
- elif id (attrs_now [key ]) != id (attrs_then [key ]):
192
- attrs_updated [key ] = value
193
- self .__spec__ .loader .exec_module (self )
194
- # If exec_module() was used directly there is no guarantee the module
195
- # object was put into sys.modules.
196
- if original_name in sys .modules :
197
- if id (self ) != id (sys .modules [original_name ]):
198
- raise ValueError (f"module object for { original_name !r} "
199
- "substituted in sys.modules during a lazy "
200
- "load" )
201
- # Update after loading since that's what would happen in an eager
202
- # loading situation.
203
- self .__dict__ .update (attrs_updated )
174
+ __spec__ = object .__getattribute__ (self , '__spec__' )
175
+ loader_state = __spec__ .loader_state
176
+ with loader_state ['lock' ]:
177
+ if object .__getattribute__ (self , '__class__' ) is _LazyModule :
178
+ # exec_module() will access dunder attributes, so we use a reentrant
179
+ # lock and an event to prevent infinite recursion.
180
+ if loader_state ['is_loading' ].is_set () and attr [:2 ] == attr [- 2 :] == '__' :
181
+ return object .__getattribute__ (self , attr )
182
+ loader_state ['is_loading' ].set ()
183
+
184
+ __dict__ = object .__getattribute__ (self , '__dict__' )
185
+
186
+ # All module metadata must be garnered from __spec__ in order to avoid
187
+ # using mutated values.
188
+ # Get the original name to make sure no object substitution occurred
189
+ # in sys.modules.
190
+ original_name = __spec__ .name
191
+ # Figure out exactly what attributes were mutated between the creation
192
+ # of the module and now.
193
+ attrs_then = loader_state ['__dict__' ]
194
+ attrs_now = __dict__
195
+ attrs_updated = {}
196
+ for key , value in attrs_now .items ():
197
+ # Code that set the attribute may have kept a reference to the
198
+ # assigned object, making identity more important than equality.
199
+ if key not in attrs_then :
200
+ attrs_updated [key ] = value
201
+ elif id (attrs_now [key ]) != id (attrs_then [key ]):
202
+ attrs_updated [key ] = value
203
+ __spec__ .loader .exec_module (self )
204
+ # If exec_module() was used directly there is no guarantee the module
205
+ # object was put into sys.modules.
206
+ if original_name in sys .modules :
207
+ if id (self ) != id (sys .modules [original_name ]):
208
+ raise ValueError (f"module object for { original_name !r} "
209
+ "substituted in sys.modules during a lazy "
210
+ "load" )
211
+ # Update after loading since that's what would happen in an eager
212
+ # loading situation.
213
+ __dict__ .update (attrs_updated )
214
+ # Finally, stop triggering this method.
215
+ self .__class__ = types .ModuleType
216
+
204
217
return getattr (self , attr )
205
218
206
219
def __delattr__ (self , attr ):
@@ -235,6 +248,8 @@ def create_module(self, spec):
235
248
236
249
def exec_module (self , module ):
237
250
"""Make the module load lazily."""
251
+ import threading
252
+
238
253
module .__spec__ .loader = self .loader
239
254
module .__loader__ = self .loader
240
255
# Don't need to worry about deep-copying as trying to set an attribute
@@ -244,5 +259,7 @@ def exec_module(self, module):
244
259
loader_state = {}
245
260
loader_state ['__dict__' ] = module .__dict__ .copy ()
246
261
loader_state ['__class__' ] = module .__class__
262
+ loader_state ['lock' ] = threading .RLock ()
263
+ loader_state ['is_loading' ] = threading .Event ()
247
264
module .__spec__ .loader_state = loader_state
248
265
module .__class__ = _LazyModule
0 commit comments