Skip to content

Commit eba9efa

Browse files
committed
Add lazy loader from skimage and add README/LICENSE
0 parents  commit eba9efa

File tree

3 files changed

+247
-0
lines changed

3 files changed

+247
-0
lines changed

LICENSE.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2022, Scientific Python project
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
1. Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
2. Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
3. Neither the name of the copyright holder nor the names of its
17+
contributors may be used to endorse or promote products derived from
18+
this software without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
`lazy_loader` makes it easy to load subpackages and functions on demand.
2+
3+
## Motivation
4+
5+
1. Allow subpackages to be made visible to users without incurring import costs.
6+
2. Allow external libraries to be imported only when used, improving import times.
7+
8+
For a more detailed discussion, see [the SPEC](https://scientific-python.org/specs/spec-0001/).
9+
10+
## Usage
11+
12+
### Lazily load subpackages
13+
14+
Consider the `__init__.py` from [scikit-image](https://scikit-image.org):
15+
16+
```python
17+
subpackages = [
18+
...
19+
'filters',
20+
...
21+
]
22+
23+
from lazy_loader import lazy
24+
__getattr__, __dir__, _ = lazy.attach(__name__, subpackages)
25+
```
26+
27+
You can now do:
28+
29+
```python
30+
import skimage as ski
31+
ski.filters.gaussian(...)
32+
```
33+
34+
The `filters` subpackages will only be loaded once accessed.
35+
36+
### Lazily load subpackages and functions
37+
38+
Consider `skimage/filters/__init__.py`:
39+
40+
```python
41+
from ..util import lazy
42+
43+
__getattr__, __dir__, __all__ = lazy.attach(
44+
__name__,
45+
submodules=['rank']
46+
submod_attrs={
47+
'_gaussian': ['gaussian', 'difference_of_gaussians'],
48+
'edges': ['sobel', 'scharr', 'prewitt', 'roberts',
49+
'laplace', 'farid']
50+
}
51+
)
52+
```
53+
54+
The above is equivalent to:
55+
56+
```python
57+
from . import rank
58+
from ._gaussian import gaussian, difference_of_gaussians
59+
from .edges import (sobel, scharr, prewitt, roberts,
60+
laplace, farid)
61+
```
62+
63+
Except that all subpackages (such as `rank`) and functions (such as `sobel`) are loaded upon access.
64+
65+
### Early failure
66+
67+
With lazy loading, missing imports no longer fail upon loading the
68+
library. During development and testing, you can set the `EAGER_IMPORT`
69+
environment variable to disable lazy loading.
70+
71+
### External libraries
72+
73+
The `lazy.attach` function discussed above is used to set up package
74+
internal imports.
75+
76+
Use `lazy.load` to lazily import external libraries:
77+
78+
```python
79+
linalg = lazy.load('scipy.linalg') # `linalg` will only be loaded when accessed
80+
```

lazy_loader/__init__.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import importlib
2+
import importlib.util
3+
import os
4+
import sys
5+
6+
7+
def attach(package_name, submodules=None, submod_attrs=None):
8+
"""Attach lazily loaded submodules, functions, or other attributes.
9+
10+
Typically, modules import submodules and attributes as follows::
11+
12+
import mysubmodule
13+
import anothersubmodule
14+
15+
from .foo import someattr
16+
17+
The idea is to replace a package's `__getattr__`, `__dir__`, and
18+
`__all__`, such that all imports work exactly the way they did
19+
before, except that they are only imported when used.
20+
21+
The typical way to call this function, replacing the above imports, is::
22+
23+
__getattr__, __dir__, __all__ = lazy.attach(
24+
__name__,
25+
['mysubmodule', 'anothersubmodule'],
26+
{'foo': ['someattr']}
27+
)
28+
29+
This functionality requires Python 3.7 or higher.
30+
31+
Parameters
32+
----------
33+
package_name : str
34+
Typically use ``__name__``.
35+
submodules : set
36+
List of submodules to attach.
37+
submod_attrs : dict
38+
Dictionary of submodule -> list of attributes / functions.
39+
These attributes are imported as they are used.
40+
41+
Returns
42+
-------
43+
__getattr__, __dir__, __all__
44+
45+
"""
46+
if submod_attrs is None:
47+
submod_attrs = {}
48+
49+
if submodules is None:
50+
submodules = set()
51+
else:
52+
submodules = set(submodules)
53+
54+
attr_to_modules = {
55+
attr: mod for mod, attrs in submod_attrs.items() for attr in attrs
56+
}
57+
58+
__all__ = list(submodules | attr_to_modules.keys())
59+
60+
def __getattr__(name):
61+
if name in submodules:
62+
return importlib.import_module(f'{package_name}.{name}')
63+
elif name in attr_to_modules:
64+
submod = importlib.import_module(
65+
f'{package_name}.{attr_to_modules[name]}'
66+
)
67+
return getattr(submod, name)
68+
else:
69+
raise AttributeError(f'No {package_name} attribute {name}')
70+
71+
def __dir__():
72+
return __all__
73+
74+
if os.environ.get('EAGER_IMPORT', ''):
75+
for attr in set(attr_to_modules.keys()) | submodules:
76+
__getattr__(attr)
77+
78+
return __getattr__, __dir__, list(__all__)
79+
80+
81+
def load(fullname):
82+
"""Return a lazily imported proxy for a module.
83+
84+
We often see the following pattern::
85+
86+
def myfunc():
87+
from numpy import linalg as la
88+
la.norm(...)
89+
....
90+
91+
This is to prevent a module, in this case `numpy`, from being
92+
imported at function definition time, since that can be slow.
93+
94+
This function provides a proxy module that, upon access, imports
95+
the actual module. So the idiom equivalent to the above example is::
96+
97+
la = lazy.load("numpy.linalg")
98+
99+
def myfunc():
100+
la.norm(...)
101+
....
102+
103+
The initial import time is fast because the actual import is delayed
104+
until the first attribute is requested. The overall import time may
105+
decrease as well for users that don't make use of large portions
106+
of the library.
107+
108+
Parameters
109+
----------
110+
fullname : str
111+
The full name of the module or submodule to import. For example::
112+
113+
sp = lazy.load('scipy') # import scipy as sp
114+
spla = lazy.load('scipy.linalg') # import scipy.linalg as spla
115+
116+
Returns
117+
-------
118+
pm : importlib.util._LazyModule
119+
Proxy module. Can be used like any regularly imported module.
120+
Actual loading of the module occurs upon first attribute request.
121+
122+
"""
123+
try:
124+
return sys.modules[fullname]
125+
except KeyError:
126+
pass
127+
128+
spec = importlib.util.find_spec(fullname)
129+
if spec is None:
130+
raise ModuleNotFoundError(f"No module name '{fullname}'")
131+
132+
module = importlib.util.module_from_spec(spec)
133+
sys.modules[fullname] = module
134+
135+
loader = importlib.util.LazyLoader(spec.loader)
136+
loader.exec_module(module)
137+
138+
return module

0 commit comments

Comments
 (0)