mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
perf: import package re-exports on first use (#724)
src/common/__init__.py and src/plugin_system/__init__.py resolve their re-exports lazily (PEP 562 __getattr__, __all__ and __dir__ unchanged, TYPE_CHECKING imports for mypy), and sync_manager imports numpy only where send_frame uses it. The web process no longer loads numpy, freetype helpers and PluginManager just to import path_safety, store_manager or schema_manager (~67 MB to ~54 MB RSS on a Pi 4). from src.common import X and from src.plugin_system import X keep working, including submodule imports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""src.common and src.plugin_system import their exports lazily (PEP 562).
|
||||
|
||||
The web interface imports both packages only for small submodules
|
||||
(path_safety, snapshot_policy, store_manager, ...). When their __init__
|
||||
imported every export eagerly, that dragged numpy, freetype and the plugin
|
||||
manager into a process that never uses them -- about 13 MB of RSS on a Pi.
|
||||
|
||||
These tests pin both halves of the change: the package import stays light,
|
||||
and every exported name still resolves to the very object its home module
|
||||
defines, so ``isinstance`` and ``is`` checks behave as before.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
#: Modules the bare package import must not load. numpy is the one that
|
||||
#: matters for memory; the rest are what the eager __init__ used to import.
|
||||
HEAVY = (
|
||||
"numpy",
|
||||
"freetype",
|
||||
"src.adaptive_layout",
|
||||
"src.common.api_helper",
|
||||
"src.common.logo_helper",
|
||||
"src.common.scroll_helper",
|
||||
"src.plugin_system.base_plugin",
|
||||
"src.plugin_system.plugin_manager",
|
||||
)
|
||||
|
||||
|
||||
def _run(script):
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", textwrap.dedent(script)], cwd=str(REPO_ROOT),
|
||||
capture_output=True, text=True, timeout=120)
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("package", ["src.common", "src.plugin_system"])
|
||||
def test_package_import_loads_nothing_heavy(package):
|
||||
# A fresh interpreter: this test process has long since imported them.
|
||||
loaded = _run(f"""
|
||||
import json, sys
|
||||
import {package}
|
||||
print(json.dumps(sorted(m for m in {HEAVY!r} if m in sys.modules)))
|
||||
""")
|
||||
assert loaded == [], f"importing {package} loaded {loaded}"
|
||||
|
||||
|
||||
def test_web_interface_submodules_do_not_load_numpy():
|
||||
# The imports web_interface/app.py and its API blueprint make from these
|
||||
# packages. numpy here costs the web process ~13 MB for nothing.
|
||||
loaded = _run("""
|
||||
import json, sys
|
||||
from src.common import path_safety, snapshot_policy, sync_manager
|
||||
from src.plugin_system import store_manager, schema_manager
|
||||
print(json.dumps("numpy" in sys.modules))
|
||||
""")
|
||||
assert loaded is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("package", ["src.common", "src.plugin_system"])
|
||||
def test_every_exported_name_resolves_to_its_home_object(package):
|
||||
pkg = importlib.import_module(package)
|
||||
assert sorted(pkg.__all__) == sorted(pkg._LAZY), "__all__ and _LAZY differ"
|
||||
for name in pkg.__all__:
|
||||
module_name, attr = pkg._LAZY[name]
|
||||
home = importlib.import_module(module_name)
|
||||
expected = home if attr is None else getattr(home, attr)
|
||||
assert getattr(pkg, name) is expected, name
|
||||
assert name in dir(pkg)
|
||||
|
||||
|
||||
def test_from_import_forms_plugins_use():
|
||||
# Every form found in ledmatrix-plugins and core: names, aliases,
|
||||
# submodules through the package, dotted submodule imports.
|
||||
from src.common import ScrollHelper, LogoHelper
|
||||
from src.common import scroll_config as _scroll_config
|
||||
from src.common import sports_card as _card
|
||||
from src.common import draw_fitted_text
|
||||
from src.plugin_system import BasePlugin, PluginManager
|
||||
from src.plugin_system import compatibility
|
||||
import src.common.scroll_helper
|
||||
import src.plugin_system.base_plugin
|
||||
import src.common
|
||||
import src.plugin_system
|
||||
|
||||
assert ScrollHelper is src.common.scroll_helper.ScrollHelper
|
||||
assert LogoHelper is src.common.LogoHelper
|
||||
assert _scroll_config is src.common.scroll_config
|
||||
assert _card is importlib.import_module("src.common.sports_card")
|
||||
assert draw_fitted_text is importlib.import_module("src.adaptive_layout").draw_fitted_text
|
||||
assert BasePlugin is src.plugin_system.base_plugin.BasePlugin
|
||||
assert PluginManager is importlib.import_module(
|
||||
"src.plugin_system.plugin_manager").PluginManager
|
||||
assert compatibility is importlib.import_module("src.plugin_system.compatibility")
|
||||
assert src.plugin_system.__version__ == "1.0.0"
|
||||
|
||||
|
||||
def test_star_import_still_binds_everything():
|
||||
namespace = {}
|
||||
exec("from src.common import *", namespace)
|
||||
import src.common
|
||||
assert set(src.common.__all__) <= set(namespace)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("package", ["src.common", "src.plugin_system"])
|
||||
def test_unknown_name_raises_attribute_error(package):
|
||||
pkg = importlib.import_module(package)
|
||||
with pytest.raises(AttributeError, match="no_such_name"):
|
||||
pkg.no_such_name # noqa: B018
|
||||
with pytest.raises(ImportError):
|
||||
exec(f"from {package} import no_such_name")
|
||||
Reference in New Issue
Block a user