gh-117182: Allow lazily loaded modules to modify their own __class__

This commit is contained in:
Chris Markiewicz 2024-04-08 23:08:48 -04:00 committed by GitHub
parent ac45766673
commit 19a2202067
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 38 additions and 4 deletions

View file

@ -178,15 +178,17 @@ def __getattribute__(self, attr):
# Only the first thread to get the lock should trigger the load # Only the first thread to get the lock should trigger the load
# and reset the module's class. The rest can now getattr(). # and reset the module's class. The rest can now getattr().
if object.__getattribute__(self, '__class__') is _LazyModule: if object.__getattribute__(self, '__class__') is _LazyModule:
__class__ = loader_state['__class__']
# Reentrant calls from the same thread must be allowed to proceed without # Reentrant calls from the same thread must be allowed to proceed without
# triggering the load again. # triggering the load again.
# exec_module() and self-referential imports are the primary ways this can # exec_module() and self-referential imports are the primary ways this can
# happen, but in any case we must return something to avoid deadlock. # happen, but in any case we must return something to avoid deadlock.
if loader_state['is_loading']: if loader_state['is_loading']:
return object.__getattribute__(self, attr) return __class__.__getattribute__(self, attr)
loader_state['is_loading'] = True loader_state['is_loading'] = True
__dict__ = object.__getattribute__(self, '__dict__') __dict__ = __class__.__getattribute__(self, '__dict__')
# All module metadata must be gathered from __spec__ in order to avoid # All module metadata must be gathered from __spec__ in order to avoid
# using mutated values. # using mutated values.
@ -216,8 +218,10 @@ def __getattribute__(self, attr):
# Update after loading since that's what would happen in an eager # Update after loading since that's what would happen in an eager
# loading situation. # loading situation.
__dict__.update(attrs_updated) __dict__.update(attrs_updated)
# Finally, stop triggering this method. # Finally, stop triggering this method, if the module did not
self.__class__ = types.ModuleType # already update its own __class__.
if isinstance(self, _LazyModule):
object.__setattr__(self, '__class__', __class__)
return getattr(self, attr) return getattr(self, attr)

View file

@ -196,6 +196,34 @@ def test_lazy_self_referential_modules(self):
test_load = module.loads('{}') test_load = module.loads('{}')
self.assertEqual(test_load, {}) self.assertEqual(test_load, {})
def test_lazy_module_type_override(self):
# Verify that lazy loading works with a module that modifies
# its __class__ to be a custom type.
# Example module from PEP 726
module = self.new_module(source_code="""\
import sys
from types import ModuleType
CONSTANT = 3.14
class ImmutableModule(ModuleType):
def __setattr__(self, name, value):
raise AttributeError('Read-only attribute!')
def __delattr__(self, name):
raise AttributeError('Read-only attribute!')
sys.modules[__name__].__class__ = ImmutableModule
""")
sys.modules[TestingImporter.module_name] = module
self.assertIsInstance(module, util._LazyModule)
self.assertEqual(module.CONSTANT, 3.14)
with self.assertRaises(AttributeError):
module.CONSTANT = 2.71
with self.assertRaises(AttributeError):
del module.CONSTANT
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -0,0 +1,2 @@
Lazy-loading of modules that modify their own ``__class__`` no longer
reverts the ``__class__`` to :class:`types.ModuleType`.