A tool for managing dependencies in a modular python project by tracking which dependencies are needed by which sub-modules
—
Context manager and function for deferring ImportError exceptions until modules are actually used, preventing crashes from missing optional dependencies during import time.
Enables lazy import error handling that captures ImportError exceptions and defers them until the module is actually used, allowing imports of optional dependencies to succeed even when the dependency is not installed.
def lazy_import_errors(
*,
get_extras_modules: Optional[Callable[[], Set[str]]] = None,
make_error_message: Optional[Callable[[str], str]] = None,
):
"""
Enable lazy import errors.
When enabled, lazy import errors will capture imports that would otherwise
raise ImportErrors and defer those errors until the last possible moment
when the functionality is needed.
This function may be used either as a function directly or as a
contextmanager which will disable lazy errors upon exit.
Args:
get_extras_modules: Optional callable that fetches the list of module names
that are managed as extras using setup_tools.parse_requirements.
(Mutually exclusive with make_error_message)
make_error_message: Optional callable that takes the name of the module which
failed to import and returns an error message string.
(Mutually exclusive with get_extras_modules)
"""Enable lazy import errors globally for all subsequent imports:
from import_tracker import lazy_import_errors
# Enable globally - all failed imports will be deferred
lazy_import_errors()
# Now these imports won't crash even if modules are missing
import optional_module
from some_package import maybe_missing_submodule
# Error will be raised only when you try to use the module
try:
optional_module.some_function() # ModuleNotFoundError raised here
except ModuleNotFoundError as e:
print(f"Module not available: {e}")Use as a context manager to enable lazy errors only for specific imports:
from import_tracker import lazy_import_errors
# Required imports - will fail immediately if missing
import requests
import json
# Optional imports - errors deferred until usage
with lazy_import_errors():
import matplotlib.pyplot as plt
import seaborn as sns
from some_package import optional_feature
# These will work fine even if matplotlib/seaborn are not installed
print("Imports completed successfully")
# Error raised only when trying to use missing modules
try:
plt.plot([1, 2, 3]) # ModuleNotFoundError raised here if matplotlib missing
except ModuleNotFoundError:
print("Matplotlib not available, using basic plotting")Provide custom error messages for missing dependencies:
def custom_error_message(module_name: str) -> str:
return f"Missing optional dependency: {module_name}. Install with: pip install {module_name}"
with lazy_import_errors(make_error_message=custom_error_message):
import some_optional_module
# When used, will show custom message:
# ModuleNotFoundError: Missing optional dependency: some_optional_module. Install with: pip install some_optional_moduleIntegration with setuptools extras_require for helpful installation messages:
def get_extras_modules():
return {'my_package.plotting', 'my_package.advanced_features'}
with lazy_import_errors(get_extras_modules=get_extras_modules):
from my_package import plotting
# When used within an extras module, provides installation instructions:
# ModuleNotFoundError: No module named 'matplotlib'.
# To install the missing dependencies, run `pip install my_package[my_package.plotting]`from import_tracker import lazy_import_errors
# Enable lazy errors for optional dependencies
with lazy_import_errors():
try:
import pandas as pd
HAS_PANDAS = True
except ImportError:
HAS_PANDAS = False
def process_data(data):
if HAS_PANDAS:
return pd.DataFrame(data).describe()
else:
return {"error": "pandas not available"}Particularly useful for packages with hierarchical wild imports:
# In my_package/__init__.py
from import_tracker import lazy_import_errors
# Enable lazy errors globally for this package
lazy_import_errors()
# These won't crash the entire package if submodules have missing dependencies
from .core import *
from .utils import *
from .optional_features import * # Won't crash if dependencies missingLazy errors work with decorators from missing dependencies:
with lazy_import_errors():
from some_optional_package import optional_decorator
# This works even if some_optional_package is not installed
@optional_decorator
def my_function():
pass
# Error only raised when decorator functionality is actually invokedThe lazy import error system is implemented using several internal classes:
class _LazyErrorMetaFinder(importlib.abc.MetaPathFinder):
"""Meta path finder for intercepting import failures"""
def find_spec(self, fullname, path, *args, **kwargs):
"""Returns lazy loader spec for missing modules"""
class _LazyErrorLoader(importlib.abc.Loader):
"""Loader for creating lazy error modules"""
def create_module(self, spec):
"""Creates _LazyErrorModule instances"""
def exec_module(self, *_, **__):
"""No-op execution"""class _LazyErrorModule(ModuleType):
"""Module that defers ImportError until attribute access"""
def __getattr__(self, name: str) -> _LazyErrorAttr:
"""Returns lazy error attributes"""
class _LazyErrorAttr(type):
"""Lazy error object that raises on any meaningful usage"""
def __call__(self, *_, **__):
"""Handles decorator usage at import time"""
def __getattr__(self, name: str) -> "_LazyErrorAttr":
"""Recursive attribute access"""# Import time - no error raised
with lazy_import_errors():
import missing_module
# Usage time - error raised on meaningful operations
missing_module.function() # Raises ModuleNotFoundError
str(missing_module) # Raises ModuleNotFoundError
missing_module == something # Returns False (import time operation)
missing_module.attr.nested # Returns another lazy error objectWhen using extras integration, be aware of shell escaping requirements:
# In bash/sh
pip install my_package[my_package.feature]
# In zsh - requires escaping
pip install my_package\[my_package.feature\]from typing import Callable, Optional, Set
from contextlib import AbstractContextManager
# Function parameter types
GetExtrasModulesFunc = Callable[[], Set[str]]
MakeErrorMessageFunc = Callable[[str], str]
# Context manager type
LazyImportErrorContext = AbstractContextManager[None]Install with Tessl CLI
npx tessl i tessl/pypi-import-tracker