|
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