|
15 | 15 | import _imp
|
16 | 16 | import functools
|
17 | 17 | import sys
|
| 18 | +import threading |
18 | 19 | import types
|
19 | 20 | import warnings
|
20 | 21 |
|
@@ -225,36 +226,54 @@ class _LazyModule(types.ModuleType):
|
225 | 226 |
|
226 | 227 | def __getattribute__(self, attr):
|
227 | 228 | """Trigger the load of the module and return the attribute."""
|
228 |
| - # All module metadata must be garnered from __spec__ in order to avoid |
229 |
| - # using mutated values. |
230 |
| - # Stop triggering this method. |
231 |
| - self.__class__ = types.ModuleType |
232 |
| - # Get the original name to make sure no object substitution occurred |
233 |
| - # in sys.modules. |
234 |
| - original_name = self.__spec__.name |
235 |
| - # Figure out exactly what attributes were mutated between the creation |
236 |
| - # of the module and now. |
237 |
| - attrs_then = self.__spec__.loader_state['__dict__'] |
238 |
| - attrs_now = self.__dict__ |
239 |
| - attrs_updated = {} |
240 |
| - for key, value in attrs_now.items(): |
241 |
| - # Code that set the attribute may have kept a reference to the |
242 |
| - # assigned object, making identity more important than equality. |
243 |
| - if key not in attrs_then: |
244 |
| - attrs_updated[key] = value |
245 |
| - elif id(attrs_now[key]) != id(attrs_then[key]): |
246 |
| - attrs_updated[key] = value |
247 |
| - self.__spec__.loader.exec_module(self) |
248 |
| - # If exec_module() was used directly there is no guarantee the module |
249 |
| - # object was put into sys.modules. |
250 |
| - if original_name in sys.modules: |
251 |
| - if id(self) != id(sys.modules[original_name]): |
252 |
| - raise ValueError(f"module object for {original_name!r} " |
253 |
| - "substituted in sys.modules during a lazy " |
254 |
| - "load") |
255 |
| - # Update after loading since that's what would happen in an eager |
256 |
| - # loading situation. |
257 |
| - self.__dict__.update(attrs_updated) |
| 229 | + __spec__ = object.__getattribute__(self, '__spec__') |
| 230 | + loader_state = __spec__.loader_state |
| 231 | + with loader_state['lock']: |
| 232 | + # Only the first thread to get the lock should trigger the load |
| 233 | + # and reset the module's class. The rest can now getattr(). |
| 234 | + if object.__getattribute__(self, '__class__') is _LazyModule: |
| 235 | + # The first thread comes here multiple times as it descends the |
| 236 | + # call stack. The first time, it sets is_loading and triggers |
| 237 | + # exec_module(), which will access module.__dict__, module.__name__, |
| 238 | + # and/or module.__spec__, reentering this method. These accesses |
| 239 | + # need to be allowed to proceed without triggering the load again. |
| 240 | + if loader_state['is_loading'] and attr.startswith('__') and attr.endswith('__'): |
| 241 | + return object.__getattribute__(self, attr) |
| 242 | + loader_state['is_loading'] = True |
| 243 | + |
| 244 | + __dict__ = object.__getattribute__(self, '__dict__') |
| 245 | + |
| 246 | + # All module metadata must be gathered from __spec__ in order to avoid |
| 247 | + # using mutated values. |
| 248 | + # Get the original name to make sure no object substitution occurred |
| 249 | + # in sys.modules. |
| 250 | + original_name = __spec__.name |
| 251 | + # Figure out exactly what attributes were mutated between the creation |
| 252 | + # of the module and now. |
| 253 | + attrs_then = loader_state['__dict__'] |
| 254 | + attrs_now = __dict__ |
| 255 | + attrs_updated = {} |
| 256 | + for key, value in attrs_now.items(): |
| 257 | + # Code that set an attribute may have kept a reference to the |
| 258 | + # assigned object, making identity more important than equality. |
| 259 | + if key not in attrs_then: |
| 260 | + attrs_updated[key] = value |
| 261 | + elif id(attrs_now[key]) != id(attrs_then[key]): |
| 262 | + attrs_updated[key] = value |
| 263 | + __spec__.loader.exec_module(self) |
| 264 | + # If exec_module() was used directly there is no guarantee the module |
| 265 | + # object was put into sys.modules. |
| 266 | + if original_name in sys.modules: |
| 267 | + if id(self) != id(sys.modules[original_name]): |
| 268 | + raise ValueError(f"module object for {original_name!r} " |
| 269 | + "substituted in sys.modules during a lazy " |
| 270 | + "load") |
| 271 | + # Update after loading since that's what would happen in an eager |
| 272 | + # loading situation. |
| 273 | + __dict__.update(attrs_updated) |
| 274 | + # Finally, stop triggering this method. |
| 275 | + self.__class__ = types.ModuleType |
| 276 | + |
258 | 277 | return getattr(self, attr)
|
259 | 278 |
|
260 | 279 | def __delattr__(self, attr):
|
@@ -298,5 +317,7 @@ def exec_module(self, module):
|
298 | 317 | loader_state = {}
|
299 | 318 | loader_state['__dict__'] = module.__dict__.copy()
|
300 | 319 | loader_state['__class__'] = module.__class__
|
| 320 | + loader_state['lock'] = threading.RLock() |
| 321 | + loader_state['is_loading'] = False |
301 | 322 | module.__spec__.loader_state = loader_state
|
302 | 323 | module.__class__ = _LazyModule
|
0 commit comments