Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork33.7k
Closed
Description
Bug report
Bug description:
Attempting to access an attribute of a lazily-loaded module causes the module's__class__ to be reset before its attributes have been populated.
importimportlib.utilimportsysimportthreadingimporttime# Lazy load httpspec=importlib.util.find_spec("http")module=importlib.util.module_from_spec(spec)http=sys.modules["http"]=moduleloader=importlib.util.LazyLoader(spec.loader)loader.exec_module(module)defcheck():time.sleep(0.2)returnhttp.HTTPStatus.ACCEPTED==202defmulticheck():for_inrange(10):threading.Thread(target=check).start()ifsys.argv[1:]== ["single"]:check()else:multicheck()
The issue is here:
Lines 168 to 177 in6de8aa3
| class_LazyModule(types.ModuleType): | |
| """A subclass of the module type which triggers loading upon attribute access.""" | |
| def__getattribute__(self,attr): | |
| """Trigger the load of the module and return the attribute.""" | |
| # All module metadata must be garnered from __spec__ in order to avoid | |
| # using mutated values. | |
| # Stop triggering this method. | |
| self.__class__=types.ModuleType |
When attempting to access an attribute, the module's__dict__ is not updated until after__class__ is reset. If other threads attempt to access between these two points, then an attribute lookup can fail.
Assuming this is considered a bug, the two fixes I can think of are:
- A module-scoped lock that is used to protect
__getattribute__'s critical section. Theself.__class__ = type.ModuleTypewould need to be moved below__dict__.update(), which in turn would mean thatself.__spec__andself.__dict__would need to change toobject.__getattribute__(self, ...)lookups to avoid recursion. - A module-scoped dictionary of locks, one-per-
_LazyModule. Here, additional work would be needed to remove no-longer-needed locks without creating another critical section where a thread enters_LazyModule.__getattribute__but looks up its lock after it is removed by the first thread.
My suspicion is that one lock is enough, so I would suggest going with 1.
CPython versions tested on:
3.8, 3.10, 3.11, 3.12
Operating systems tested on:
Linux