diff --git a/src/features/__init__.py b/src/features/__init__.py index e2604f6..66a791b 100644 --- a/src/features/__init__.py +++ b/src/features/__init__.py @@ -3,30 +3,40 @@ Provides comprehensive feature extraction for market regime detection and position sizing algorithms. + +Public names are imported lazily (PEP 562). A broken dependency in one +module (for example base.py's data-layer import) therefore no longer +prevents importing unrelated, self-contained modules in this package +(greeks, pnl, position_models, ...). """ -from .base import FeatureEngineering -from .regime_features import ( - PriceStructureFeatures, - TrendIndicators, - MomentumIndicators, - VolatilityFeatures, - VolumeFeatures, - SupportResistanceFeatures, - MarketContextFeatures, - EventFeatures, - RegimeStateVector -) - -__all__ = [ - "FeatureEngineering", - "PriceStructureFeatures", - "TrendIndicators", - "MomentumIndicators", - "VolatilityFeatures", - "VolumeFeatures", - "SupportResistanceFeatures", - "MarketContextFeatures", - "EventFeatures", - "RegimeStateVector", -] \ No newline at end of file +import importlib + +# Map each public name to the submodule that defines it. +_EXPORTS = { + "FeatureEngineering": ".base", + "PriceStructureFeatures": ".regime_features", + "TrendIndicators": ".regime_features", + "MomentumIndicators": ".regime_features", + "VolatilityFeatures": ".regime_features", + "VolumeFeatures": ".regime_features", + "SupportResistanceFeatures": ".regime_features", + "MarketContextFeatures": ".regime_features", + "EventFeatures": ".regime_features", + "RegimeStateVector": ".regime_features", +} + +__all__ = list(_EXPORTS) + + +def __getattr__(name: str): + """Lazily import and return a public attribute (PEP 562).""" + module_name = _EXPORTS.get(name) + if module_name is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + module = importlib.import_module(module_name, __name__) + return getattr(module, name) + + +def __dir__(): + return sorted(__all__) diff --git a/tests/features/test_lazy_init.py b/tests/features/test_lazy_init.py new file mode 100644 index 0000000..8e994fd --- /dev/null +++ b/tests/features/test_lazy_init.py @@ -0,0 +1,40 @@ +""" +Regression tests for lazy feature-package imports (issue #10). + +The src.features package must not eagerly import its submodules, so a broken +dependency in one module (e.g. base.py's data-layer import) cannot take down +unrelated, self-contained modules in the package. +""" + +import importlib +import sys + +import pytest + + +def _purge_features(): + for name in list(sys.modules): + if name == "src.features" or name.startswith("src.features."): + del sys.modules[name] + + +def test_importing_package_does_not_eagerly_load_base(): + """Importing the package must not pull in base (its broken dependency).""" + _purge_features() + importlib.import_module("src.features") + assert "src.features.base" not in sys.modules + + +def test_clean_submodule_imports_without_base(): + """A self-contained submodule imports without requiring base.""" + _purge_features() + importlib.import_module("src.features.position_models") + assert "src.features.base" not in sys.modules + + +def test_unknown_attribute_raises(): + """Accessing an undefined public name still raises AttributeError.""" + _purge_features() + features = importlib.import_module("src.features") + with pytest.raises(AttributeError): + features.DefinitelyNotExported