Files
LEDMatrix/src/common/__init__.py
T
ChuckandClaude Opus 5.5 ef69201770 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>
2026-10-03 14:38:27 -04:00

145 lines
5.3 KiB
Python

"""
Common utilities and helpers for LEDMatrix.
This package provides reusable functionality for plugins and core modules:
- API helpers
- Logo helpers
- Text/scroll helpers
- Adaptive layout and image helpers
The names below are imported on first use (PEP 562), not when the package is
imported. ``from src.common import ScrollHelper`` and
``src.common.ScrollHelper`` work as before and return the same objects, but
``import src.common`` -- or importing any submodule, such as
``src.common.path_safety`` -- no longer loads numpy, requests and freetype
along with every helper. The web interface imports src.common only for a few
small modules and never needs those.
"""
import importlib
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
if TYPE_CHECKING:
# What mypy and editors see: the real names and their types.
from src.common.api_helper import APIHelper
from src.common.scroll_helper import ScrollHelper
from src.common import scroll_config
from src.common.scroll_config import (
ScrollSettings,
configure as configure_scroll,
resolve as resolve_scroll_settings,
refresh_hz_from_config,
)
from src.common.logo_helper import LogoHelper
from src.common.text_helper import TextHelper
# Adaptive layout & images (canonical homes: src.adaptive_layout /
# src.adaptive_images — re-exported here so plugin authors find them in the
# blessed-helpers package). See docs/ADAPTIVE_LAYOUT.md.
from src.adaptive_layout import (
Region,
LayoutContext,
FontStep,
FontLadder,
LADDER_GRID,
LADDER_ARCADE,
FitResult,
draw_fitted_text,
ScoreboardRegions,
scoreboard_regions,
MediaRow,
media_row,
)
from src.adaptive_images import (
ImageFitResult,
fit_image,
draw_fitted_image,
RESAMPLE_LANCZOS,
RESAMPLE_NEAREST,
)
#: Exported name -> (module it lives in, attribute name there). An attribute
#: of None means the name is the module itself. Keep in step with the
#: TYPE_CHECKING imports above and with __all__.
_LAZY: Dict[str, Tuple[str, Optional[str]]] = {
'APIHelper': ('src.common.api_helper', 'APIHelper'),
'ScrollHelper': ('src.common.scroll_helper', 'ScrollHelper'),
'scroll_config': ('src.common.scroll_config', None),
'ScrollSettings': ('src.common.scroll_config', 'ScrollSettings'),
'configure_scroll': ('src.common.scroll_config', 'configure'),
'resolve_scroll_settings': ('src.common.scroll_config', 'resolve'),
'refresh_hz_from_config': ('src.common.scroll_config', 'refresh_hz_from_config'),
'LogoHelper': ('src.common.logo_helper', 'LogoHelper'),
'TextHelper': ('src.common.text_helper', 'TextHelper'),
# adaptive layout & images
'Region': ('src.adaptive_layout', 'Region'),
'LayoutContext': ('src.adaptive_layout', 'LayoutContext'),
'FontStep': ('src.adaptive_layout', 'FontStep'),
'FontLadder': ('src.adaptive_layout', 'FontLadder'),
'LADDER_GRID': ('src.adaptive_layout', 'LADDER_GRID'),
'LADDER_ARCADE': ('src.adaptive_layout', 'LADDER_ARCADE'),
'FitResult': ('src.adaptive_layout', 'FitResult'),
'draw_fitted_text': ('src.adaptive_layout', 'draw_fitted_text'),
'ScoreboardRegions': ('src.adaptive_layout', 'ScoreboardRegions'),
'scoreboard_regions': ('src.adaptive_layout', 'scoreboard_regions'),
'MediaRow': ('src.adaptive_layout', 'MediaRow'),
'media_row': ('src.adaptive_layout', 'media_row'),
'ImageFitResult': ('src.adaptive_images', 'ImageFitResult'),
'fit_image': ('src.adaptive_images', 'fit_image'),
'draw_fitted_image': ('src.adaptive_images', 'draw_fitted_image'),
'RESAMPLE_LANCZOS': ('src.adaptive_images', 'RESAMPLE_LANCZOS'),
'RESAMPLE_NEAREST': ('src.adaptive_images', 'RESAMPLE_NEAREST'),
}
__all__ = [
'APIHelper',
'ScrollHelper',
'scroll_config',
'ScrollSettings',
'configure_scroll',
'resolve_scroll_settings',
'refresh_hz_from_config',
'LogoHelper',
'TextHelper',
# adaptive layout & images
'Region',
'LayoutContext',
'FontStep',
'FontLadder',
'LADDER_GRID',
'LADDER_ARCADE',
'FitResult',
'draw_fitted_text',
'ScoreboardRegions',
'scoreboard_regions',
'MediaRow',
'media_row',
'ImageFitResult',
'fit_image',
'draw_fitted_image',
'RESAMPLE_LANCZOS',
'RESAMPLE_NEAREST',
]
def __getattr__(name: str) -> Any:
"""Import an exported name on first access (PEP 562).
Only called for names not already in the module namespace, so after the
first access the cached value below is returned directly. Unknown names
raise AttributeError, which ``from src.common import <submodule>`` relies
on to fall through to importing the submodule.
"""
try:
module_name, attr = _LAZY[name]
except KeyError:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
module = importlib.import_module(module_name) # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import -- module_name comes from the fixed _LAZY table
value = module if attr is None else getattr(module, attr)
globals()[name] = value
return value
def __dir__() -> List[str]:
return sorted(set(globals()) | set(__all__))