Skip to content

Commit ef976c5

Browse files
committed
Guard against import machinery masking out attributes
1 parent af0adfc commit ef976c5

1 file changed

Lines changed: 50 additions & 0 deletions

File tree

src/lazy_loader/__init__.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,36 @@
2121
threadlock = threading.Lock()
2222

2323

24+
class _ShadowGuardModule(types.ModuleType):
25+
"""Module type to protect function attributes from being overwritten.
26+
27+
When a function has the same name as the submodule it resides in
28+
(e.g. a ``max_tree`` function defined in ``max_tree.py``),
29+
importing that submodule causes the import machinery to call
30+
``setattr(pkg, "max_tree", <submodule>)``. That updates the
31+
package ``__dict__``, preventing ``__getattr__`` from ever
32+
resolving the name to the function again.
33+
34+
This subclass suppresses those dictionary updates (only in the
35+
shadowing case).
36+
37+
We track the set of protected names in the ``__lazy_shadowed__``
38+
attr.
39+
40+
"""
41+
42+
def __setattr__(self, name, value):
43+
shadowed = self.__dict__.get("__lazy_shadowed__")
44+
if (
45+
shadowed is not None
46+
and name in shadowed
47+
# Is it trying to set this attribute to the system module?
48+
and value is sys.modules.get(f"{self.__name__}.{name}")
49+
):
50+
return
51+
super().__setattr__(name, value)
52+
53+
2454
def attach(package_name, submodules=None, submod_attrs=None):
2555
"""Attach lazily loaded submodules, functions, or other attributes.
2656
@@ -92,6 +122,26 @@ def __getattr__(name):
92122
def __dir__():
93123
return __all__.copy()
94124

125+
# When a function attribute has the same name as the submodule it
126+
# resides in (e.g. `max_tree` from `max_tree.py`), importing that
127+
# submodule makes the import machinery overwrite the parent
128+
# package attribute with the module object, shadowing the function
129+
# (see _ShadowGuardModule).
130+
#
131+
# Record affected cases and, only in those cases, swap in the
132+
# guarding module type.
133+
shadowed = {attr for attr, mod in attr_to_modules.items() if attr == mod}
134+
if shadowed:
135+
pkg = sys.modules.get(package_name)
136+
# Only touch plain package modules (or our own wrapper) --- we
137+
# don't want to mess with custom module classes.
138+
if type(pkg) in (types.ModuleType, _ShadowGuardModule):
139+
pkg.__dict__["__lazy_shadowed__"] = (
140+
pkg.__dict__.get("__lazy_shadowed__", set()) | shadowed
141+
)
142+
if type(pkg) is types.ModuleType:
143+
pkg.__class__ = _ShadowGuardModule
144+
95145
eager_import = os.environ.get("EAGER_IMPORT", "") not in ("0", "")
96146
if eager_import:
97147
for attr in set(attr_to_modules.keys()) | submodules:

0 commit comments

Comments
 (0)