Skip to content

Commit 725692e

Browse files
split implementation up into multiple files
1 parent 5f28dfe commit 725692e

File tree

3 files changed

+206
-192
lines changed

3 files changed

+206
-192
lines changed

src/apipkg/__init__.py

+4-192
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,21 @@
77
"""
88
from __future__ import annotations
99

10-
import functools
10+
__all__ = ["initpkg", "ApiModule", "AliasModule"]
1111
import sys
12-
import threading
13-
from types import ModuleType
1412
from typing import Any
15-
from typing import Callable
16-
from typing import cast
17-
from typing import Iterable
1813

1914
from ._alias_module import AliasModule
20-
from ._importing import _py_abspath
2115
from ._importing import distribution_version as distribution_version # NOQA:F401
22-
from ._importing import importobj
16+
from ._module import _initpkg
17+
from ._module import ApiModule
2318
from ._version import version as __version__ # NOQA:F401
2419

25-
_PRESERVED_MODULE_ATTRS = {
26-
"__file__",
27-
"__version__",
28-
"__loader__",
29-
"__path__",
30-
"__package__",
31-
"__doc__",
32-
"__spec__",
33-
"__dict__",
34-
}
35-
3620

3721
def initpkg(
3822
pkgname: str,
3923
exportdefs: dict[str, Any],
40-
attr: dict[str, Any] | None = None,
24+
attr: dict[str, object] | None = None,
4125
eager: bool = False,
4226
) -> ApiModule:
4327
"""initialize given package from the export definitions."""
@@ -53,175 +37,3 @@ def initpkg(
5337
getattr(module, "__dict__")
5438

5539
return mod
56-
57-
58-
def _initpkg(mod: ModuleType | None, pkgname, exportdefs, attr=None) -> ApiModule:
59-
"""Helper for initpkg.
60-
61-
Python 3.3+ uses finer grained locking for imports, and checks sys.modules before
62-
acquiring the lock to avoid the overhead of the fine-grained locking. This
63-
introduces a race condition when a module is imported by multiple threads
64-
concurrently - some threads will see the initial module and some the replacement
65-
ApiModule. We avoid this by updating the existing module in-place.
66-
67-
"""
68-
if mod is None:
69-
d = {"__file__": None, "__spec__": None}
70-
d.update(attr)
71-
mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d)
72-
sys.modules[pkgname] = mod
73-
return mod
74-
else:
75-
f = getattr(mod, "__file__", None)
76-
if f:
77-
f = _py_abspath(f)
78-
mod.__file__ = f
79-
if hasattr(mod, "__path__"):
80-
mod.__path__ = [_py_abspath(p) for p in mod.__path__]
81-
if "__doc__" in exportdefs and hasattr(mod, "__doc__"):
82-
del mod.__doc__
83-
for name in dir(mod):
84-
if name not in _PRESERVED_MODULE_ATTRS:
85-
delattr(mod, name)
86-
87-
# Updating class of existing module as per importlib.util.LazyLoader
88-
mod.__class__ = ApiModule
89-
apimod = cast(ApiModule, mod)
90-
ApiModule.__init__(apimod, pkgname, exportdefs, implprefix=pkgname, attr=attr)
91-
return apimod
92-
93-
94-
def _synchronized(wrapped_function):
95-
"""Decorator to synchronise __getattr__ calls."""
96-
97-
# Lock shared between all instances of ApiModule to avoid possible deadlocks
98-
lock = threading.RLock()
99-
100-
@functools.wraps(wrapped_function)
101-
def synchronized_wrapper_function(*args, **kwargs):
102-
with lock:
103-
return wrapped_function(*args, **kwargs)
104-
105-
return synchronized_wrapper_function
106-
107-
108-
class ApiModule(ModuleType):
109-
"""the magical lazy-loading module standing"""
110-
111-
def __docget(self) -> str | None:
112-
try:
113-
return self.__doc
114-
except AttributeError:
115-
if "__doc__" in self.__map__:
116-
return cast(str, self.__makeattr("__doc__"))
117-
else:
118-
return None
119-
120-
def __docset(self, value: str) -> None:
121-
self.__doc = value
122-
123-
__doc__ = property(__docget, __docset) # type: ignore
124-
__map__: dict[str, tuple[str, str]]
125-
126-
def __init__(
127-
self,
128-
name: str,
129-
importspec: dict[str, Any],
130-
implprefix: str | None = None,
131-
attr: dict[str, Any] | None = None,
132-
) -> None:
133-
super().__init__(name)
134-
self.__name__ = name
135-
self.__all__ = [x for x in importspec if x != "__onfirstaccess__"]
136-
self.__map__ = {}
137-
self.__implprefix__ = implprefix or name
138-
if attr:
139-
for name, val in attr.items():
140-
setattr(self, name, val)
141-
for name, importspec in importspec.items():
142-
if isinstance(importspec, dict):
143-
subname = f"{self.__name__}.{name}"
144-
apimod = ApiModule(subname, importspec, implprefix)
145-
sys.modules[subname] = apimod
146-
setattr(self, name, apimod)
147-
else:
148-
parts = importspec.split(":")
149-
modpath = parts.pop(0)
150-
attrname = parts and parts[0] or ""
151-
if modpath[0] == ".":
152-
modpath = implprefix + modpath
153-
154-
if not attrname:
155-
subname = f"{self.__name__}.{name}"
156-
apimod = AliasModule(subname, modpath)
157-
sys.modules[subname] = apimod
158-
if "." not in name:
159-
setattr(self, name, apimod)
160-
else:
161-
self.__map__[name] = (modpath, attrname)
162-
163-
def __repr__(self):
164-
repr_list = [f"<ApiModule {self.__name__!r}"]
165-
if hasattr(self, "__version__"):
166-
repr_list.append(f" version={self.__version__!r}")
167-
if hasattr(self, "__file__"):
168-
repr_list.append(f" from {self.__file__!r}")
169-
repr_list.append(">")
170-
return "".join(repr_list)
171-
172-
@_synchronized
173-
def __makeattr(self, name, isgetattr=False):
174-
"""lazily compute value for name or raise AttributeError if unknown."""
175-
target = None
176-
if "__onfirstaccess__" in self.__map__:
177-
target = self.__map__.pop("__onfirstaccess__")
178-
fn = cast(Callable[[], None], importobj(*target))
179-
fn()
180-
try:
181-
modpath, attrname = self.__map__[name]
182-
except KeyError:
183-
# __getattr__ is called when the attribute does not exist, but it may have
184-
# been set by the onfirstaccess call above. Infinite recursion is not
185-
# possible as __onfirstaccess__ is removed before the call (unless the call
186-
# adds __onfirstaccess__ to __map__ explicitly, which is not our problem)
187-
if target is not None and name != "__onfirstaccess__":
188-
return getattr(self, name)
189-
# Attribute may also have been set during a concurrent call to __getattr__
190-
# which executed after this call was already waiting on the lock. Check
191-
# for a recently set attribute while avoiding infinite recursion:
192-
# * Don't call __getattribute__ if __makeattr was called from a data
193-
# descriptor such as the __doc__ or __dict__ properties, since data
194-
# descriptors are called as part of object.__getattribute__
195-
# * Only call __getattribute__ if there is a possibility something has set
196-
# the attribute we're looking for since __getattr__ was called
197-
if threading is not None and isgetattr:
198-
return super().__getattribute__(name)
199-
raise AttributeError(name)
200-
else:
201-
result = importobj(modpath, attrname)
202-
setattr(self, name, result)
203-
# in a recursive-import situation a double-del can happen
204-
self.__map__.pop(name, None)
205-
return result
206-
207-
def __getattr__(self, name):
208-
return self.__makeattr(name, isgetattr=True)
209-
210-
def __dir__(self) -> Iterable[str]:
211-
yield from super().__dir__()
212-
yield from self.__map__
213-
214-
@property
215-
def __dict__(self) -> dict[str, Any]: # type: ignore
216-
# force all the content of the module
217-
# to be loaded when __dict__ is read
218-
dictdescr = ModuleType.__dict__["__dict__"] # type: ignore
219-
ns: dict[str, Any] = dictdescr.__get__(self)
220-
if ns is not None:
221-
hasattr(self, "some")
222-
for name in self.__all__:
223-
try:
224-
self.__makeattr(name)
225-
except AttributeError:
226-
pass
227-
return ns

0 commit comments

Comments
 (0)