Skip to main content

A Python optional dependency manager.

Project description

optional-dependency-manager

Tests Lint codecov Python 3.10+

A Python library for managing optional dependencies with lazy loading and version validation.

Features

  • Lazy Loading: Dependencies are only imported when first accessed, reducing startup time
  • Version Validation: Automatically validates installed versions against specifiers
  • Decorator API: Simple decorator-based interface for classes and functions
  • Metadata Integration: Can read version specifiers from your package's pyproject.toml
  • Dependency Reporting: Generate reports of all optional dependencies and their status

Installation

pip install optional-dependency-manager

Quick Start

from optional_dependency_manager import OptionalDependencyManager

odm = OptionalDependencyManager()

@odm(modules={"numpy": {"specifiers": ">=1.20.0"}})
class DataProcessor:
    def process(self, data):
        np = self.modules["numpy"]
        return np.array(data).mean()

# numpy is only imported when accessing self.modules
processor = DataProcessor()
result = processor.process([1, 2, 3, 4, 5])

Usage

Basic Usage with Classes

from optional_dependency_manager import OptionalDependencyManager

odm = OptionalDependencyManager()

@odm(modules={
    "pandas": {"specifiers": ">=2.0.0"},
    "numpy": {"specifiers": ">=1.20.0", "alias": "np"},
})
class DataAnalyzer:
    def analyze(self, data):
        pd = self.modules["pandas"]
        np = self.modules["np"]  # using alias
        df = pd.DataFrame(data)
        return np.mean(df.values)

Basic Usage with Functions

@odm(modules={"requests": {"specifiers": ">=2.25.0"}})
def fetch_data(url, modules):
    # modules is injected as a keyword argument
    response = modules["requests"].get(url)
    return response.json()

Reading Specifiers from Package Metadata

If your package defines optional dependencies in pyproject.toml, you can read specifiers directly from there.

Using optional-dependencies (extras)

# pyproject.toml
[project.optional-dependencies]
ml = ["numpy>=1.20.0", "pandas>=2.0.0"]
odm = OptionalDependencyManager(source="my-package")

@odm(modules={
    "numpy": {"from_meta": True, "extra": "ml"},
    "pandas": {"from_meta": True, "extra": "ml"},
})
class MLModel:
    def train(self, data):
        np = self.modules["numpy"]
        pd = self.modules["pandas"]
        # ...

Using dependency-groups (PEP 735)

For packages using uv or other tools supporting PEP 735 dependency groups:

# pyproject.toml
[dependency-groups]
ml = ["numpy>=1.20.0", "pandas>=2.0.0"]
odm = OptionalDependencyManager(source="my-package")

@odm(modules={
    "numpy": {"from_meta": True, "group": "ml"},
    "pandas": {"from_meta": True, "group": "ml"},
})
class MLModel:
    def train(self, data):
        np = self.modules["numpy"]
        pd = self.modules["pandas"]
        # ...

To use dependency groups, install with the groups extra:

pip install optional-dependency-manager[groups]

Note: Dependency groups are only accessible during development (editable installs) since they are not included in package metadata.

Handling Import Errors

When a dependency is missing or has an incompatible version, an ImportError is raised with helpful information:

@odm(modules={"nonexistent": {"specifiers": ">=1.0.0"}})
class MyClass:
    pass

instance = MyClass()
instance.modules  # Raises ImportError
ImportError: Missing or incompatible dependencies:
  - nonexistent: not installed (requires >=1.0.0)

Generating Dependency Reports

You can generate a report of all registered optional dependencies:

odm = OptionalDependencyManager()

@odm(modules={"numpy": {"specifiers": ">=1.20.0"}})
class ClassA:
    pass

@odm(modules={"pandas": {"specifiers": ">=2.0.0"}})
class ClassB:
    pass

# Generate report (triggers loading to check versions)
reports = odm.report()

for r in reports:
    print(f"{r.module_name}: {r.status} (used by {r.used_by})")
    if r.installed_version:
        print(f"  Installed: {r.installed_version}")
    if r.specifier:
        print(f"  Required: {r.specifier}")

Module Specification Options

Option Type Description
specifiers str Version specifier (e.g., ">=1.0.0,<2.0.0")
alias str Alternative name for accessing the module
from_meta bool Read specifier from package metadata
extra str Extra name for [project.optional-dependencies]
group str Group name for [dependency-groups] (PEP 735)
distribution_name str PyPI package name if different from import name

Packages with Different Import Names

Some packages have different PyPI names and import names (e.g., scikit-learn vs sklearn):

@odm(modules={
    "sklearn": {
        "specifiers": ">=1.0.0",
        "distribution_name": "scikit-learn",
    }
})
class Classifier:
    def fit(self, X, y):
        sklearn = self.modules["sklearn"]
        return sklearn.linear_model.LogisticRegression().fit(X, y)

API Reference

OptionalDependencyManager

class OptionalDependencyManager:
    def __init__(self, source: str | None = None):
        """
        Initialize the manager.

        Args:
            source: Package name for reading metadata (optional)
        """

    def __call__(self, modules: dict[str, dict[str, str]]):
        """Decorator for classes or functions."""

    def report(self) -> list[ModuleReport]:
        """Generate a report of all registered dependencies."""

ModuleReport

@dataclass
class ModuleReport:
    module_name: str
    specifier: str | None
    extra: str | None
    group: str | None
    installed_version: str | None
    status: Literal["satisfied", "missing", "version_mismatch"]
    used_by: str

Development

# Install dev dependencies
uv sync --group dev

# Run tests
uv run pytest

# Run tests with coverage
uv run pytest --cov=optional_dependency_manager

# Type checking
uv run mypy src/

# Linting
uv run ruff check .

# Formatting
uv run ruff format .

License

MIT License - see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

optional_dependency_manager-0.1.0.tar.gz (9.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

optional_dependency_manager-0.1.0-py3-none-any.whl (10.4 kB view details)

Uploaded Python 3

File details

Details for the file optional_dependency_manager-0.1.0.tar.gz.

File metadata

File hashes

Hashes for optional_dependency_manager-0.1.0.tar.gz
Algorithm Hash digest
SHA256 66014b0e5bd539f8032d4770d6ef11d2e7df0b667bb831321d33bb6a984aa2d4
MD5 3be74f678173dd2e9a6adf6727ed5c14
BLAKE2b-256 96492b0190c78e8ad1ad10267a0fb6bf61ed3d648eada341772e0f7fbc330ea1

See more details on using hashes here.

Provenance

The following attestation bundles were made for optional_dependency_manager-0.1.0.tar.gz:

Publisher: release.yaml on forge-labs-dev/optional-dependency-manager

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file optional_dependency_manager-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for optional_dependency_manager-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4c243cac61a91cd0983a05f0953e79637d35563b4db04ca0e4815b88924f9fee
MD5 feb23d56598d730ade6b3fb11fb8a1a8
BLAKE2b-256 209ef911bcfa34f531544165748541e0e5739e570243e23c9194dabfa0ff58c8

See more details on using hashes here.

Provenance

The following attestation bundles were made for optional_dependency_manager-0.1.0-py3-none-any.whl:

Publisher: release.yaml on forge-labs-dev/optional-dependency-manager

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page