|
21 | 21 | threadlock = threading.Lock() |
22 | 22 |
|
23 | 23 |
|
| 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 | + |
24 | 54 | def attach(package_name, submodules=None, submod_attrs=None): |
25 | 55 | """Attach lazily loaded submodules, functions, or other attributes. |
26 | 56 |
|
@@ -92,6 +122,26 @@ def __getattr__(name): |
92 | 122 | def __dir__(): |
93 | 123 | return __all__.copy() |
94 | 124 |
|
| 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 | + |
95 | 145 | eager_import = os.environ.get("EAGER_IMPORT", "") not in ("0", "") |
96 | 146 | if eager_import: |
97 | 147 | for attr in set(attr_to_modules.keys()) | submodules: |
|
0 commit comments