Inject() Function¶
The Inject()
function provides explicit dependency injection for specific parameters. It's an alternative to the @inject
decorator when you need fine-grained control over which dependencies are injected.
๐ฏ Basic Usage¶
Use Inject()
to specify which parameters should be injected:
from injectq import InjectQ, Inject
# Set up container
container = InjectQ.get_instance()
container[Database] = Database
container[UserService] = UserService
# Use Inject() for specific parameters
def process_user(user_id: int, service=Inject(UserService)) -> User:
# Only 'service' is injected, 'user_id' is passed normally
return service.get_user(user_id)
# Usage
user = process_user(user_id=123) # service automatically injected
๐ง How It Works¶
Selective Injection¶
Inject()
allows you to mix injected and regular parameters:
def create_report(
report_id: str, # Regular parameter
user_service=Inject(UserService), # Injected
analytics=Inject(Analytics), # Injected
format: str = "pdf" # Regular parameter with default
) -> Report:
# report_id and format passed normally
# user_service and analytics injected automatically
pass
# Usage
report = create_report(
"report_001", # report_id
format="excel" # format
# user_service and analytics automatically injected
)
Type-Based Resolution¶
Like @inject
, Inject()
uses type hints for resolution:
class IDatabase(Protocol):
def connect(self) -> None: ...
class PostgreSQLDatabase:
def connect(self) -> None:
print("Connected to PostgreSQL")
# Register implementation
container.bind(IDatabase, PostgreSQLDatabase)
def use_database(db=Inject(IDatabase)) -> None:
# InjectQ resolves IDatabase to PostgreSQLDatabase
db.connect()
use_database() # Prints: Connected to PostgreSQL
๐จ Advanced Patterns¶
Multiple Inject() Calls¶
Use multiple Inject()
calls in the same function:
def complex_operation(
operation_id: str,
db=Inject(Database),
cache=Inject(Cache),
logger=Inject(Logger),
config=Inject(AppConfig)
) -> Result:
logger.info(f"Starting operation {operation_id}")
# Use all injected dependencies
data = cache.get(operation_id)
if not data:
data = db.query(f"SELECT * FROM operations WHERE id = {operation_id}")
cache.set(operation_id, data)
return process_data(data, config)
With Default Values¶
Combine Inject()
with regular default values:
def send_email(
to: str,
subject: str,
body: str,
smtp=Inject(SMTPClient), # Injected
from_addr: str = "noreply@app.com", # Regular default
priority: str = "normal" # Regular default
) -> None:
smtp.send(
from_addr=from_addr,
to=to,
subject=subject,
body=body,
priority=priority
)
Conditional Injection¶
Use Inject()
conditionally:
def process_data(
data: bytes,
use_cache: bool = True,
cache=Inject(Cache), # Always injected
processor=Inject(DataProcessor)
) -> ProcessedData:
if use_cache:
# Use cache
cached = cache.get(data_hash(data))
if cached:
return cached
# Process data
result = processor.process(data)
if use_cache:
cache.set(data_hash(data), result)
return result
๐ Comparison with @inject¶
@inject Decorator¶
@inject
def process_all(service: UserService, cache: Cache, user_id: int) -> User:
# All parameters except self/cls are injected
return service.get_user(user_id)
Inject() Function¶
def process_selective(
user_id: int, # Regular parameter
service=Inject(UserService), # Injected
use_cache: bool = True # Regular parameter
) -> User:
# Only service is injected
return service.get_user(user_id)
๐งช Testing with Inject()¶
Override Specific Dependencies¶
from injectq.testing import override_dependency
def test_process_user():
mock_service = MockUserService()
with override_dependency(UserService, mock_service):
# Only UserService is mocked
result = process_user(user_id=1)
assert result.name == "Mock User"
Partial Mocking¶
def test_complex_operation():
mock_cache = MockCache()
mock_db = MockDatabase()
with override_dependency(Cache, mock_cache):
with override_dependency(Database, mock_db):
# Only Cache and Database are mocked
# Logger and Config use real implementations
result = complex_operation("op_123")
assert result is not None
๐ Real-World Examples¶
HTTP Handler¶
def handle_user_request(
request: HttpRequest,
response: HttpResponse,
user_service=Inject(UserService),
auth_service=Inject(AuthService)
) -> HttpResponse:
# Validate authentication
user = auth_service.authenticate(request.token)
if not user:
response.status = 401
return response
# Process request
data = user_service.get_user_data(user.id)
response.json = {"user": data}
return response
Background Job¶
def process_user_notifications(
user_ids: List[int],
notification_service=Inject(NotificationService),
user_service=Inject(UserService),
logger=Inject(Logger)
) -> None:
for user_id in user_ids:
try:
user = user_service.get_user(user_id)
if user and user.notifications_enabled:
notification_service.send_daily_digest(user)
logger.info(f"Sent notifications to user {user_id}")
except Exception as e:
logger.error(f"Failed to process notifications for user {user_id}: {e}")
Data Pipeline¶
def run_data_pipeline(
pipeline_config: dict,
extractor=Inject(DataExtractor),
transformer=Inject(DataTransformer),
loader=Inject(DataLoader),
monitor=Inject(PipelineMonitor)
) -> PipelineResult:
monitor.start_pipeline(pipeline_config["name"])
try:
# Extract
raw_data = extractor.extract(pipeline_config["source"])
# Transform
processed_data = transformer.transform(raw_data, pipeline_config["rules"])
# Load
result = loader.load(processed_data, pipeline_config["destination"])
monitor.end_pipeline(success=True)
return result
except Exception as e:
monitor.end_pipeline(success=False, error=str(e))
raise
โก Performance Considerations¶
Resolution Overhead¶
Inject()
has similar performance characteristics to @inject
:
# Each call resolves dependencies
for i in range(1000):
result = process_user(user_id=i) # Resolves UserService each time
Caching¶
Dependencies are cached based on their scope:
def use_service(service=Inject(UserService)) -> None:
pass
# If UserService is singleton
use_service() # Creates and caches UserService
use_service() # Reuses cached UserService
๐ Best Practices¶
1. Use for Selective Injection¶
# โ
Good - selective injection
def api_handler(request, service=Inject(UserService)):
pass
# โ Avoid - use @inject for all dependencies
def api_handler(service=Inject(UserService), cache=Inject(Cache)):
pass
2. Combine with Regular Defaults¶
# โ
Good - mix injected and regular defaults
def process(
data: bytes,
service=Inject(Processor),
retries: int = 3,
timeout: float = 30.0
):
pass
# โ Avoid - all parameters injected
def process(
data=Inject(bytes), # Doesn't make sense
service=Inject(Processor)
):
pass
3. Use Descriptive Parameter Names¶
# โ
Good - clear parameter names
def authenticate_user(
credentials: UserCredentials,
auth_service=Inject(AuthenticationService),
audit_logger=Inject(AuditLogger)
):
pass
# โ Avoid - unclear names
def auth(creds, auth_svc=Inject(AuthSvc), logger=Inject(Logger)):
pass
4. Document Injected Parameters¶
def create_user(
user_data: dict,
user_service=Inject(UserService), # Creates and saves user
email_service=Inject(EmailService) # Sends welcome email
) -> User:
"""Create a new user and send welcome email.
Args:
user_data: User information (name, email, etc.)
user_service: Automatically injected user service
email_service: Automatically injected email service
"""
pass
๐จ Common Patterns and Pitfalls¶
Pattern: Factory Functions¶
def create_user_service(db=Inject(Database)) -> UserService:
"""Factory function that creates UserService with injected dependencies."""
return UserService(db)
# Usage
service = create_user_service() # Dependencies injected
Pattern: Builder Pattern¶
class ReportBuilder:
def __init__(
self,
data_source=Inject(DataSource),
formatter=Inject(ReportFormatter)
):
self.data_source = data_source
self.formatter = formatter
def build_report(self, config: ReportConfig) -> Report:
data = self.data_source.get_data(config.query)
return self.formatter.format(data, config.format)
Pitfall: Over-Injection¶
# โ Too many injected parameters
def complex_function(
a=Inject(A), b=Inject(B), c=Inject(C),
d=Inject(D), e=Inject(E), f=Inject(F)
):
pass
# โ
Group related dependencies
@dataclass
class ProcessingContext:
service_a: A
service_b: B
service_c: C
def complex_function(context=Inject(ProcessingContext)):
pass
๐ฏ When to Use Inject()¶
โ Good For¶
- Selective injection - Only some parameters need injection
- Mixed parameters - Some injected, some passed normally
- Fine-grained control - Explicit about what's injected
- Legacy code - Gradually add DI without changing all functions
- Factory functions - Create objects with injected dependencies
โ Not Ideal For¶
- All parameters injected - Use
@inject
decorator instead - Simple functions - May add unnecessary complexity
- Type safety - Slightly less type-safe than
@inject
๐ Migration from Manual DI¶
Before (Manual)¶
def process_user(container, user_id: int) -> User:
service = container.get(UserService)
return service.get_user(user_id)
# Usage
user = process_user(container, 123)
After (Inject())¶
def process_user(user_id: int, service=Inject(UserService)) -> User:
return service.get_user(user_id)
# Usage
user = process_user(123) # container not needed
๐ Summary¶
The Inject()
function provides:
- Selective dependency injection - Choose which parameters to inject
- Fine-grained control - Mix injected and regular parameters
- Explicit injection - Clear about what's being injected
- Type-based resolution - Uses type hints for dependency resolution
- Testing friendly - Easy to override specific dependencies
Key features: - Works with any function (regular, async, methods, etc.) - Can be combined with regular default values - Supports all InjectQ dependency resolution features - Minimal performance overhead - Excellent for gradual adoption of DI
Ready to explore binding patterns?