Skip to content

Commit ccf1426

Browse files
improve perf, dogfood
1 parent af0adfc commit ccf1426

2 files changed

Lines changed: 54 additions & 40 deletions

File tree

src/lazy_loader/__init__.py

Lines changed: 43 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,18 @@
55
Makes it easy to load subpackages and functions on demand.
66
"""
77

8-
import ast
8+
import _thread
99
import importlib
10-
import importlib.util
1110
import os
1211
import sys
13-
import threading
1412
import types
15-
import warnings
1613

1714
__version__ = "0.6rc0.dev0"
1815
__all__ = ["attach", "attach_stub", "load"]
1916

2017

21-
threadlock = threading.Lock()
18+
# Same lock type as threading.Lock(), without the threading import cost
19+
threadlock = _thread.allocate_lock()
2220

2321

2422
def attach(package_name, submodules=None, submod_attrs=None):
@@ -72,23 +70,23 @@ def attach(package_name, submodules=None, submod_attrs=None):
7270

7371
def __getattr__(name):
7472
if name in submodules:
75-
return importlib.import_module(f"{package_name}.{name}")
73+
attr = importlib.import_module(f"{package_name}.{name}")
7674
elif name in attr_to_modules:
7775
submod_path = f"{package_name}.{attr_to_modules[name]}"
7876
submod = importlib.import_module(submod_path)
7977
attr = getattr(submod, name)
80-
81-
# If the attribute lives in a file (module) with the same
82-
# name as the attribute, ensure that the attribute and *not*
83-
# the module is accessible on the package.
84-
if name == attr_to_modules[name]:
85-
pkg = sys.modules[package_name]
86-
pkg.__dict__[name] = attr
87-
88-
return attr
8978
else:
9079
raise AttributeError(f"No {package_name} attribute {name}")
9180

81+
# Cache the resolved value on the package so that subsequent
82+
# accesses bypass __getattr__; this also ensures an attribute
83+
# shadows a same-named submodule.
84+
pkg = sys.modules.get(package_name)
85+
if pkg is not None:
86+
pkg.__dict__[name] = attr
87+
88+
return attr
89+
9290
def __dir__():
9391
return __all__.copy()
9492

@@ -191,7 +189,11 @@ def myfunc():
191189
if have_module and require is None:
192190
return module
193191

192+
import importlib.util
193+
194194
if not suppress_warning and "." in fullname:
195+
import warnings
196+
195197
msg = (
196198
"subpackages can technically be lazily loaded, but it causes the "
197199
"package to be eagerly loaded even if it is already lazily loaded. "
@@ -276,31 +278,6 @@ def _check_requirement(require: str) -> bool:
276278
)
277279

278280

279-
class _StubVisitor(ast.NodeVisitor):
280-
"""AST visitor to parse a stub file for submodules and submod_attrs."""
281-
282-
def __init__(self):
283-
self._submodules = set()
284-
self._submod_attrs = {}
285-
286-
def visit_ImportFrom(self, node: ast.ImportFrom):
287-
if node.level != 1:
288-
raise ValueError(
289-
"Only within-module imports are supported (`from .* import`)"
290-
)
291-
if node.module:
292-
attrs: list = self._submod_attrs.setdefault(node.module, [])
293-
aliases = [alias.name for alias in node.names]
294-
if "*" in aliases:
295-
raise ValueError(
296-
"lazy stub loader does not support star import "
297-
f"`from {node.module} import *`"
298-
)
299-
attrs.extend(aliases)
300-
else:
301-
self._submodules.update(alias.name for alias in node.names)
302-
303-
304281
def attach_stub(package_name: str, filename: str):
305282
"""Attach lazily loaded submodules, functions from a type stub.
306283
@@ -327,6 +304,32 @@ def attach_stub(package_name: str, filename: str):
327304
If a stub file is not found for `filename`, or if the stubfile is formmated
328305
incorrectly (e.g. if it contains an relative import from outside of the module)
329306
"""
307+
import ast
308+
309+
class _StubVisitor(ast.NodeVisitor):
310+
"""AST visitor to parse a stub file for submodules and submod_attrs."""
311+
312+
def __init__(self):
313+
self._submodules = set()
314+
self._submod_attrs = {}
315+
316+
def visit_ImportFrom(self, node: ast.ImportFrom):
317+
if node.level != 1:
318+
raise ValueError(
319+
"Only within-module imports are supported (`from .* import`)"
320+
)
321+
if node.module:
322+
attrs: list = self._submod_attrs.setdefault(node.module, [])
323+
aliases = [alias.name for alias in node.names]
324+
if "*" in aliases:
325+
raise ValueError(
326+
"lazy stub loader does not support star import "
327+
f"`from {node.module} import *`"
328+
)
329+
attrs.extend(aliases)
330+
else:
331+
self._submodules.update(alias.name for alias in node.names)
332+
330333
stubfile = (
331334
filename if filename.endswith("i") else f"{os.path.splitext(filename)[0]}.pyi"
332335
)

tests/test_lazy_loader.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,17 @@ def test_attach_same_module_and_attr_name(clean_fake_pkg, eager_import):
178178
assert isinstance(some_func, types.FunctionType)
179179

180180

181+
def test_attach_caches_resolved_attrs(clean_fake_pkg):
182+
from tests import fake_pkg
183+
184+
assert "aux_func" not in vars(fake_pkg)
185+
aux_func = fake_pkg.aux_func
186+
# The resolved attribute is cached on the package, so later accesses
187+
# do not go through __getattr__ again
188+
assert vars(fake_pkg)["aux_func"] is aux_func
189+
assert fake_pkg.aux_func is aux_func
190+
191+
181192
FAKE_STUB = """
182193
from . import rank
183194
from ._gaussian import gaussian

0 commit comments

Comments
 (0)