Metadata-Version: 2.4
Name: getit-python
Version: 1.0.1
Summary: Service locator and dependency injection container for Python with thread-safe singletons, lazy loading, and factory patterns
Author-email: Aziz Al-Soufi <azizalsoufi.inbox@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/AzizAl-Soufi/get_it
Project-URL: Documentation, https://github.com/AzizAl-Soufi/get_it#readme
Project-URL: Repository, https://github.com/AzizAl-Soufi/get_it.git
Project-URL: Issues, https://github.com/AzizAl-Soufi/get_it/issues
Project-URL: Changelog, https://github.com/AzizAl-Soufi/get_it/releases
Keywords: getit,get-it,get_it,dependency-injection,service-locator,di-container,ioc,singleton,lazy-loading,factory-pattern,thread-safe,decorators
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Systems Administration
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: isort>=5.0; extra == "dev"
Requires-Dist: flake8>=6.0; extra == "dev"
Requires-Dist: pylint>=2.0; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx>=5.0; extra == "docs"
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
Dynamic: license-file
Dynamic: requires-python

# GetIt: Service Locator & Dependency Injection Container

[![Python Version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

**GetIt** is a comprehensive, thread-safe service locator and dependency injection container for Python. It provides production-grade dependency management through declarative registration, multiple lifecycle strategies, async-first design, and automatic resource management—with minimal boilerplate and maximum architectural clarity.

Designed for WSGI/ASGI applications, microservices, cloud-native systems, and complex Python projects requiring flexible, maintainable dependency management.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Core Concepts](#core-concepts)
- [Basic Usage](#basic-usage)
- [Dependency Lifecycles](#dependency-lifecycles)
- [Resolution Strategies](#resolution-strategies)
- [Advanced Features](#advanced-features)
- [Async Patterns](#async-patterns)
- [Scoped Dependencies](#scoped-dependencies)
- [Best Practices](#best-practices)
- [Testing Strategies](#testing-strategies)
- [API Reference](#api-reference)
- [Troubleshooting](#troubleshooting)

## Features

**Core Capabilities**
- **Thread-Safe Implementation** — Double-checked locking guarantees singleton instantiation exactly once under concurrent load
- **Multiple Lifecycle Strategies** — Singleton, Lazy Singleton, Factory, Async Singleton, Scoped, and Weak Singleton patterns
- **Async-First Design** — Full support for async initialization with topological dependency ordering and concurrent initialization
- **Named Registrations** — Support multiple instances of the same type with distinct identifiers
- **Flexible Resolution APIs** — Type-based, dot-notation, instance call, and metaclass syntaxes for different coding styles

**Developer Experience**
- **Zero Circular Imports** — Defer dependency resolution to runtime without architectural constraints
- **Declarative Registration** — Use `@singleton`, `@lazy_singleton`, `@factory` decorators at point of definition
- **Full Type Hint Support** — Complete type annotations with `@overload` for IDE autocomplete in VS Code and PyCharm
- **Comprehensive Error Handling** — Custom exceptions with actionable diagnostics and debugging information
- **Structured Logging** — Built-in logging at DEBUG level for observability and troubleshooting

**Production Features**
- **Scope Management** — Context-local dependency isolation for request-scoped, transaction-scoped, and context-scoped patterns
- **Resource Cleanup** — Automatic disposal hooks with LIFO cleanup ordering for proper resource finalization
- **Circular Dependency Detection** — Validates dependency graph at registration time to prevent runtime failures
- **Override & Restore** — Temporarily replace dependencies for testing without modifying production code

## Installation

```bash
pip install getit-python
```

**Requirements:** Python 3.8 or later

## Core Concepts

### Service Locator Pattern

GetIt implements the Service Locator pattern, which centralizes dependency resolution through a singleton container. Services request dependencies from the container at runtime rather than requiring explicit constructor injection.

### Dependency Lifecycle

Every registered dependency has a lifecycle that determines instantiation timing and instance count:

| Lifecycle | Instantiation | Instances | Use Case |
|-----------|---------------|-----------|----------|
| **Singleton** | At registration | Exactly 1 | Static config, caches, shared state |
| **Lazy Singleton** | On first access | Exactly 1 | Database connections, expensive initialization |
| **Factory** | Every access | Many (fresh each time) | Request contexts, temporary objects |
| **Async Singleton** | At `all_ready()` | Exactly 1 | Async-initialized services with ordering |
| **Scoped** | Per scope | 1 per scope | Request-scoped, transaction-scoped dependencies |
| **Weak Singleton** | On first access | 0 or 1 (GC-sensitive) | Memory-sensitive caches |

## Basic Usage

### Declarative Registration with Decorators

Services self-register using decorators at class definition:

```python
from get_it import GetIt, singleton, lazy_singleton, factory

@singleton
class Configuration:
    """Loaded once at container initialization."""
    def __init__(self):
        self.debug = True
        self.database_url = "postgresql://localhost/mydb"

@lazy_singleton
class DatabasePool:
    """Loaded on first access."""
    def __init__(self):
        print("Initializing database connection pool...")
        self.connections = self._create_pool()
    
    def _create_pool(self):
        # Expensive initialization deferred until needed
        return []

@factory
class RequestContext:
    """Fresh instance for each request."""
    def __init__(self):
        import uuid
        self.request_id = str(uuid.uuid4())
        self.user = None

# Resolution
config = GetIt.get(Configuration)
db_pool = GetIt.get(DatabasePool)
request_ctx = GetIt.get(RequestContext)
```

### Manual Runtime Registration

Register services programmatically when needed:

```python
from get_it import GetIt

class Logger:
    def log(self, message: str) -> None:
        print(f"[LOG] {message}")

# Register an existing instance
logger = Logger()
GetIt.register_singleton(logger)

# Register a factory
GetIt.register_factory(Logger, lambda: Logger())

# Register a lazy-loaded service
GetIt.register_lazy_singleton(Logger, lambda: Logger())

# Resolve
logger_instance = GetIt.get(Logger)
logger_instance.log("Service initialized")
```

## Dependency Lifecycles

### Singleton (Eager Initialization)

Instantiated immediately when registered. The instance exists for the entire application lifetime:

```python
from get_it import GetIt, singleton

@singleton
class ApplicationMetadata:
    """Initialized immediately at import time."""
    def __init__(self):
        print("[INIT] Loading application metadata...")
        self.version = "2.0.0"
        self.environment = "production"

# Initialization happens during decorator evaluation
metadata = GetIt.get(ApplicationMetadata)
```

**Advantages:**
- Failures occur at startup (fail-fast principle)
- Simple, predictable initialization

**Disadvantages:**
- Blocks application startup if initialization is expensive
- Resources allocated even if never used

**Use when:**
- Initialization is fast and non-blocking
- Failures should prevent application startup
- Resource is stateless or read-only
- Configuration affects the entire application

### Lazy Singleton (Deferred Initialization)

Instantiated on first access using double-checked locking for thread safety. The instance is cached for subsequent accesses:

```python
from get_it import GetIt, lazy_singleton

@lazy_singleton
class DatabaseConnection:
    """Initialized only on first access."""
    def __init__(self):
        print("[DB] Establishing connection...")
        import psycopg2
        self.conn = psycopg2.connect("postgresql://localhost/mydb")
    
    def query(self, sql: str):
        return self.conn.execute(sql)

# No initialization yet
print("Application starting...")

# First access triggers initialization
result = GetIt.get(DatabaseConnection).query("SELECT VERSION();")
```

**Advantages:**
- Defers expensive operations until needed
- Reduces startup time
- Allows graceful handling of initialization failures

**Disadvantages:**
- First access slightly slower due to initialization
- Initialization timing unpredictable

**Use when:**
- Initialization is expensive (I/O, network, computation)
- Service may not be needed in all execution paths
- You want to defer failures until actual usage

### Factory (Fresh Instance Per Access)

Creates a new instance on every resolution request. Each instance is independent:

```python
from get_it import GetIt, factory

@factory
class RequestContext:
    """New instance for each request or operation."""
    def __init__(self):
        import uuid
        self.request_id = str(uuid.uuid4())
        self.user = None
        self.metadata = {}

# Each call creates a new instance
ctx1 = GetIt.get(RequestContext)
ctx2 = GetIt.get(RequestContext)

assert ctx1 is not ctx2
assert ctx1.request_id != ctx2.request_id
```

**Advantages:**
- Complete isolation between instances
- No shared state issues
- Suitable for request/transaction scoping

**Disadvantages:**
- Memory overhead from frequent instantiation
- Initialization cost paid every time

**Use when:**
- Each invocation needs isolated state
- Objects are request-scoped or transaction-scoped
- State must not leak between calls
- Stateless factories with minimal overhead

## Resolution Strategies

GetIt provides multiple ways to resolve dependencies. Choose based on context and coding style:

### Strategy 1: Explicit Type-Based Resolution (Recommended)

Most explicit and type-safe approach:

```python
from get_it import GetIt, lazy_singleton

@lazy_singleton
class UserRepository:
    pass

# Clearest intent, best for type checking
repo = GetIt.get(UserRepository)
```

### Strategy 2: Instance Method Call

Using the `GetIt` instance as a callable:

```python
get_it = GetIt()
repo = get_it(UserRepository)
```

### Strategy 3: Dot-Notation (Flutter Style)

Resolves by matching class name:

```python
get_it = GetIt()
repo = get_it.UserRepository  # Resolves by class name
```

**Note:** Less type-safe; rely on `TYPE_CHECKING` for IDE support.

### Strategy 4: Metaclass Call

Direct class-level resolution:

```python
repo = GetIt(UserRepository)
```

## Advanced Features

### Named Registrations

Support multiple instances of the same type with distinct names:

```python
from get_it import GetIt

class DatabaseConnection:
    def __init__(self, url: str):
        self.url = url

# Register multiple instances with names
prod_db = DatabaseConnection("postgresql://prod.example.com/db")
test_db = DatabaseConnection("sqlite:///:memory:")
cache_db = DatabaseConnection("redis://localhost:6379")

GetIt.register_singleton(prod_db, name="prod")
GetIt.register_singleton(test_db, name="test")
GetIt.register_singleton(cache_db, name="cache")

# Resolve by name
production = GetIt.get(DatabaseConnection, name="prod")
testing = GetIt.get(DatabaseConnection, name="test")
cache = GetIt.get(DatabaseConnection, name="cache")
```

### Override and Restore (Testing)

Temporarily replace a dependency for testing without modifying production code:

```python
from get_it import GetIt, lazy_singleton

@lazy_singleton
class PaymentGateway:
    def charge(self, amount: float) -> bool:
        # Real implementation calls payment processor
        return True

class MockPaymentGateway:
    def charge(self, amount: float) -> bool:
        # Test implementation returns fixed response
        return True

# In tests
def test_checkout_flow():
    # Setup
    mock_gateway = MockPaymentGateway()
    GetIt.override(PaymentGateway, mock_gateway)
    
    # Execute
    service = OrderService()
    result = service.checkout(amount=99.99)
    
    # Assert
    assert result is True
    
    # Cleanup
    GetIt.restore_override(PaymentGateway)
```

### Weak Singletons

Singleton that recreates if garbage collected. Useful for memory-sensitive caches:

```python
from get_it import GetIt

class CacheItem:
    def __init__(self, key: str, value: str):
        self.key = key
        self.value = value

def create_cache_item():
    return CacheItem("config", "expensive_value")

GetIt.register_weak_singleton(CacheItem, create_cache_item)

# Instance exists while referenced
item = GetIt.get(CacheItem)
print(item.value)

# If no external references and GC runs, 
# next GetIt.get() recreates it
del item  # Remove external reference
import gc
gc.collect()

item2 = GetIt.get(CacheItem)  # Recreated
```

**Use when:**
- Caching objects that should be reclaimed under memory pressure
- Memory-sensitive applications with soft reference caches

## Async Patterns

### Async Singleton with Dependency Ordering

Register async-initialized services with dependency graph and topological initialization:

```python
import asyncio
from get_it import GetIt

class Config:
    async def load(self):
        await asyncio.sleep(0.1)
        return "config_loaded"

class Database:
    def __init__(self, config: str):
        self.config = config

async def create_config():
    config = Config()
    await config.load()
    return config

async def create_database(config: Config):
    # Config is guaranteed to be initialized first
    # Parameter is auto-injected based on type hint
    return Database(config)

GetIt.register_singleton_async(Config, create_config)
GetIt.register_singleton_async(
    Database, 
    create_database, 
    depends_on=[Config]  # Ensures Config initializes first
)

async def main():
    # Initializes in topological order: Config → Database
    await GetIt.all_ready()
    
    db = GetIt.get(Database)
    print(f"Database ready with config: {db.config}")

asyncio.run(main())
```

### Complex Dependency Graph

Topological ordering handles multi-level dependencies with concurrent initialization:

```python
import asyncio
from get_it import GetIt

async def init_logger():
    await asyncio.sleep(0.01)
    return {"type": "logger"}

async def init_config(logger: dict):
    # logger parameter auto-injected from dict type
    await asyncio.sleep(0.01)
    return {"logger": logger, "debug": True}

async def init_database(config: dict):
    # config parameter auto-injected from dict type
    await asyncio.sleep(0.01)
    return {"config": config}

async def init_repository(database: dict, logger: dict):
    # Both parameters auto-injected based on type hints
    await asyncio.sleep(0.01)
    return {"database": database, "logger": logger}

GetIt.register_singleton_async(dict, init_logger, name="logger")
GetIt.register_singleton_async(
    dict, 
    init_config, 
    name="config",
    depends_on=[dict]  # Depends on logger
)
GetIt.register_singleton_async(
    dict,
    init_database,
    name="database",
    depends_on=[dict]  # Depends on config
)
GetIt.register_singleton_async(
    dict,
    init_repository,
    name="repository",
    depends_on=[dict]  # Depends on database and logger
)

async def main():
    # Initialization order: logger → config → database → repository
    await GetIt.all_ready()
    
    repository = GetIt.get(dict, name="repository")
    print(repository)

asyncio.run(main())
```

**Auto-Injection for Async Factories:** Parameters in async factory functions are automatically injected based on their type hints. The framework resolves dependencies in topological order, ensuring all dependencies are initialized before a dependent service. Parameters without type hints use default values if provided, or raise an error if required.

### Async Factory

Creates new instances asynchronously on each access:

```python
import asyncio
from get_it import GetIt

async def create_connection():
    await asyncio.sleep(0.1)  # Simulated async connection
    return {"status": "connected"}

GetIt.register_factory_async(dict, create_connection)

async def main():
    # Each call creates a new instance asynchronously
    conn1 = await GetIt.get_async(dict)
    conn2 = await GetIt.get_async(dict)
    
    assert conn1 is not conn2

asyncio.run(main())
```

## Scoped Dependencies

Scopes isolate dependencies within a context (request, transaction, operation). Perfect for web frameworks:

```python
import asyncio
from get_it import GetIt

class RequestContext:
    def __init__(self, request_id: str):
        self.request_id = request_id
        self.user = None
        self.closed = False
    
    async def cleanup(self):
        self.closed = True
        print(f"[CLEANUP] Request {self.request_id}")

async def handle_request(request_id: str):
    # Enter scope for this request
    GetIt.push_scope(f"request_{request_id}")
    
    try:
        # Register request-scoped services
        ctx = RequestContext(request_id)
        GetIt.register_singleton_scoped(RequestContext, ctx)
        
        # Use services
        retrieved_ctx = GetIt.get(RequestContext)
        print(f"Processing request {retrieved_ctx.request_id}")
        
    finally:
        # Exit scope and trigger cleanup
        await GetIt.pop_scope()

async def main():
    # Run multiple requests with isolated scopes
    await asyncio.gather(
        handle_request("req-001"),
        handle_request("req-002"),
        handle_request("req-003")
    )

asyncio.run(main())
```

### FastAPI Integration

```python
from fastapi import FastAPI, Request
from get_it import GetIt
import asyncio

app = FastAPI()

class RequestDB:
    def __init__(self, request_id: str):
        self.request_id = request_id
        self.session = None

@app.middleware("http")
async def setup_scope(request: Request, call_next):
    """Middleware to manage request scope."""
    request_id = request.headers.get("X-Request-ID", "unknown")
    GetIt.push_scope(f"request_{request_id}")
    
    try:
        response = await call_next(request)
    finally:
        await GetIt.pop_scope()
    
    return response

@app.get("/data/{item_id}")
async def get_data(item_id: int):
    db = GetIt.get(RequestDB)
    return {"item_id": item_id, "request_id": db.request_id}
```

## Best Practices

### 1. Organize by Feature

Group related services and register them together:

```python
# services/auth.py
from get_it import lazy_singleton

@lazy_singleton
class AuthService:
    def authenticate(self, token: str) -> bool:
        return len(token) > 10

# services/database.py
from get_it import lazy_singleton

@lazy_singleton
class Database:
    def query(self, sql: str):
        return f"Result: {sql}"

# main.py
from services import auth, database  # Services auto-register on import
from get_it import GetIt

auth_service = GetIt.get(auth.AuthService)
db = GetIt.get(database.Database)
```

### 2. Prefer Explicit Dependencies

Use constructor injection over service locator lookups when possible:

```python
# ❌ Anti-pattern: Hidden dependencies
class UserService:
    def get_user(self, user_id: int):
        db = GetIt.get(Database)  # Hidden dependency
        return db.query(f"SELECT * FROM users WHERE id = {user_id}")

# ✅ Better: Explicit constructor injection
class UserService:
    def __init__(self, database: Database):
        self.database = database
    
    def get_user(self, user_id: int):
        return self.database.query(f"SELECT * FROM users WHERE id = {user_id}")

# Register with dependency
GetIt.register_lazy_singleton(
    UserService, 
    lambda: UserService(GetIt.get(Database))
)
```

### 3. Use Named Registrations for Variants

```python
# ✅ Better than creating separate classes
from get_it import GetIt

cache_redis = RedisCache(url="redis://localhost")
cache_memory = MemoryCache()

GetIt.register_singleton(cache_redis, name="redis")
GetIt.register_singleton(cache_memory, name="memory")

redis_cache = GetIt.get(Cache, name="redis")
memory_cache = GetIt.get(Cache, name="memory")
```

### 4. Fail Fast with Singletons

Use singletons for critical initialization so failures occur at startup:

```python
@singleton
class CriticalDatabasePool:
    def __init__(self):
        # Raises exception if connection fails
        # Prevents application startup on failure
        self.pool = self._establish_connection()
    
    def _establish_connection(self):
        import psycopg2
        return psycopg2.pool.SimpleConnectionPool(
            1, 20, 
            "postgresql://localhost/critical_db"
        )
```

### 5. Use Scopes for Request Isolation

```python
# For FastAPI/Flask/etc
@app.middleware("http")
async def request_scope(request: Request, call_next):
    GetIt.push_scope(f"request_{id(request)}")
    try:
        response = await call_next(request)
    finally:
        await GetIt.pop_scope()  # Cleanup
    return response
```

## Testing Strategies

### Test Isolation with Reset

```python
import pytest
from get_it import GetIt

@pytest.fixture(autouse=True)
def clean_di():
    """Ensure clean DI container for each test."""
    GetIt.reset()
    yield
    GetIt.reset()

def test_user_service():
    class MockDatabase:
        def query(self, sql):
            return [{"id": 1, "name": "Test User"}]
    
    GetIt.register_singleton(MockDatabase())
    
    service = UserService()
    users = service.get_users()
    assert len(users) == 1
```

### Override Dependencies

```python
from get_it import GetIt

class RealPaymentService:
    def charge(self, amount: float) -> bool:
        # Real implementation
        return self._call_payment_gateway(amount)

class MockPaymentService:
    def charge(self, amount: float) -> bool:
        return True

def test_checkout():
    GetIt.register_singleton(RealPaymentService())
    mock = MockPaymentService()
    GetIt.override(RealPaymentService, mock)
    
    try:
        service = OrderService()
        assert service.checkout(100) is True
    finally:
        GetIt.restore_override(RealPaymentService)
```

### Fixture-Based Setup

```python
import pytest
from get_it import GetIt

@pytest.fixture
def mock_database():
    class MockDB:
        def query(self, sql):
            return []
    
    db = MockDB()
    GetIt.register_singleton(db)
    yield db
    GetIt.reset()

def test_with_mock(mock_database):
    service = UserService()
    assert service.get_users() == []
```

## API Reference

### Registration Methods

```python
# Eager singleton
GetIt.register_singleton(instance, name=None, dispose=None)

# Lazy singleton
GetIt.register_lazy_singleton(Type, factory, name=None, depends_on=None, dispose=None)

# Factory
GetIt.register_factory(Type, factory, name=None, depends_on=None, dispose=None)

# Async singleton
GetIt.register_singleton_async(Type, async_factory, name=None, depends_on=None, dispose=None)

# Async factory
GetIt.register_factory_async(Type, async_factory, name=None, dispose=None)

# Weak singleton
GetIt.register_weak_singleton(Type, factory, name=None, depends_on=None, dispose=None)

# Scoped singleton
GetIt.register_singleton_scoped(Type, instance, name=None, dispose=None)

# Scoped factory
GetIt.register_factory_scoped(Type, factory, name=None, dispose=None)
```

### Resolution Methods

```python
# Synchronous resolution
instance = GetIt.get(Type, name=None)

# Asynchronous resolution
instance = await GetIt.get_async(Type, name=None)

# Instance call
instance = GetIt()(Type)

# Dot notation
instance = GetIt().ServiceName
```

### Async Coordination

```python
# Initialize all async singletons with dependency ordering
await GetIt.all_ready()
```

### Scope Management

```python
# Enter scope
scope = GetIt.push_scope(name: str) -> Scope

# Exit scope and cleanup
await GetIt.pop_scope()

# Get current scope
current = GetIt.current_scope -> Scope
```

### Testing Utilities

```python
# Clear all registrations
GetIt.reset()

# Override a dependency
GetIt.override(Type, instance, name=None)

# Restore overridden dependency
GetIt.restore_override(Type, name=None)
```

## Troubleshooting

### DependencyNotRegisteredError

**Symptom:** `DependencyNotRegisteredError: Type 'SomeService' is not registered`

**Cause:** Attempting to resolve an unregistered type.

**Solution:**
```python
# ❌ Not registered
service = GetIt.get(UnregisteredService)

# ✅ Register first
GetIt.register_lazy_singleton(UnregisteredService, lambda: UnregisteredService())
service = GetIt.get(UnregisteredService)
```

### CircularDependencyError

**Symptom:** `CircularDependencyError: Circular dependency detected: A -> B -> C -> A`

**Cause:** Dependency cycle detected (A depends on B, B depends on C, C depends on A).

**Solution:** Refactor to break the cycle:
```python
# ❌ Circular
GetIt.register_lazy_singleton(A, factory_a, depends_on=[C])
GetIt.register_lazy_singleton(B, factory_b, depends_on=[A])
GetIt.register_lazy_singleton(C, factory_c, depends_on=[B])

# ✅ Refactored
GetIt.register_lazy_singleton(A, factory_a)
GetIt.register_lazy_singleton(B, lambda: factory_b(GetIt.get(A)))
GetIt.register_lazy_singleton(C, lambda: factory_c(GetIt.get(B)))
```

### DependencyNotReadyError

**Symptom:** `DependencyNotReadyError: Async singleton 'Database' is not ready`

**Cause:** Accessing an async singleton before `await GetIt.all_ready()`.

**Solution:**
```python
# ❌ Not ready
GetIt.register_singleton_async(Database, create_database_async)
db = GetIt.get(Database)

# ✅ Wait first
GetIt.register_singleton_async(Database, create_database_async)
await GetIt.all_ready()
db = GetIt.get(Database)
```

### InvalidScopeError

**Symptom:** `InvalidScopeError: Cannot pop from empty scope stack`

**Cause:** Calling `pop_scope()` when no scope is active.

**Solution:**
```python
# ❌ Empty stack
await GetIt.pop_scope()

# ✅ Push before pop
GetIt.push_scope("request")
try:
    # Use scoped services
    pass
finally:
    await GetIt.pop_scope()
```

## License

MIT License. See LICENSE file for details.
