{"id":40631508,"url":"https://github.com/mikelane/dioxide","last_synced_at":"2026-01-27T07:10:10.479Z","repository":{"id":320099772,"uuid":"1080787961","full_name":"mikelane/dioxide","owner":"mikelane","description":"Opinionated, Zero-ceremony, Rust-backed dependency injection for Python","archived":false,"fork":false,"pushed_at":"2026-01-21T15:09:23.000Z","size":2322,"stargazers_count":8,"open_issues_count":20,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-21T17:39:45.719Z","etag":null,"topics":["clean-architecture","clean-code","dependency-injection","dependency-inversion-principle","hexagonal-architecture","python","python311","python312","python313","python314","software-engineering"],"latest_commit_sha":null,"homepage":"https://dioxide.readthedocs.io","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mikelane.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":"ROADMAP_OLD.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"mikelane"}},"created_at":"2025-10-21T21:42:00.000Z","updated_at":"2026-01-21T05:29:09.000Z","dependencies_parsed_at":"2025-10-28T07:12:55.849Z","dependency_job_id":null,"html_url":"https://github.com/mikelane/dioxide","commit_stats":null,"previous_names":["mikelane/rivet-di","mikelane/dioxide"],"tags_count":31,"template":false,"template_full_name":null,"purl":"pkg:github/mikelane/dioxide","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikelane%2Fdioxide","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikelane%2Fdioxide/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikelane%2Fdioxide/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikelane%2Fdioxide/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mikelane","download_url":"https://codeload.github.com/mikelane/dioxide/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikelane%2Fdioxide/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28807430,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-27T06:25:51.065Z","status":"ssl_error","status_checked_at":"2026-01-27T06:25:50.640Z","response_time":168,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clean-architecture","clean-code","dependency-injection","dependency-inversion-principle","hexagonal-architecture","python","python311","python312","python313","python314","software-engineering"],"created_at":"2026-01-21T07:36:47.588Z","updated_at":"2026-01-27T07:10:10.451Z","avatar_url":"https://github.com/mikelane.png","language":"Python","funding_links":["https://github.com/sponsors/mikelane"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cpicture\u003e\n    \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/mikelane/dioxide/main/docs/_static/images/dioxide-logo-light.png\"\u003e\n    \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/mikelane/dioxide/main/docs/_static/images/dioxide-logo-dark.png\"\u003e\n    \u003cimg alt=\"dioxide logo\" src=\"https://raw.githubusercontent.com/mikelane/dioxide/main/docs/_static/images/dioxide-logo-dark.png\" width=\"200\"\u003e\n  \u003c/picture\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eDI that makes hexagonal architecture feel inevitable\u003c/strong\u003e\n\u003c/p\u003e\n\n[![CI](https://github.com/mikelane/dioxide/workflows/CI/badge.svg)](https://github.com/mikelane/dioxide/actions)\n[![Release](https://github.com/mikelane/dioxide/actions/workflows/release-automated.yml/badge.svg)](https://github.com/mikelane/dioxide/actions/workflows/release-automated.yml)\n[![PyPI version](https://badge.fury.io/py/dioxide.svg)](https://pypi.org/project/dioxide/)\n[![Documentation](https://readthedocs.org/projects/dioxide/badge/?version=latest)](https://dioxide.readthedocs.io/en/latest/?badge=latest)\n[![Python Versions](https://img.shields.io/pypi/pyversions/dioxide.svg)](https://pypi.org/project/dioxide/)\n[![Platform Support](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-blue)](https://github.com/mikelane/dioxide)\n[![Architecture](https://img.shields.io/badge/arch-x86__64%20%7C%20aarch64-green)](https://github.com/mikelane/dioxide)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Sponsor](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?logo=github)](https://github.com/sponsors/mikelane)\n\n\u003e **[📖 Read the Documentation](https://dioxide.readthedocs.io)** | **[🚀 Quick Start](#quick-start)** | **[💡 Examples](https://dioxide.readthedocs.io/en/latest/examples/)** | **[📋 API Reference](https://dioxide.readthedocs.io/en/latest/api/dioxide/)**\n\n---\n\n## Overview\n\n`dioxide` is a dependency injection framework for Python designed to make clean architecture the path of least resistance:\n\n- **Zero-ceremony API** - Just type hints and decorators, no XML or configuration files\n- **Built-in Profile system** - Swap PostgreSQL for in-memory with `profile=Profile.TEST`\n- **Hexagonal architecture made easy** - `@adapter.for_(Port)` and `@service` guide you to clean code\n- **Type safety** - Full mypy and pyright support; if it type-checks, it wires correctly\n- **Rust-backed** - Fast container operations via PyO3 for competitive runtime performance\n\n## Installation\n\n```bash\npip install dioxide\n```\n\n### Platform Support\n\n| Platform | x86_64 | ARM64/aarch64 |\n|----------|--------|---------------|\n| Linux    | ✅     | ✅            |\n| macOS    | ✅     | ✅ (M1/M2/M3) |\n| Windows  | ✅     | ❌            |\n\n**Python Versions**: 3.11, 3.12, 3.13, 3.14\n\n## Status\n\n**STABLE**: dioxide v1.0.0 is production-ready with a stable, frozen API.\n\n- **Latest Release**: [v1.0.0](https://github.com/mikelane/dioxide/releases/tag/v1.0.0) - Published to PyPI\n- **API Status**: Frozen - No breaking changes until v2.0\n- **Production Ready**: All MLP features complete, comprehensive test coverage, battle-tested\n\n**Migrating from alpha/beta versions?** See [MIGRATION.md](MIGRATION.md) for the complete migration guide.\n\nSee [Issues](https://github.com/mikelane/dioxide/issues) for current work and planned enhancements.\n\n## Why dioxide?\n\n**Make the Dependency Inversion Principle feel inevitable.**\n\ndioxide exists to make clean architecture (ports-and-adapters) the path of least resistance:\n\n| What you get | How dioxide helps |\n|--------------|-------------------|\n| **Testable code** | Profile system swaps real adapters for fakes with one line |\n| **Type-safe wiring** | If mypy passes, your dependencies are correct |\n| **Clean boundaries** | `@adapter.for_(Port)` makes seams explicit and visible |\n| **Fast tests** | In-memory fakes, not slow mocks or external services |\n| **Competitive performance** | Rust-backed container with sub-microsecond resolution |\n\nSee [MLP_VISION.md](docs/MLP_VISION.md) for the complete design philosophy and [TESTING_GUIDE.md](docs/TESTING_GUIDE.md) for testing patterns.\n\n## Anti-goals\n\ndioxide is intentionally focused. Here's what we're **not** building:\n\n- **Not a framework** - dioxide is a library; it doesn't control your application structure\n- **Not runtime reflection magic** - Everything is explicit via decorators and type hints\n- **Not a general-purpose container** - We optimize for hexagonal architecture patterns\n- **Not configuration management** - Use Pydantic Settings or python-decouple for that\n- **Not trying to solve every DI pattern** - Constructor injection only, no property/method injection\n\nThis focus keeps dioxide simple and predictable. See [MLP_VISION.md](docs/MLP_VISION.md) for the complete design philosophy.\n\n## Quick Start\n\ndioxide embraces **hexagonal architecture** (ports-and-adapters) to make clean, testable code the path of least resistance.\n\n```python\nfrom typing import Protocol\nfrom dioxide import Container, Profile, adapter, service\n\n# 1. Define port (interface) - your seam\nclass EmailPort(Protocol):\n    async def send(self, to: str, subject: str, body: str) -\u003e None: ...\n\n# 2. Create adapters (implementations) for different environments\n@adapter.for_(EmailPort, profile=Profile.PRODUCTION)\nclass SendGridAdapter:\n    async def send(self, to: str, subject: str, body: str) -\u003e None:\n        # Real SendGrid API calls\n        print(f\"📧 Sending via SendGrid to {to}: {subject}\")\n\n@adapter.for_(EmailPort, profile=Profile.TEST)\nclass FakeEmailAdapter:\n    def __init__(self):\n        self.sent_emails = []\n\n    async def send(self, to: str, subject: str, body: str) -\u003e None:\n        self.sent_emails.append({\"to\": to, \"subject\": subject, \"body\": body})\n        print(f\"✅ Fake email sent to {to}\")\n\n# 3. Create service (core business logic) - depends on port, not adapter\n@service\nclass UserService:\n    def __init__(self, email: EmailPort):\n        self.email = email\n\n    async def register_user(self, email_addr: str, name: str):\n        # Core logic doesn't know/care which adapter is active\n        await self.email.send(\n            to=email_addr,\n            subject=\"Welcome!\",\n            body=f\"Hello {name}, thanks for signing up!\"\n        )\n\n# Production usage\ncontainer = Container()\ncontainer.scan(profile=Profile.PRODUCTION)\nuser_service = container.resolve(UserService)\nawait user_service.register_user(\"user@example.com\", \"Alice\")\n# 📧 Sends real email via SendGrid\n\n# Testing - just change the profile!\ntest_container = Container()\ntest_container.scan(profile=Profile.TEST)\ntest_service = test_container.resolve(UserService)\nawait test_service.register_user(\"test@example.com\", \"Bob\")\n\n# Verify in tests (no mocks!)\nfake_email = test_container.resolve(EmailPort)\nassert len(fake_email.sent_emails) == 1\nassert fake_email.sent_emails[0][\"to\"] == \"test@example.com\"\n```\n\n**Why this is powerful**:\n- ✅ **Type-safe**: If mypy passes, your wiring is correct\n- ✅ **Testable**: Fast fakes at the seams, not mocks\n- ✅ **Clean**: Business logic has zero knowledge of infrastructure\n- ✅ **Simple**: One line change to swap implementations (`profile=...`)\n- ✅ **Explicit**: Port definitions make boundaries visible\n\n**Key concepts**:\n- **Ports** (`Protocol`): Define what operations you need (the seam)\n- **Adapters** (`@adapter.for_(Port, profile=...)`): Concrete implementations\n- **Services** (`@service`): Core business logic that depends on ports\n- **Profiles** (`Profile.PRODUCTION`, `Profile.TEST`): Environment selection\n- **Container**: Auto-wires dependencies based on type hints\n\n## Constructor Dependency Injection\n\nWhen you create an adapter or service with constructor parameters, dioxide **automatically injects dependencies** based on type hints. This is the core mechanism that makes dependency injection \"just work\".\n\n### How It Works\n\nWhen you write:\n\n```python\n@adapter.for_(UserRepository, profile=Profile.PRODUCTION)\nclass SqliteUserRepository:\n    def __init__(self, db: Connection) -\u003e None:  # \u003c-- type hint tells dioxide what to inject\n        self.db = db\n```\n\nYou might ask: \"How does dioxide know where to get `db`?\"\n\nThe answer: **The dependency must be registered in the container.**\n\ndioxide:\n1. Reads the type hints from your constructor (`db: Connection`)\n2. Looks up `Connection` in the container registry\n3. Resolves `Connection` (creating an instance if needed)\n4. Passes it to your constructor\n\n### The Dependency Must Be Registered\n\nConstructor dependencies **must** be registered in the container before dioxide can inject them. There are three ways to register a dependency:\n\n#### Option 1: Make it an Adapter for a Port\n\nThe most common pattern - create a port and register an adapter:\n\n```python\nfrom typing import Protocol\nfrom dioxide import Container, Profile, adapter, service\n\n# Define a port (interface) for database connections\nclass DatabaseConnection(Protocol):\n    def execute(self, sql: str) -\u003e Any: ...\n\n# Register an adapter for the port\n@adapter.for_(DatabaseConnection, profile=Profile.PRODUCTION)\nclass SqliteConnection:\n    def __init__(self) -\u003e None:\n        import sqlite3\n        self.conn = sqlite3.connect(\"app.db\")\n\n    def execute(self, sql: str) -\u003e Any:\n        return self.conn.execute(sql)\n\n# Now other adapters can depend on DatabaseConnection\n@adapter.for_(UserRepository, profile=Profile.PRODUCTION)\nclass SqliteUserRepository:\n    def __init__(self, db: DatabaseConnection) -\u003e None:  # Auto-injected!\n        self.db = db\n\n    async def get_user(self, user_id: str) -\u003e dict | None:\n        result = self.db.execute(f\"SELECT * FROM users WHERE id = ?\", (user_id,))\n        # ...\n```\n\n#### Option 2: Make it a Service\n\nIf the dependency is core domain logic (not infrastructure), use `@service`:\n\n```python\nfrom dioxide import service\n\n@service\nclass AppConfig:\n    \"\"\"Configuration loaded from environment.\"\"\"\n    def __init__(self) -\u003e None:\n        import os\n        self.database_url = os.getenv(\"DATABASE_URL\", \"sqlite:///dev.db\")\n        self.sendgrid_api_key = os.getenv(\"SENDGRID_API_KEY\", \"\")\n\n# Adapters can depend on services\n@adapter.for_(EmailPort, profile=Profile.PRODUCTION)\nclass SendGridAdapter:\n    def __init__(self, config: AppConfig) -\u003e None:  # Auto-injected!\n        self.api_key = config.sendgrid_api_key\n```\n\n#### Option 3: Manual Registration\n\nFor external dependencies or special cases, register manually:\n\n```python\nimport sqlite3\nfrom dioxide import Container, Profile\n\ncontainer = Container()\n\n# Register a factory that creates the dependency\ncontainer.register_singleton(sqlite3.Connection, lambda: sqlite3.connect(\"app.db\"))\n\n# Now scan - adapters depending on sqlite3.Connection will get it injected\ncontainer.scan(profile=Profile.PRODUCTION)\n```\n\n### Complete Example: Adapters with Dependencies\n\nHere's a complete example showing adapters that depend on other components:\n\n```python\nfrom typing import Protocol\nfrom dioxide import Container, Profile, adapter, service\n\n# --- Ports (interfaces) ---\n\nclass ConfigPort(Protocol):\n    \"\"\"Port for configuration access.\"\"\"\n    def get(self, key: str, default: str = \"\") -\u003e str:\n        \"\"\"Get configuration value by key.\"\"\"\n        ...\n\nclass EmailPort(Protocol):\n    async def send(self, to: str, subject: str, body: str) -\u003e None: ...\n\nclass UserRepository(Protocol):\n    async def save(self, user: dict) -\u003e dict: ...\n    async def get(self, user_id: str) -\u003e dict | None: ...\n\n# --- Configuration adapter ---\n\n@adapter.for_(ConfigPort, profile=Profile.PRODUCTION)\nclass EnvConfigAdapter:\n    \"\"\"Configuration from environment variables.\"\"\"\n    def get(self, key: str, default: str = \"\") -\u003e str:\n        import os\n        return os.environ.get(key, default)\n\n@adapter.for_(ConfigPort, profile=Profile.TEST)\nclass FakeConfigAdapter:\n    \"\"\"Fake configuration for testing.\"\"\"\n    def __init__(self) -\u003e None:\n        self.values = {\"SENDGRID_API_KEY\": \"test-key\", \"DATABASE_URL\": \":memory:\"}\n\n    def get(self, key: str, default: str = \"\") -\u003e str:\n        return self.values.get(key, default)\n\n# --- Email adapter (depends on ConfigPort) ---\n\n@adapter.for_(EmailPort, profile=Profile.PRODUCTION)\nclass SendGridAdapter:\n    def __init__(self, config: ConfigPort) -\u003e None:  # ConfigPort auto-injected!\n        self.api_key = config.get(\"SENDGRID_API_KEY\")\n\n    async def send(self, to: str, subject: str, body: str) -\u003e None:\n        # Use self.api_key to call SendGrid API\n        print(f\"Sending via SendGrid to {to}: {subject}\")\n\n@adapter.for_(EmailPort, profile=Profile.TEST)\nclass FakeEmailAdapter:\n    def __init__(self) -\u003e None:  # No dependencies needed for test fake\n        self.sent_emails: list[dict] = []\n\n    async def send(self, to: str, subject: str, body: str) -\u003e None:\n        self.sent_emails.append({\"to\": to, \"subject\": subject, \"body\": body})\n\n# --- Service (depends on ports) ---\n\n@service\nclass UserService:\n    def __init__(self, repo: UserRepository, email: EmailPort) -\u003e None:\n        self.repo = repo\n        self.email = email\n\n    async def register_user(self, name: str, email_addr: str) -\u003e dict:\n        user = await self.repo.save({\"name\": name, \"email\": email_addr})\n        await self.email.send(email_addr, \"Welcome!\", f\"Hello {name}!\")\n        return user\n\n# --- Usage ---\n\ncontainer = Container()\ncontainer.scan(profile=Profile.PRODUCTION)\n\n# When UserService is resolved:\n# 1. dioxide sees UserService needs UserRepository and EmailPort\n# 2. Resolves UserRepository -\u003e gets SqliteUserRepository (if registered)\n# 3. Resolves EmailPort -\u003e gets SendGridAdapter\n# 4. SendGridAdapter needs ConfigPort -\u003e gets EnvConfigAdapter\n# 5. Everything is wired up automatically!\n\nservice = container.resolve(UserService)\n```\n\n### Key Points\n\n1. **Type hints are required**: Constructor parameters must have type hints for injection to work\n2. **Dependencies must be registered**: Either via `@adapter.for_()`, `@service`, or manual registration\n3. **Resolution is recursive**: If your dependency has dependencies, those are resolved too\n4. **Circular dependencies are detected**: dioxide fails fast if A depends on B and B depends on A\n5. **Test fakes often have no dependencies**: Fakes are typically simpler and don't need injection\n\n### Common Patterns\n\n**Config Adapter Pattern**: Create a `ConfigPort` that adapters depend on for configuration:\n\n```python\n@adapter.for_(EmailPort, profile=Profile.PRODUCTION)\nclass SendGridAdapter:\n    def __init__(self, config: ConfigPort) -\u003e None:\n        self.api_key = config.get(\"SENDGRID_API_KEY\")\n```\n\n**Database Connection Pattern**: Wrap database connections in a port for injection:\n\n```python\n@adapter.for_(DatabasePort, profile=Profile.PRODUCTION)\nclass PostgresAdapter:\n    def __init__(self, config: ConfigPort) -\u003e None:\n        self.url = config.get(\"DATABASE_URL\")\n```\n\n**Test Fakes Without Dependencies**: Test adapters are often simple and don't need dependencies:\n\n```python\n@adapter.for_(EmailPort, profile=Profile.TEST)\nclass FakeEmailAdapter:\n    def __init__(self) -\u003e None:  # No dependencies - just in-memory storage\n        self.sent_emails = []\n```\n\n## Lifecycle Management\n\nServices and adapters can opt into lifecycle management using the `@lifecycle` decorator for components that need initialization and cleanup:\n\n```python\nfrom dioxide import Container, Profile, service, lifecycle, adapter\nfrom typing import Protocol\n\n# Port for cache operations\nclass CachePort(Protocol):\n    async def get(self, key: str) -\u003e str | None: ...\n    async def set(self, key: str, value: str) -\u003e None: ...\n\n# Production adapter with lifecycle\n@adapter.for_(CachePort, profile=Profile.PRODUCTION)\n@lifecycle\nclass RedisAdapter:\n    \"\"\"Redis cache with connection lifecycle.\"\"\"\n\n    def __init__(self, config: AppConfig):\n        self.config = config\n        self.redis = None\n\n    async def initialize(self) -\u003e None:\n        \"\"\"Called automatically when container starts.\"\"\"\n        self.redis = await aioredis.create_redis_pool(self.config.redis_url)\n        print(f\"Redis connected: {self.config.redis_url}\")\n\n    async def dispose(self) -\u003e None:\n        \"\"\"Called automatically when container stops.\"\"\"\n        if self.redis:\n            self.redis.close()\n            await self.redis.wait_closed()\n            print(\"Redis connection closed\")\n\n    async def get(self, key: str) -\u003e str | None:\n        return await self.redis.get(key)\n\n    async def set(self, key: str, value: str) -\u003e None:\n        await self.redis.set(key, value)\n\n# Service with lifecycle\n@service\n@lifecycle\nclass Database:\n    \"\"\"Database service with connection lifecycle.\"\"\"\n\n    def __init__(self, config: AppConfig):\n        self.config = config\n        self.engine = None\n\n    async def initialize(self) -\u003e None:\n        \"\"\"Called automatically when container starts.\"\"\"\n        self.engine = create_async_engine(self.config.database_url)\n        print(f\"Database connected: {self.config.database_url}\")\n\n    async def dispose(self) -\u003e None:\n        \"\"\"Called automatically when container stops.\"\"\"\n        if self.engine:\n            await self.engine.dispose()\n            print(\"Database connection closed\")\n\n# Use async context manager for automatic lifecycle\ncontainer = Container()\ncontainer.scan(profile=Profile.PRODUCTION)\n\nasync with container:\n    # All @lifecycle components initialized here (in dependency order)\n    app = container.resolve(Application)\n    await app.run()\n# All @lifecycle components disposed here (in reverse order)\n\n# Or manually control lifecycle\nawait container.start()  # Initialize all @lifecycle components\ntry:\n    app = container.resolve(Application)\n    await app.run()\nfinally:\n    await container.stop()  # Dispose all @lifecycle components\n```\n\n**Why `@lifecycle`?**\n- ✅ **Optional**: Only components that need it use lifecycle (test fakes typically don't!)\n- ✅ **Validated**: Decorator ensures `initialize()` and `dispose()` methods exist and are async\n- ✅ **Consistent**: Matches dioxide's decorator-based API (`@service`, `@adapter.for_()`)\n- ✅ **Type-safe**: Type stubs provide IDE autocomplete and mypy validation\n- ✅ **Dependency-ordered**: Components initialized/disposed in correct dependency order\n\n**Key Features**:\n- **Async context manager**: `async with container:` handles start/stop automatically\n- **Manual control**: `await container.start()` and `await container.stop()` for explicit lifecycle\n- **Dependency ordering**: Initialization happens in dependency order (dependencies first)\n- **Reverse disposal**: Disposal happens in reverse order (dependents disposed before dependencies)\n- **Graceful rollback**: If initialization fails, already-initialized components are disposed\n- **Error resilience**: Disposal continues even if individual components fail\n\n**Status**: Fully implemented.\n\n## Multi-Binding (Collection Injection)\n\nFor plugin systems where multiple implementations should be collected rather than selecting one, dioxide supports **multi-binding**:\n\n```python\nfrom typing import Protocol\nfrom dioxide import Container, Profile, adapter, service\n\n# Define a port for plugins\nclass PluginPort(Protocol):\n    def process(self, data: str) -\u003e str: ...\n\n# Register MULTIPLE adapters with multi=True\n@adapter.for_(PluginPort, multi=True, priority=10)\nclass ValidationPlugin:\n    def process(self, data: str) -\u003e str:\n        # Validate data\n        return data\n\n@adapter.for_(PluginPort, multi=True, priority=20)\nclass TransformPlugin:\n    def process(self, data: str) -\u003e str:\n        # Transform data\n        return data.upper()\n\n@adapter.for_(PluginPort, multi=True, priority=30)\nclass LoggingPlugin:\n    def process(self, data: str) -\u003e str:\n        print(f\"Processing: {data}\")\n        return data\n\n# Inject ALL plugins as a list\n@service\nclass DataProcessor:\n    def __init__(self, plugins: list[PluginPort]) -\u003e None:\n        self.plugins = plugins  # All plugins, ordered by priority\n\n    def run(self, data: str) -\u003e str:\n        for plugin in self.plugins:\n            data = plugin.process(data)\n        return data\n\n# Usage\ncontainer = Container()\ncontainer.scan(profile=Profile.PRODUCTION)\n\nprocessor = container.resolve(DataProcessor)\n# processor.plugins == [ValidationPlugin, TransformPlugin, LoggingPlugin]\n# Ordered by priority (lowest first)\n\nresult = processor.run(\"hello\")\n# Processes through all plugins in priority order\n```\n\n**Key features:**\n- **`multi=True`**: Enables multi-binding mode (default is `False` for single adapter)\n- **`priority=N`**: Controls ordering (lower values first, default is 0)\n- **`list[Port]`**: Type hint tells container to inject all multi-bindings\n- **Profile filtering**: Only adapters matching the active profile are included\n- **Empty list OK**: Returns empty list if no implementations (valid for optional plugins)\n\n**Constraints:**\n- A port must be either single-binding OR multi-binding, not both\n- Error at startup if same port has both `multi=True` and `multi=False` adapters\n\n**Use cases:**\n- Plugin systems (mutation operators, validators, transformers)\n- Pipeline stages that run in sequence\n- Event handlers that all process the same event\n- Middleware chains\n\n## Function Injection\n\ndioxide works with **any callable**, not just classes. You can inject dependencies into standalone functions, route handlers, and background tasks by using default parameters with `container.resolve()`:\n\n### Standalone Functions\n\n```python\nfrom dioxide import Container, Profile, adapter, service\nfrom typing import Protocol\n\n# Define port\nclass EmailPort(Protocol):\n    async def send(self, to: str, subject: str, body: str) -\u003e None: ...\n\n# Set up container\ncontainer = Container()\ncontainer.scan(profile=Profile.PRODUCTION)\n\n# Standalone function with injected dependencies\nasync def send_welcome_email(\n    user_email: str,\n    user_name: str,\n    email: EmailPort = container.resolve(EmailPort)\n) -\u003e None:\n    \"\"\"Send welcome email using injected email service.\"\"\"\n    await email.send(\n        to=user_email,\n        subject=\"Welcome!\",\n        body=f\"Thanks for joining, {user_name}!\"\n    )\n\n# Call like a normal function\nawait send_welcome_email(\"alice@example.com\", \"Alice\")\n# EmailPort dependency injected automatically\n```\n\n### Route Handlers (Web Frameworks)\n\nPerfect for FastAPI, Flask, or any web framework:\n\n```python\nfrom fastapi import FastAPI, Request\nfrom dioxide import Container, Profile\n\napp = FastAPI()\ncontainer = Container()\ncontainer.scan(profile=Profile.PRODUCTION)\n\n@app.post(\"/users\")\nasync def create_user(\n    request: Request,\n    db: DatabasePort = container.resolve(DatabasePort),\n    email: EmailPort = container.resolve(EmailPort)\n) -\u003e dict:\n    \"\"\"Create user with injected database and email services.\"\"\"\n    # Parse request\n    user_data = await request.json()\n\n    # Use injected dependencies\n    user = await db.save_user(user_data)\n    await email.send(\n        to=user_data[\"email\"],\n        subject=\"Welcome!\",\n        body=f\"Hello {user_data['name']}!\"\n    )\n\n    return {\"id\": user.id, \"status\": \"created\"}\n```\n\n### Background Tasks\n\nGreat for Celery, RQ, or any background job system:\n\n```python\nfrom dioxide import Container, Profile\nfrom typing import Protocol\n\n# Define ports\nclass PaymentPort(Protocol):\n    async def charge(self, invoice_id: str) -\u003e dict: ...\n\nclass InvoiceEmailPort(Protocol):\n    \"\"\"Port for sending invoice-related emails.\"\"\"\n    async def send_receipt(self, email: str, invoice: dict) -\u003e None: ...\n\nclass LoggerPort(Protocol):\n    \"\"\"Port for logging.\"\"\"\n    def info(self, msg: str) -\u003e None: ...\n    def error(self, msg: str) -\u003e None: ...\n\n# Set up container\ncontainer = Container()\ncontainer.scan(profile=Profile.PRODUCTION)\n\n# Background task with injected dependencies\nasync def process_invoice(\n    invoice_id: str,\n    payment: PaymentPort = container.resolve(PaymentPort),\n    email: InvoiceEmailPort = container.resolve(InvoiceEmailPort),\n    logger: LoggerPort = container.resolve(LoggerPort)\n) -\u003e None:\n    \"\"\"Process invoice payment and send receipt.\"\"\"\n    try:\n        # Charge payment\n        result = await payment.charge(invoice_id)\n\n        # Send receipt\n        await email.send_receipt(result[\"customer_email\"], result)\n\n        # Log success\n        logger.info(f\"Invoice {invoice_id} processed successfully\")\n\n    except Exception as e:\n        logger.error(f\"Failed to process invoice {invoice_id}: {e}\")\n        raise\n\n# Schedule the task (example with Celery)\n@celery_app.task\ndef process_invoice_task(invoice_id: str):\n    \"\"\"Celery task wrapper.\"\"\"\n    import asyncio\n    return asyncio.run(process_invoice(invoice_id))\n```\n\n### Testing Functions with Injection\n\nFunction injection works seamlessly with the profile system for testing:\n\n```python\nimport pytest\nfrom dioxide import Container, Profile\n\n@pytest.fixture\ndef test_container():\n    \"\"\"Container with test profile.\"\"\"\n    container = Container()\n    container.scan(profile=Profile.TEST)\n    return container\n\nasync def test_send_welcome_email(test_container):\n    \"\"\"Test function injection with fake email adapter.\"\"\"\n    # Function uses test profile automatically\n    await send_welcome_email(\"test@example.com\", \"TestUser\")\n\n    # Verify with fake adapter\n    fake_email = test_container.resolve(EmailPort)\n    assert len(fake_email.sent_emails) == 1\n    assert fake_email.sent_emails[0][\"to\"] == \"test@example.com\"\n```\n\n**Why function injection?**\n- ✅ **Flexible**: Works with any callable (classes, functions, lambdas)\n- ✅ **Practical**: Perfect for route handlers, background jobs, utility functions\n- ✅ **Testable**: Same profile system works for function injection\n- ✅ **No magic**: Just default parameters with `container.resolve()`\n- ✅ **Type-safe**: Full mypy support for injected types\n\n### A Note on This Pattern\n\n\u003e **Transparency**: The `container.resolve()` default-arg pattern is service-locator-adjacent. We intentionally support it for zero-ceremony use cases.\n\n**When this pattern shines:**\n- CLI commands with Click integration\n- Simple scripts and small applications\n- Prototyping and exploration\n- Route handlers in small web apps\n\n**When to prefer explicit injection:**\n\nFor larger codebases with many dependencies, prefer explicit constructor injection—it's more testable and makes dependencies visible:\n\n```python\n# Service-locator-adjacent (convenient for small apps)\nasync def send_welcome_email(\n    user_email: str,\n    email: EmailPort = container.resolve(EmailPort)\n) -\u003e None:\n    await email.send(to=user_email, subject=\"Welcome!\", body=\"...\")\n\n# Explicit injection (recommended for larger codebases)\n@service\nclass WelcomeEmailHandler:\n    def __init__(self, email: EmailPort):\n        self.email = email\n\n    async def send(self, user_email: str) -\u003e None:\n        await self.email.send(to=user_email, subject=\"Welcome!\", body=\"...\")\n```\n\nBoth patterns work with dioxide's profile system for testing. Choose based on your codebase size and team preferences.\n\n## Testing with dioxide\n\ndioxide makes testing easy through **fakes at the seams** instead of mocks. The key pattern is creating a fresh `Container` instance per test for complete isolation.\n\n\u003e **[📖 Full Testing Guide](https://dioxide.readthedocs.io/en/latest/testing/)** - Comprehensive patterns for testing with dioxide\n\n### Fresh Container Per Test (Recommended)\n\nThe simplest approach uses the `fresh_container` helper from `dioxide.testing`:\n\n```python\nimport pytest\nfrom dioxide import Profile\nfrom dioxide.testing import fresh_container\n\n@pytest.fixture\nasync def container():\n    \"\"\"Fresh container per test - complete test isolation.\"\"\"\n    async with fresh_container(profile=Profile.TEST) as c:\n        yield c\n    # Cleanup happens automatically\n```\n\nOr create the container manually for more control:\n\n```python\nimport pytest\nfrom dioxide import Container, Profile\n\n@pytest.fixture\nasync def container():\n    \"\"\"Fresh container per test - complete test isolation.\n\n    Each test gets a fresh Container instance with:\n    - Clean singleton cache (no state from previous tests)\n    - Fresh adapter instances\n    - Automatic lifecycle management via async context manager\n\n    This is the RECOMMENDED pattern for test isolation.\n    \"\"\"\n    c = Container()\n    c.scan(profile=Profile.TEST)\n    async with c:\n        yield c\n    # Cleanup happens automatically\n```\n\n**Why this works:**\n- **Complete isolation**: Each test starts with a clean slate\n- **No state leakage**: Singletons are scoped to the container instance\n- **Lifecycle handled**: `@lifecycle` components are properly initialized/disposed\n- **Simple**: No need to track or clear fake state\n\n### Typed Fixtures for Fakes\n\nCreate typed fixtures to access your fake adapters with IDE support:\n\n```python\nfrom app.adapters.fakes import FakeEmailAdapter, FakeDatabaseAdapter\nfrom app.domain.ports import EmailPort, DatabasePort\n\n@pytest.fixture\ndef email(container) -\u003e FakeEmailAdapter:\n    \"\"\"Typed access to fake email for assertions.\"\"\"\n    return container.resolve(EmailPort)\n\n@pytest.fixture\ndef db(container) -\u003e FakeDatabaseAdapter:\n    \"\"\"Typed access to fake db for seeding test data.\"\"\"\n    return container.resolve(DatabasePort)\n```\n\n### Complete Test Example\n\n```python\nasync def test_user_registration_sends_welcome_email(container, email, db):\n    \"\"\"Test that registering a user sends a welcome email.\"\"\"\n    # Arrange: Get the service (dependencies auto-injected)\n    service = container.resolve(UserService)\n\n    # Act: Call the real service with real fakes\n    await service.register_user(\"alice@example.com\", \"Alice\")\n\n    # Assert: Check observable outcomes (no mock verification!)\n    assert len(email.sent_emails) == 1\n    assert email.sent_emails[0][\"to\"] == \"alice@example.com\"\n    assert \"Welcome\" in email.sent_emails[0][\"subject\"]\n```\n\n**Benefits over mocking:**\n- **Test real behavior**: Business logic actually runs\n- **No brittle mocks**: Tests don't break when you refactor\n- **Fast**: In-memory fakes, no I/O\n- **Deterministic**: Controllable fakes (FakeClock, etc.)\n\n### Alternative: Reset Container Between Tests\n\nIf you need a shared container (e.g., for TestClient integration tests), use `container.reset()`:\n\n```python\nfrom dioxide import container, Profile\n\n@pytest.fixture(autouse=True)\ndef setup_container():\n    \"\"\"Reset container between tests for isolation.\"\"\"\n    container.scan(profile=Profile.TEST)\n    yield\n    container.reset()  # Clears singleton cache, keeps registrations\n```\n\nOr clear fake state manually if you need more control:\n\n```python\n@pytest.fixture(autouse=True)\ndef clear_fakes():\n    \"\"\"Clear fake state before each test.\"\"\"\n    # Clear adapters from global container before test runs\n    db = container.resolve(DatabasePort)\n    if hasattr(db, \"users\"):\n        db.users.clear()\n\n    email = container.resolve(EmailPort)\n    if hasattr(email, \"sent_emails\"):\n        email.sent_emails.clear()\n```\n\n**Note**: The fresh container pattern is preferred because it requires no knowledge of fake internals.\n\nFor comprehensive testing patterns, see [TESTING_GUIDE.md](docs/TESTING_GUIDE.md).\n\n## Framework Integrations\n\ndioxide provides seamless integration with popular Python web frameworks.\n\n### FastAPI Integration\n\n```python\nfrom fastapi import FastAPI\nfrom dioxide import Profile\nfrom dioxide.fastapi import DioxideMiddleware, Inject\n\napp = FastAPI()\napp.add_middleware(DioxideMiddleware, profile=Profile.PRODUCTION, packages=[\"myapp\"])\n\n@app.get(\"/users\")\nasync def list_users(service: UserService = Inject(UserService)):\n    return await service.list_all()\n```\n\nInstall with: `pip install dioxide[fastapi]`\n\n**Features**: Single middleware setup, automatic container lifecycle, REQUEST-scoped components, sync/async support.\n\nSee the complete [FastAPI example](examples/fastapi/) for a full hexagonal architecture application.\n\n### Django Integration\n\n```python\n# settings.py or apps.py\nfrom dioxide import Profile\nfrom dioxide.django import configure_dioxide\n\nconfigure_dioxide(profile=Profile.PRODUCTION, packages=[\"myapp\"])\n\n# settings.py - add middleware\nMIDDLEWARE = [\n    ...\n    'dioxide.django.DioxideMiddleware',\n    ...\n]\n\n# views.py\nfrom dioxide.django import inject\n\ndef user_list(request):\n    service = inject(UserService)\n    return JsonResponse({\"users\": service.list_all()})\n```\n\nInstall with: `pip install dioxide[django]`\n\n**Features**: Single function setup, request scoping via middleware, thread-safe injection, works with class-based and function-based views.\n\n### Django REST Framework Integration\n\ndioxide works seamlessly with Django REST Framework - just use the same `inject()` function in your APIViews, ViewSets, or `@api_view` decorated functions:\n\n```python\nfrom rest_framework.decorators import api_view\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\nfrom dioxide.django import inject\n\n@api_view(['GET'])\ndef user_list(request):\n    service = inject(UserService)\n    return Response(service.list_all())\n\nclass UserViewSet(APIView):\n    def get(self, request):\n        service = inject(UserService)\n        return Response(service.list_all())\n```\n\n**Features**: Full DRF compatibility, works with all DRF view types (APIView, ViewSet, @api_view), request scoping through Django middleware.\n\nSee [Django Integration Guide](docs/integrations/django.md) for complete documentation including request scoping and lifecycle management.\n\n## Features\n\n### v1.0.0 STABLE - MLP Production Ready\n\n**Core Dependency Injection**:\n- [x] `@adapter.for_(Port, profile=...)` decorator for hexagonal architecture\n- [x] `@service` decorator for core business logic\n- [x] `Profile` enum (PRODUCTION, TEST, DEVELOPMENT, STAGING, CI, ALL)\n- [x] Container with `scan(profile=...)` for profile-based activation\n- [x] Port-based resolution (`container.resolve(Port)` returns active adapter)\n- [x] Constructor dependency injection via type hints\n- [x] Type-safe `Container.resolve()` with full mypy support\n- [x] Optional `container[Type]` bracket syntax\n- [x] Multi-binding support (`multi=True`, `priority=N`) for plugin patterns\n- [x] Collection injection via `list[Port]` type hint\n\n**Lifecycle Management**:\n- [x] `@lifecycle` decorator for opt-in lifecycle management\n- [x] `async with container:` context manager support\n- [x] Manual `container.start()` / `container.stop()` methods\n- [x] Dependency-ordered initialization (Kahn's algorithm)\n- [x] Reverse-order disposal with error resilience\n- [x] Graceful rollback on initialization failures\n\n**Test Ergonomics**:\n- [x] `fresh_container()` helper for isolated test containers\n- [x] `container.reset()` to clear singleton cache between tests\n- [x] `scope=Scope.FACTORY` on adapters for fresh instances per resolution\n- [x] Complete test isolation with fresh Container per test pattern\n\n**Reliability**:\n- [x] Circular dependency detection at startup (fail-fast)\n- [x] Excellent error messages with actionable suggestions\n- [x] Package scanning: `container.scan(package=\"app.services\")`\n- [x] High test coverage (~93%, 213+ tests)\n- [x] Full CI/CD automation with multi-platform wheels\n\n**Documentation \u0026 Examples**:\n- [x] **[ReadTheDocs](https://dioxide.readthedocs.io)** - Full documentation with API reference\n- [x] Complete FastAPI integration example\n- [x] Comprehensive testing guide (fakes \u003e mocks philosophy)\n- [x] Performance benchmarks with [honest comparisons](benchmarks/)\n- [x] Tutorials, guides, and architectural patterns\n- [x] Migration guides for all versions\n\n**Production Ready**:\n- [x] API frozen - No breaking changes until v2.0\n- [x] Published to PyPI with cross-platform wheels\n- [x] Battle-tested in real applications\n- [x] Ready for production deployment\n\n### Post-MLP Features (v1.1.0+)\n\nFuture enhancements under consideration:\n- Request scoping for web frameworks\n- Property injection\n- Django integration\n- Developer tooling (CLI, IDE plugins)\n\n## Development\n\n### Prerequisites\n\n- Python 3.11+\n- Rust 1.70+\n- [uv](https://github.com/astral-sh/uv) for Python package management\n- [maturin](https://github.com/PyO3/maturin) for building Rust extensions\n\n### Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/mikelane/dioxide.git\ncd dioxide\n\n# Install dependencies with uv (uses PEP 735 dependency groups)\nuv venv\nsource .venv/bin/activate  # or `.venv\\Scripts\\activate` on Windows\nuv sync --group dev\n\n# Build the Rust extension\nmaturin develop\n\n# Run tests\npytest\n\n# Run all quality checks\ntox\n```\n\n### Development Workflow\n\n```bash\n# Format code\ntox -e format\n\n# Lint\ntox -e lint\n\n# Type check\ntox -e type\n\n# Run tests for all Python versions\ntox\n\n# Run tests with coverage\ntox -e cov\n\n# Mutation testing\ntox -e mutate\n```\n\n### Pre-commit Hooks\n\nInstall pre-commit hooks to ensure code quality:\n\n```bash\npre-commit install\n```\n\n## Architecture\n\n```\ndioxide/\n├── python/dioxide/       # Python API\n│   ├── __init__.py\n│   ├── container.py       # Main Container class\n│   ├── decorators.py      # @component decorator\n│   └── scope.py           # Scope enum\n├── rust/src/              # Rust core\n│   └── lib.rs             # PyO3 bindings and graph logic\n├── tests/                 # Python tests\n└── pyproject.toml         # Project configuration\n```\n\n### Key Design Decisions\n\n1. **Python-first API** - Developers work in pure Python; Rust is an implementation detail\n2. **Type hints as the contract** - Leverage Python's type system for DI metadata\n3. **Hexagonal architecture by design** - `@adapter.for_(Port)` makes clean architecture obvious\n4. **Profile-based testing** - Built-in support for swapping implementations by environment\n5. **Rust for container operations** - Memory-efficient singleton caching and graph traversal\n6. **Test-driven development** - Every feature starts with failing tests\n\n## Comparison to Other Frameworks\n\n| Feature | dioxide | dependency-injector | injector |\n|---------|----------|---------------------|----------|\n| Zero-ceremony API | ✅ | ❌ | ✅ |\n| Built-in Profile system | ✅ | ❌ | ❌ |\n| Hexagonal architecture support | ✅ | ❌ | ❌ |\n| Type-based DI | ✅ | ✅ | ✅ |\n| Lifecycle management | ✅ | ✅ | ❌ |\n| Circular dependency detection | ✅ | ❌ | ❌ |\n| Memory efficiency | ✅ | ⚡ | ⚡ |\n\n**Performance notes**: Both dioxide (Rust/PyO3) and dependency-injector (Cython) offer excellent performance for cached singleton resolution. dependency-injector's mature Cython backend is slightly faster for raw lookups; dioxide is more memory-efficient. Both handle concurrent workloads equally well. See [benchmarks/](benchmarks/) for detailed, honest comparisons.\n\n## Contributing\n\nContributions are welcome! We follow a strict workflow to maintain code quality:\n\n**Quick start for contributors:**\n1. **Create or find an issue** - All work must be associated with a GitHub issue\n2. **Fork the repository** (external contributors)\n3. **Create a feature branch** with issue reference (e.g., `fix/issue-123-description`)\n4. **Follow TDD** - Write tests first, then implementation\n5. **Submit a Pull Request** - All changes must go through PR review\n\n**Key requirements:**\n- ✅ All work must have an associated GitHub issue\n- ✅ All changes must go through the Pull Request process\n- ✅ Tests and documentation are mandatory\n- ✅ Branch protection enforces these requirements on `main`\n\nPlease see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.\n\n**Resources:**\n- **[📖 Documentation](https://dioxide.readthedocs.io)** - Full documentation with tutorials and API reference\n- **[🗺️ Roadmap](ROADMAP.md)** - Complete product roadmap\n- **[💡 Design Philosophy](docs/MLP_VISION.md)** - MLP vision and architectural decisions\n- **[🧪 Testing Guide](docs/TESTING_GUIDE.md)** - Comprehensive testing patterns (fakes \u003e mocks)\n\n## License\n\nMIT License - see [LICENSE](LICENSE) for details.\n\n## Acknowledgments\n\n- Inspired by [dependency-injector](https://github.com/ets-labs/python-dependency-injector) and Spring Framework\n- Built with [PyO3](https://github.com/PyO3/pyo3) and [maturin](https://github.com/PyO3/maturin)\n- Graph algorithms powered by [petgraph](https://github.com/petgraph/petgraph)\n\n---\n\n**Production Ready**: dioxide v1.0.0 is production-ready with a stable, frozen API. All MLP (Minimum Loveable Product) features are complete, thoroughly tested, and battle-proven.\n\n**API Guarantee**: No breaking changes until v2.0. Your code written against v1.x will continue to work through all v1.x releases.\n\n**What's Next**:\n- **v1.1.0+**: Post-MLP enhancements (request scoping, property injection, additional framework integrations)\n- **v2.0.0+**: Major enhancements based on community needs\n\n**Get Started**: **[Read the Documentation](https://dioxide.readthedocs.io)** | **[Install from PyPI](https://pypi.org/project/dioxide/)** | **[View on GitHub](https://github.com/mikelane/dioxide)**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmikelane%2Fdioxide","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmikelane%2Fdioxide","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmikelane%2Fdioxide/lists"}