Architecture Decision Record (ADR)¶
Title¶
Use importlib
for Lazy Optional Dependency Loading with Rich Errors
Status¶
Accepted
Date¶
2025-10-14
Context¶
The Azure Key Vault provider depends on optional packages (azure-identity
, azure-keyvault-secrets
). Direct imports at module load time cause two problems:
- Importing
envresolve.providers.azure_kv
raisesModuleNotFoundError
in environments without the Azure SDK, even if Azure functionality is never used. - The resulting errors do not clearly instruct users which extras to install.
We need a pattern that defers importing optional dependencies until the provider is actually requested and that explains how to resolve missing dependencies.
Decision¶
Use importlib.import_module
inside EnvResolver.register_azure_kv_provider
to load the provider lazily and produce a detailed error message when dependencies are missing.
import importlib
def register_azure_kv_provider(self, **kwargs: Any) -> None:
try:
provider_module = importlib.import_module("envresolve.providers.azure_kv")
provider_class = provider_module.AzureKVProvider
except ImportError as exc: # Missing provider module or downstream deps
missing: list[str] = []
try:
importlib.util.find_spec("azure.identity")
except (ImportError, ModuleNotFoundError):
missing.append("azure-identity")
try:
importlib.util.find_spec("azure.keyvault.secrets")
except (ImportError, ModuleNotFoundError):
missing.append("azure-keyvault-secrets")
if missing:
hint = ", ".join(missing)
raise ProviderRegistrationError(
f"Azure Key Vault provider requires {hint}. "
"Install with `pip install envresolve[azure]`.",
original_error=exc
) from exc
raise ProviderRegistrationError(
"Failed to import Azure Key Vault provider; see chained exception for details.",
original_error=exc
) from exc
provider = provider_class(**kwargs)
self._providers["akv"] = provider
Rationale¶
- Lazy optional dependencies: Users without Azure extras can still import the package and use non-Azure features (aligns with ADR-0012).
- Actionable errors: When dependencies are missing, the raised message explicitly lists the missing packages and the extras command to install.
- Custom exception hierarchy: Uses
ProviderRegistrationError
instead ofImportError
to align with ADR-0002 (custom exception hierarchy). - Style compliance: Avoids Ruff's unused-import warnings and removes the need for
noqa
comments. - Extensibility: Same pattern can be reused for future optional providers.
Implications¶
Positive Implications¶
- Importing
envresolve
no longer requires the Azure SDK. - Developers receive clear instructions on how to enable Azure functionality.
- Custom exception allows clients to handle provider registration errors separately from other errors (catch
ProviderRegistrationError
specifically or all envresolve errors viaEnvResolveError
). - Unit tests can patch
importlib.import_module
to simulate missing dependencies.
Concerns¶
- Slightly more boilerplate: Lazy import logic is longer than a simple
try/except ImportError
. Mitigation: Isolated inside a helper method and well-documented. - Runtime detection cost: Uses
importlib.util.find_spec
to check dependencies. Mitigation: Called only when the provider is registered, not on every secret resolution.
Alternatives¶
Traditional try/except
around Direct Import¶
- Pros: Very concise.
- Cons: Cannot distinguish which dependency is missing; forces import at module load time.
- Rejection reason: Fails the usability goal of actionable diagnostics and prevents import without Azure SDK.
TYPE_CHECKING
Guards¶
- Pros: Keeps type checkers aware of symbols while skipping runtime import.
- Cons: Does not help when the provider is actually used at runtime; still fails once the guarded code executes.
- Rejection reason: Insufficient for runtime optionality.
Entry-Point Based Discovery¶
- Pros: Can lazily load providers via plugin mechanism.
- Cons: Overkill for first-party providers and shifts complexity to packaging.
- Rejection reason: Simpler explicit import is adequate.
Future Direction¶
- Extract repeated dependency-check logic into a reusable helper once additional providers are added.
- Log missing-dependency diagnostics at DEBUG level when running in verbose mode to aid support investigations.
- Combine with ADR-0012 to remove explicit
--ignore
flags once all optional modules load lazily.
References¶
- Implementation:
src/envresolve/api.py::EnvResolver.register_azure_kv_provider
- Related ADRs: 0002 (custom exception hierarchy), 0009 (provider registry), 0012 (pytest markers for optional providers)
- Python docs: https://docs.python.org/3/library/importlib.html
- Issue #5: Changed from
ImportError
toProviderRegistrationError
to align with ADR-0002