"""Step definitions for services __init__ lazy-import coverage (round 3). Exercises every lazy-loaded symbol declared in ``cleveragents.application.services._LAZY_IMPORTS`` so that the ``__getattr__`` code path (lines 561-569) is executed at runtime. Each scenario mocks ``importlib.import_module`` to avoid pulling in the full dependency graph and simply verifies that the lazy-loading machinery resolves the requested attribute name correctly. """ from __future__ import annotations import importlib import sys import types from unittest.mock import patch from behave import given, then, when # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _PKG = "cleveragents.application.services" def _get_fresh_module(): """Return the services __init__ module after purging cached lazy attrs. We reload the module so that every scenario exercises ``__getattr__`` afresh rather than hitting the ``globals()`` cache set by a previous lazy load. """ mod = sys.modules.get(_PKG) if mod is None: mod = importlib.import_module(_PKG) else: # Remove any previously-cached lazy symbols from module globals # so __getattr__ is triggered again. for attr_name in list(mod._LAZY_IMPORTS): mod.__dict__.pop(attr_name, None) return mod def _make_fake_submodule(attr_name: str) -> types.ModuleType: """Build a tiny fake module that carries ``attr_name`` as a sentinel.""" fake = types.ModuleType(f"fake_{attr_name}") setattr(fake, attr_name, type(attr_name, (), {})) return fake # --------------------------------------------------------------------------- # Given # --------------------------------------------------------------------------- @given("svcov3 a fresh services module") def step_svcov3_fresh_module(context): """Prepare a clean services module and set up the import_module mock.""" context.svcov3_mod = _get_fresh_module() context.svcov3_error = None context.svcov3_value = None # Patch importlib.import_module inside the services __init__ so that we # do not trigger heavy transitive imports. The patched version creates # lightweight fake modules on the fly. original_import_module = importlib.import_module def _patched_import_module(name, package=None): # Only intercept calls that originate from the lazy-loader. if package == _PKG and name.startswith("."): submodule_name = name.lstrip(".") full_name = f"{_PKG}.{submodule_name}" # If the real submodule is already in sys.modules use it - # this avoids breakage when earlier tests legitimately loaded # the submodule. if full_name in sys.modules: return sys.modules[full_name] # Otherwise try a real import; fall back to a fake module. try: return original_import_module(name, package) except Exception: # Build a fake module that provides every attr declared # in _LAZY_IMPORTS for this submodule. fake = types.ModuleType(full_name) lazy = context.svcov3_mod._LAZY_IMPORTS for _sym, (sub, attr) in lazy.items(): if sub == submodule_name: setattr(fake, attr, type(attr, (), {})) sys.modules[full_name] = fake return fake return original_import_module(name, package) patcher = patch("importlib.import_module", side_effect=_patched_import_module) patcher.start() context.add_cleanup(patcher.stop) # --------------------------------------------------------------------------- # When # --------------------------------------------------------------------------- @when('svcov3 I access lazy attribute "{attr_name}"') def step_svcov3_access_lazy(context, attr_name): """Trigger ``__getattr__`` for *attr_name* and store the result.""" try: # Remove from globals to force __getattr__ path context.svcov3_mod.__dict__.pop(attr_name, None) context.svcov3_value = getattr(context.svcov3_mod, attr_name) except Exception as exc: context.svcov3_error = exc @when('svcov3 I access lazy attribute "{attr_name}" again') def step_svcov3_access_lazy_again(context, attr_name): """Access the attribute a second time — should come from globals cache.""" try: context.svcov3_value = getattr(context.svcov3_mod, attr_name) except Exception as exc: context.svcov3_error = exc @when('svcov3 I access an unknown attribute "{attr_name}"') def step_svcov3_access_unknown(context, attr_name): """Try to access an attribute not in _LAZY_IMPORTS.""" try: getattr(context.svcov3_mod, attr_name) except AttributeError as exc: context.svcov3_error = exc except Exception as exc: context.svcov3_error = exc # --------------------------------------------------------------------------- # Then # --------------------------------------------------------------------------- @then('svcov3 the attribute "{attr_name}" should be resolved') def step_svcov3_resolved(context, attr_name): """Assert the lazy attribute was resolved without error.""" assert context.svcov3_error is None, ( f"Expected no error for {attr_name!r}, got: {context.svcov3_error}" ) assert context.svcov3_value is not None, ( f"Expected a non-None value for {attr_name!r}" ) @then('svcov3 an AttributeError should be stored for "{attr_name}"') def step_svcov3_attribute_error(context, attr_name): """Assert that an ``AttributeError`` was raised.""" assert isinstance(context.svcov3_error, AttributeError), ( f"Expected AttributeError, got {type(context.svcov3_error).__name__}: " f"{context.svcov3_error}" ) assert attr_name in str(context.svcov3_error) @then("svcov3 __all__ should equal sorted _LAZY_IMPORTS keys") def step_svcov3_all_matches(context): """Verify ``__all__`` is the sorted list of ``_LAZY_IMPORTS`` keys.""" mod = context.svcov3_mod expected = sorted(mod._LAZY_IMPORTS.keys()) assert mod.__all__ == expected, ( f"__all__ mismatch.\n" f" Missing from __all__: {set(expected) - set(mod.__all__)}\n" f" Extra in __all__: {set(mod.__all__) - set(expected)}" )