lazyload-py 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lazyload/__init__.py +42 -0
- lazyload/_api.py +257 -0
- lazyload/_compat.py +627 -0
- lazyload/_context.py +50 -0
- lazyload/_decorator.py +51 -0
- lazyload/_exceptions.py +407 -0
- lazyload/_lazy.py +34 -0
- lazyload/_native.py +366 -0
- lazyload/_types.py +22 -0
- lazyload/_version.py +153 -0
- lazyload/py.typed +2 -0
- lazyload_py-0.1.0.dist-info/METADATA +209 -0
- lazyload_py-0.1.0.dist-info/RECORD +15 -0
- lazyload_py-0.1.0.dist-info/WHEEL +4 -0
- lazyload_py-0.1.0.dist-info/licenses/LICENSE +21 -0
lazyload/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""lazyload — Lazy imports for Python 3.10 through 3.15+, one API, every version.
|
|
2
|
+
|
|
3
|
+
This package gives developers a single, ergonomic interface for deferring module
|
|
4
|
+
imports until the moment they are first used. Whether you are trimming the startup
|
|
5
|
+
time of a CLI, preventing circular-import tangles in a large library, or writing
|
|
6
|
+
optional-dependency guards without boilerplate, lazyload exposes three complementary
|
|
7
|
+
tools that cover every common pattern cleanly and without runtime overhead once the
|
|
8
|
+
import is resolved:
|
|
9
|
+
|
|
10
|
+
* :func:`lazy` — defer a single named module.
|
|
11
|
+
* :func:`lazy_imports` — defer a group of imports inside a ``with`` block.
|
|
12
|
+
* :func:`lazy_module` — defer imports inside a function until its first call.
|
|
13
|
+
|
|
14
|
+
On Python 3.15+ the package delegates to CPython's native PEP 810 machinery.
|
|
15
|
+
On Python 3.10–3.14 a transparent deferred-proxy shim provides identical behaviour.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from lazyload._api import lazy, lazy_imports, lazy_module
|
|
21
|
+
from lazyload._exceptions import (
|
|
22
|
+
CircularImportError,
|
|
23
|
+
LazyLoadError,
|
|
24
|
+
ModuleNotFoundError,
|
|
25
|
+
UnsupportedPythonVersion,
|
|
26
|
+
)
|
|
27
|
+
from lazyload._version import __version__
|
|
28
|
+
|
|
29
|
+
__author__: str = "jithubaiju55"
|
|
30
|
+
__license__: str = "MIT"
|
|
31
|
+
|
|
32
|
+
__all__: list[str] = [
|
|
33
|
+
# Core API
|
|
34
|
+
"lazy",
|
|
35
|
+
"lazy_imports",
|
|
36
|
+
"lazy_module",
|
|
37
|
+
# Exceptions
|
|
38
|
+
"LazyLoadError",
|
|
39
|
+
"UnsupportedPythonVersion",
|
|
40
|
+
"CircularImportError",
|
|
41
|
+
"ModuleNotFoundError",
|
|
42
|
+
]
|
lazyload/_api.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""lazyload._api — Version-aware dispatcher for the public API.
|
|
2
|
+
|
|
3
|
+
Why this file exists
|
|
4
|
+
--------------------
|
|
5
|
+
The public symbols ``lazy``, ``lazy_imports``, and ``lazy_module`` need to
|
|
6
|
+
work identically on every supported Python version, but the *mechanism* that
|
|
7
|
+
makes them work is completely different between Python 3.10–3.14 (the
|
|
8
|
+
deferred-proxy shim in :mod:`lazyload._compat`) and Python 3.15+ (the native
|
|
9
|
+
CPython machinery in :mod:`lazyload._native`).
|
|
10
|
+
|
|
11
|
+
This file is the single place where that routing decision is made.
|
|
12
|
+
|
|
13
|
+
Why not inline this into ``__init__.py``?
|
|
14
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
15
|
+
``__init__.py`` is the *public face* of the package — its job is to declare
|
|
16
|
+
what the package exports and document it for users. Mixing version-dispatch
|
|
17
|
+
logic into it would blur that responsibility and make both files harder to
|
|
18
|
+
read, test, and maintain. Keeping the dispatch here means:
|
|
19
|
+
|
|
20
|
+
* ``__init__.py`` stays a clean, one-paragraph reception desk.
|
|
21
|
+
* This module can be imported independently in tests to verify routing.
|
|
22
|
+
* Adding a third backend in the future (e.g. a C-extension fast path) only
|
|
23
|
+
requires editing this file and adding the new backend module.
|
|
24
|
+
|
|
25
|
+
How routing works — zero call-time overhead
|
|
26
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
27
|
+
The routing decision happens **once**, at the moment this module is first
|
|
28
|
+
imported, not at each individual call. The ``if NATIVE_LAZY_IMPORTS`` branch
|
|
29
|
+
at module level binds ``_backend_lazy``, ``_backend_lazy_imports``, and
|
|
30
|
+
``_backend_lazy_module`` to the appropriate backend callables. Each public
|
|
31
|
+
function then delegates to those module-level names with a single Python call
|
|
32
|
+
— no ``if`` branch, no ``getattr``, no lookup in any dispatch table.
|
|
33
|
+
|
|
34
|
+
This module is **internal**. Users should import from :mod:`lazyload`
|
|
35
|
+
directly, not from ``lazyload._api``.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from collections.abc import Callable
|
|
41
|
+
from types import ModuleType
|
|
42
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
43
|
+
|
|
44
|
+
from lazyload._version import NATIVE_LAZY_IMPORTS
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from lazyload._types import LazyImportsProtocol
|
|
48
|
+
|
|
49
|
+
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
50
|
+
_backend_lazy_imports: Callable[[], LazyImportsProtocol]
|
|
51
|
+
|
|
52
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
# Backend selection — happens once, at import time
|
|
54
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
if NATIVE_LAZY_IMPORTS:
|
|
57
|
+
# Python 3.15+ — delegate to CPython's own PEP 810 machinery.
|
|
58
|
+
from lazyload._native import _lazy_module_native as _backend_lazy_module
|
|
59
|
+
from lazyload._native import _lazy_native as _backend_lazy
|
|
60
|
+
from lazyload._native import _LazyImportsNative as _backend_lazy_imports
|
|
61
|
+
else:
|
|
62
|
+
# Python 3.10–3.14 — delegate to the deferred-proxy shim.
|
|
63
|
+
from lazyload._compat import _lazy_compat as _backend_lazy
|
|
64
|
+
from lazyload._compat import _lazy_module_compat as _backend_lazy_module
|
|
65
|
+
from lazyload._compat import _LazyImportsCompat as _backend_lazy_imports
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
# Public API functions
|
|
70
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def lazy(name: str, package: str | None = None) -> ModuleType:
|
|
74
|
+
"""Defer loading of a module until its first attribute is accessed.
|
|
75
|
+
|
|
76
|
+
Call ``lazy()`` anywhere you would write a regular ``import`` statement
|
|
77
|
+
when you want to postpone the cost of loading that module. The returned
|
|
78
|
+
object looks and behaves exactly like the real module — you can pass it
|
|
79
|
+
around, assign it, and use it as normal — but the actual file-system read,
|
|
80
|
+
bytecode compilation, and module body execution are all held back until the
|
|
81
|
+
first time you touch any attribute on it.
|
|
82
|
+
|
|
83
|
+
This is the simplest and most explicit form of lazy loading. Use it when
|
|
84
|
+
you want to defer a single, named module, and you know the module's name
|
|
85
|
+
at the point where you write the call.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
name:
|
|
90
|
+
The module to defer. Accepts the same forms as a regular ``import``
|
|
91
|
+
statement: a plain name (``"numpy"``), a dotted path
|
|
92
|
+
(``"email.mime.text"``), or a relative name starting with dots
|
|
93
|
+
(``".utils"``).
|
|
94
|
+
package:
|
|
95
|
+
Required only when *name* is a relative import (starts with ``"."``).
|
|
96
|
+
Pass the ``__name__`` attribute of the calling module so that the
|
|
97
|
+
relative path can be resolved correctly. Ignored for absolute names.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
-------
|
|
101
|
+
ModuleType
|
|
102
|
+
A module proxy. On Python 3.15+ this is CPython's native
|
|
103
|
+
``LazyModuleProxy``; on 3.10–3.14 it is a :class:`~lazyload._compat.
|
|
104
|
+
_DeferredProxy`. Either way it quacks exactly like the real module
|
|
105
|
+
once any attribute is accessed.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
------
|
|
109
|
+
ImportError
|
|
110
|
+
If *name* is relative (starts with ``"."``) and *package* is ``None``.
|
|
111
|
+
ModuleNotFoundError
|
|
112
|
+
If the module cannot be found on :data:`sys.path`. The error is
|
|
113
|
+
raised at the moment you first access an attribute, not when you call
|
|
114
|
+
``lazy()``.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
-------
|
|
118
|
+
Imagine you have a CLI tool that only needs ``pandas`` when the user runs
|
|
119
|
+
a specific sub-command. Instead of paying the full ``pandas`` import cost
|
|
120
|
+
at startup for every invocation, you write::
|
|
121
|
+
|
|
122
|
+
import lazyload
|
|
123
|
+
|
|
124
|
+
pd = lazyload.lazy("pandas") # ← no disk I/O here
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def summarise(path):
|
|
128
|
+
# pandas is imported here, on the first call to summarise()
|
|
129
|
+
return pd.read_csv(path).describe()
|
|
130
|
+
|
|
131
|
+
The user launching ``--help`` or any other sub-command never waits for
|
|
132
|
+
``pandas`` to load. Only callers of ``summarise()`` pay the import cost,
|
|
133
|
+
and they pay it exactly once.
|
|
134
|
+
"""
|
|
135
|
+
return _backend_lazy(name, package)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def lazy_imports() -> Any:
|
|
139
|
+
"""Return a context manager that defers every ``import X`` in its block.
|
|
140
|
+
|
|
141
|
+
Use ``lazy_imports()`` when you want to defer a *group* of imports that
|
|
142
|
+
are written together in the same place. Inside the ``with`` block, every
|
|
143
|
+
bare ``import X`` statement is intercepted and converted to a lazy proxy;
|
|
144
|
+
nothing is actually loaded from the file system until one of the imported
|
|
145
|
+
names is first used.
|
|
146
|
+
|
|
147
|
+
This is the most readable form of lazy loading for modules you import at
|
|
148
|
+
the top of a function or method body, because it lets you keep the
|
|
149
|
+
``import`` statements exactly as you would write them normally — you just
|
|
150
|
+
wrap them in a ``with`` block.
|
|
151
|
+
|
|
152
|
+
.. note::
|
|
153
|
+
|
|
154
|
+
``from X import Y`` statements are **always eager** — because Python
|
|
155
|
+
must load module ``X`` before it can resolve attribute ``Y``, there is
|
|
156
|
+
no way to defer them without loading the module anyway. Use
|
|
157
|
+
:func:`lazy` for those cases instead.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
None
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
-------
|
|
165
|
+
contextmanager
|
|
166
|
+
A context manager. On Python 3.15+ this wraps ``importlib.lazy()``;
|
|
167
|
+
on 3.10–3.14 it temporarily patches ``builtins.__import__`` with a
|
|
168
|
+
lazy interceptor and restores it on exit. Both behave identically
|
|
169
|
+
from the caller's perspective.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
-------
|
|
173
|
+
Suppose you have a data-processing function that relies on several heavy
|
|
174
|
+
scientific libraries, but those libraries are only an optional dependency
|
|
175
|
+
that not all users install. You can defer all of their imports to the
|
|
176
|
+
moment the function is first called::
|
|
177
|
+
|
|
178
|
+
import lazyload
|
|
179
|
+
|
|
180
|
+
with lazyload.lazy_imports():
|
|
181
|
+
import numpy as np
|
|
182
|
+
import scipy.signal as signal
|
|
183
|
+
import matplotlib.pyplot as plt
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def plot_spectrum(data):
|
|
187
|
+
# numpy, scipy, and matplotlib are loaded here on first use,
|
|
188
|
+
# not at the top of the module.
|
|
189
|
+
freqs = np.fft.rfftfreq(len(data))
|
|
190
|
+
plt.plot(freqs, np.abs(signal.periodogram(data)[1]))
|
|
191
|
+
plt.show()
|
|
192
|
+
|
|
193
|
+
If the user never calls ``plot_spectrum()``, none of those three libraries
|
|
194
|
+
are ever loaded.
|
|
195
|
+
"""
|
|
196
|
+
return _backend_lazy_imports()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def lazy_module(fn: _F) -> _F:
|
|
200
|
+
"""Decorator that defers every ``import X`` inside a function until first call.
|
|
201
|
+
|
|
202
|
+
Apply ``@lazy_module`` to any function whose body contains ``import``
|
|
203
|
+
statements that you want to defer. The decorated function behaves
|
|
204
|
+
identically to the original — same name, same signature, same return value
|
|
205
|
+
— but on its *first* invocation the import statements inside it are
|
|
206
|
+
silently converted to lazy proxies. On all subsequent calls, the function
|
|
207
|
+
runs exactly as written; the modules are already cached in
|
|
208
|
+
:data:`sys.modules` and the import statements resolve instantly.
|
|
209
|
+
|
|
210
|
+
This is the most convenient form of lazy loading for functions that import
|
|
211
|
+
heavy dependencies internally, because it requires no change to the code
|
|
212
|
+
inside the function body — you simply add one decorator line.
|
|
213
|
+
|
|
214
|
+
.. note::
|
|
215
|
+
|
|
216
|
+
Like :func:`lazy_imports`, this decorator only intercepts bare
|
|
217
|
+
``import X`` statements. ``from X import Y`` statements remain eager.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
fn:
|
|
222
|
+
Any callable. The decorator preserves its ``__name__``,
|
|
223
|
+
``__doc__``, ``__annotations__``, and ``__wrapped__`` attributes via
|
|
224
|
+
:func:`functools.wraps`, so introspection tools and type-checkers see
|
|
225
|
+
the original function unchanged.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
-------
|
|
229
|
+
Callable
|
|
230
|
+
A wrapper with the same type signature as *fn*.
|
|
231
|
+
|
|
232
|
+
Example:
|
|
233
|
+
-------
|
|
234
|
+
Consider a module that converts documents using a heavyweight library.
|
|
235
|
+
Without ``@lazy_module``, every program that imports your module pays the
|
|
236
|
+
full import cost of the library — even if it never calls
|
|
237
|
+
``convert_document``::
|
|
238
|
+
|
|
239
|
+
import lazyload
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@lazyload.lazy_module
|
|
243
|
+
def convert_document(path, fmt="pdf"):
|
|
244
|
+
import weasyprint # ← deferred until convert_document()
|
|
245
|
+
import pypandoc # ← deferred until convert_document()
|
|
246
|
+
|
|
247
|
+
if fmt == "pdf":
|
|
248
|
+
return weasyprint.HTML(path).write_pdf()
|
|
249
|
+
return pypandoc.convert_file(path, fmt)
|
|
250
|
+
|
|
251
|
+
The first time ``convert_document()`` is called, ``weasyprint`` and
|
|
252
|
+
``pypandoc`` are imported. Every subsequent call finds them already
|
|
253
|
+
in :data:`sys.modules` and runs at full speed. Any program that imports
|
|
254
|
+
your module but never calls ``convert_document()`` pays zero import cost
|
|
255
|
+
for those libraries.
|
|
256
|
+
"""
|
|
257
|
+
return _backend_lazy_module(fn)
|