Skip to content

@inject Decorator

The @inject decorator is InjectQ's most powerful and recommended way to inject dependencies. It automatically resolves and injects dependencies based on type hints, making your code clean and declarative.

๐ŸŽฏ Basic Usage

The @inject decorator eliminates manual dependency management:

from injectq import InjectQ, inject

container = InjectQ.get_instance()

# Set up container
container[Database] = Database
container[UserService] = UserService

# Use @inject decorator
@inject
def process_user(service: UserService, db: Database) -> None:
    # Dependencies automatically injected
    user = service.get_user(1)
    db.save(user)

# Call without parameters - dependencies injected automatically
process_user()

๐Ÿ—๏ธ How It Works

Automatic Resolution

The @inject decorator analyzes function signatures and resolves dependencies:

@inject
def create_report(
    user_service: UserService,
    analytics: AnalyticsService,
    cache: Cache,
    config: AppConfig
) -> Report:
    # InjectQ automatically:
    # 1. Gets UserService from container
    # 2. Gets AnalyticsService from container
    # 3. Gets Cache from container
    # 4. Gets AppConfig from container
    # 5. Calls the function with all dependencies
    pass

Type-Based Resolution

Dependencies are resolved based on type hints:

from injectq import InjectQ

container = InjectQ.get_instance()

class IUserRepository(Protocol):
    def get_by_id(self, id: int) -> Optional[User]: ...

class UserRepository:
    def get_by_id(self, id: int) -> Optional[User]:
        # Implementation
        pass

# Register implementation
container.bind(IUserRepository, UserRepository)

@inject
def get_user(repo: IUserRepository) -> Optional[User]:
    # InjectQ finds UserRepository for IUserRepository
    return repo.get_by_id(1)

๐ŸŽจ Advanced Patterns

Async Functions

Works seamlessly with async functions:

@inject
async def process_user_async(service: UserService, user_id: int) -> User:
    # All dependencies injected
    user = await service.get_user_async(user_id)
    return user

# Usage
result = await process_user_async(user_id=123)

Class Methods

Can be used on class methods:

class UserController:
    @inject
    def get_user(self, service: UserService, user_id: int) -> User:
        # 'self' is not injected, other parameters are
        return service.get_user(user_id)

    @classmethod
    @inject
    def create_user(cls, service: UserService, data: dict) -> User:
        # 'cls' is not injected
        return service.create_user(data)

Static Methods

Works with static methods:

class UserUtils:
    @staticmethod
    @inject
    def validate_user(service: UserService, user_id: int) -> bool:
        user = service.get_user(user_id)
        return user is not None and user.is_active

Nested Injection

Dependencies can have their own dependencies:

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

class UserRepository:
    def __init__(self, db: Database):
        self.db = db

class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo

# Register only the top-level service
container[UserService] = UserService

@inject
def use_service(service: UserService) -> None:
    # InjectQ automatically creates:
    # 1. DatabaseConfig
    # 2. Database (with DatabaseConfig)
    # 3. UserRepository (with Database)
    # 4. UserService (with UserRepository)
    pass

๐Ÿ”ง Integration Patterns

With FastAPI

from fastapi import FastAPI
from injectq.integrations.fastapi import Injected

app = FastAPI()

@app.get("/users/{user_id}")
@inject
def get_user(user_id: int, service: UserService) -> User:
    return service.get_user(user_id)

# Or using Injected type
@app.get("/users/{user_id}")
def get_user(user_id: int, service: Injected[UserService]) -> User:
    return service.get_user(user_id)

With Classes

Use @inject on __init__ methods:

class UserController:
    @inject
    def __init__(self, service: UserService, logger: Logger):
        self.service = service
        self.logger = logger

    def get_user(self, user_id: int) -> User:
        self.logger.info(f"Getting user {user_id}")
        return self.service.get_user(user_id)

# Usage
controller = UserController()  # Dependencies automatically injected

With Context Managers

class DatabaseTransaction:
    @inject
    def __init__(self, db: Database):
        self.db = db

    def __enter__(self):
        self.db.begin_transaction()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            self.db.rollback()
        else:
            self.db.commit()

@inject
def process_with_transaction(service: UserService) -> None:
    with DatabaseTransaction() as tx:
        # Transaction automatically injected
        service.update_user(1, {"name": "New Name"})

๐Ÿงช Testing with @inject

Mock Dependencies

from injectq import InjectQ
from injectq.testing import override_dependency

def test_user_service():
    mock_service = MockUserService()

    with override_dependency(UserService, mock_service):
        # @inject decorated functions use the mock
        result = get_user(user_id=1)
        assert result.name == "Mock User"

Test Containers

from injectq import InjectQ
from injectq.testing import test_container

def test_with_isolation():
    with test_container() as container:
        # Set up test dependencies
        container.bind(UserService, MockUserService)
        container.bind(Database, MockDatabase)

        # Test the function
        result = get_user(user_id=1)
        assert result is not None

๐Ÿšจ Error Handling

Missing Dependencies

@inject
def process_data(service: UserService) -> None:
    pass

# If UserService is not registered
try:
    process_data()
except DependencyNotFoundError as e:
    print(f"Missing dependency: {e}")

Circular Dependencies

class A:
    def __init__(self, b: B):
        self.b = b

class B:
    def __init__(self, a: A):  # Circular!
        self.a = a

container.bind(A, A)
container.bind(B, B)

@inject
def use_a(a: A) -> None:
    pass

# Will raise CircularDependencyError
use_a()

โšก Performance Considerations

Compilation

For better performance in production:

# Pre-compile dependency resolution
container.compile()

# Now @inject functions resolve faster
@inject
def fast_function(service: UserService) -> None:
    pass

Caching

Resolved instances are cached based on scope:

@inject
def use_service(service: UserService) -> None:
    pass

# First call - creates UserService
use_service()

# Second call - reuses cached UserService (if singleton)
use_service()

๐Ÿ† Best Practices

1. Use Type Hints

# โœ… Good - explicit type hints
@inject
def process_user(service: UserService, user_id: int) -> User:
    pass

# โŒ Avoid - missing type hints
@inject
def process_user(service, user_id):
    pass

2. Prefer Protocols

# โœ… Good - depend on abstractions
class IUserService(Protocol):
    def get_user(self, id: int) -> User: ...

@inject
def process_user(service: IUserService) -> None:
    pass

# โŒ Avoid - depend on concrete classes
@inject
def process_user(service: UserService) -> None:
    pass

3. Keep Functions Focused

# โœ… Good - single responsibility
@inject
def create_user(service: UserService, data: CreateUserRequest) -> User:
    return service.create_user(data)

@inject
def send_welcome_email(email_service: EmailService, user: User) -> None:
    email_service.send_welcome(user)

# โŒ Avoid - multiple responsibilities
@inject
def create_user_and_send_email(
    user_service: UserService,
    email_service: EmailService,
    data: CreateUserRequest
) -> User:
    user = user_service.create_user(data)
    email_service.send_welcome(user)  # Multiple concerns
    return user

4. Handle Optional Dependencies

# โœ… Good - optional dependencies
@inject
def log_request(logger: Optional[Logger], request: Request) -> None:
    if logger:
        logger.info(f"Request: {request.path}")

# โœ… Good - default values
@inject
def process_data(cache: Optional[Cache] = None) -> None:
    if cache:
        # Use cache
        pass
    else:
        # Cache not available
        pass

5. Use Descriptive Names

# โœ… Good - descriptive parameter names
@inject
def authenticate_user(
    auth_service: AuthenticationService,
    user_credentials: UserCredentials
) -> AuthResult:
    pass

# โŒ Avoid - unclear names
@inject
def auth(s: AuthenticationService, c: UserCredentials) -> AuthResult:
    pass

๐Ÿ”„ Comparison with Other Patterns

@inject vs Dict Interface

# Dict interface - manual resolution
def process_user(user_id: int) -> User:
    service = container[UserService]
    return service.get_user(user_id)

# @inject - automatic resolution
@inject
def process_user(service: UserService, user_id: int) -> User:
    return service.get_user(user_id)

@inject vs Inject() Function

# Inject() function - explicit injection
def process_user(user_id: int, service=Inject(UserService)) -> User:
    return service.get_user(user_id)

# @inject - implicit injection
@inject
def process_user(service: UserService, user_id: int) -> User:
    return service.get_user(user_id)

๐ŸŽฏ When to Use @inject

โœ… Ideal For

  • Most applications - Recommended default approach
  • Complex dependency graphs - Automatic resolution
  • Type safety - Full mypy support
  • Clean code - Declarative dependency specification
  • Testing - Easy to mock and override

โš ๏ธ Considerations

  • Performance - Slight overhead for resolution (can be optimized)
  • Debugging - Dependencies not visible in function calls
  • Learning curve - Need to understand type hints

๐ŸŽ‰ Summary

The @inject decorator provides:

  • Automatic dependency resolution - No manual wiring
  • Type-based injection - Uses type hints for resolution
  • Clean syntax - Declarative and readable
  • Full type safety - Works with mypy and IDEs
  • Async support - Works with async functions
  • Testing friendly - Easy to override dependencies

Key benefits: - Eliminates boilerplate dependency management - Makes dependencies explicit through type hints - Enables easy testing with dependency overrides - Works seamlessly with all InjectQ features - Provides excellent IDE support and autocomplete

Ready to explore the Inject() function?