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 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)