|
13 | 13 |
|
14 | 14 | import _imp
|
15 | 15 | import sys
|
| 16 | +import threading |
16 | 17 | import types
|
17 | 18 |
|
18 | 19 |
|
@@ -171,36 +172,54 @@ class _LazyModule(types.ModuleType):
|
171 | 172 |
|
172 | 173 | def __getattribute__(self, attr):
|
173 | 174 | """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) |
| 175 | + __spec__ = object.__getattribute__(self, '__spec__') |
| 176 | + loader_state = __spec__.loader_state |
| 177 | + with loader_state['lock']: |
| 178 | + # Only the first thread to get the lock should trigger the load |
| 179 | + # and reset the module's class. The rest can now getattr(). |
| 180 | + if object.__getattribute__(self, '__class__') is _LazyModule: |
| 181 | + # The first thread comes here multiple times as it descends the |
| 182 | + # call stack. The first time, it sets is_loading and triggers |
| 183 | + # exec_module(), which will access module.__dict__, module.__name__, |
| 184 | + # and/or module.__spec__, reentering this method. These accesses |
| 185 | + # need to be allowed to proceed without triggering the load again. |
| 186 | + if loader_state['is_loading'] and attr.startswith('__') and attr.endswith('__'): |
| 187 | + return object.__getattribute__(self, attr) |
| 188 | + loader_state['is_loading'] = True |
| 189 | + |
| 190 | + __dict__ = object.__getattribute__(self, '__dict__') |
| 191 | + |
| 192 | + # All module metadata must be gathered from __spec__ in order to avoid |
| 193 | + # using mutated values. |
| 194 | + # Get the original name to make sure no object substitution occurred |
| 195 | + # in sys.modules. |
| 196 | + original_name = __spec__.name |
| 197 | + # Figure out exactly what attributes were mutated between the creation |
| 198 | + # of the module and now. |
| 199 | + attrs_then = loader_state['__dict__'] |
| 200 | + attrs_now = __dict__ |
| 201 | + attrs_updated = {} |
| 202 | + for key, value in attrs_now.items(): |
| 203 | + # Code that set an attribute may have kept a reference to the |
| 204 | + # assigned object, making identity more important than equality. |
| 205 | + if key not in attrs_then: |
| 206 | + attrs_updated[key] = value |
| 207 | + elif id(attrs_now[key]) != id(attrs_then[key]): |
| 208 | + attrs_updated[key] = value |
| 209 | + __spec__.loader.exec_module(self) |
| 210 | + # If exec_module() was used directly there is no guarantee the module |
| 211 | + # object was put into sys.modules. |
| 212 | + if original_name in sys.modules: |
| 213 | + if id(self) != id(sys.modules[original_name]): |
| 214 | + raise ValueError(f"module object for {original_name!r} " |
| 215 | + "substituted in sys.modules during a lazy " |
| 216 | + "load") |
| 217 | + # Update after loading since that's what would happen in an eager |
| 218 | + # loading situation. |
| 219 | + __dict__.update(attrs_updated) |
| 220 | + # Finally, stop triggering this method. |
| 221 | + self.__class__ = types.ModuleType |
| 222 | + |
204 | 223 | return getattr(self, attr)
|
205 | 224 |
|
206 | 225 | def __delattr__(self, attr):
|
@@ -244,5 +263,7 @@ def exec_module(self, module):
|
244 | 263 | loader_state = {}
|
245 | 264 | loader_state['__dict__'] = module.__dict__.copy()
|
246 | 265 | loader_state['__class__'] = module.__class__
|
| 266 | + loader_state['lock'] = threading.RLock() |
| 267 | + loader_state['is_loading'] = False |
247 | 268 | module.__spec__.loader_state = loader_state
|
248 | 269 | module.__class__ = _LazyModule
|
0 commit comments