Decorators¶
Version: v0.90
Author: Plumeink
This document describes the decorator-based component registration system in Cullinan 0.90.
Overview¶
Cullinan 0.90 introduces a powerful decorator system that provides a clean, declarative way to register components. This approach is consistent with Cullinan's original design philosophy while integrating with the new IoC/DI 2.0 architecture.
Key Features¶
- Simple Syntax: Use
@service,@controller,@componentwithout parentheses - Two-Phase Registration: Decorators collect metadata →
refresh()registers all - Dependency Injection: Use
Inject,InjectByName,Lazymarkers - Conditional Registration: Register components based on conditions
Component Decorators¶
@service¶
Marks a class as a service component. Services are singleton by default.
from cullinan.core import service
from cullinan.core.decorators import Inject
# Simple usage (no parentheses needed)
@service
class UserService:
def get_user(self, id: int):
return {"id": id}
# With parameters
@service(name="customUserService", scope="prototype")
class UserService:
pass
# With dependencies
@service(dependencies=["EmailService"])
class NotificationService:
email: EmailService = Inject()
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
Class name | Custom component name |
scope |
str |
"singleton" |
Scope: "singleton", "prototype", "request" |
dependencies |
list[str] |
None |
Explicit dependencies for ordering |
@controller¶
Marks a class as a controller component. Controllers handle HTTP requests.
from cullinan.controller import controller, get_api
from cullinan.core import Inject
from cullinan.params import Path
# Simple usage
@controller
class RootController:
pass
# With URL prefix
@controller(url="/api/users")
class UserController:
user_service: UserService = Inject()
@get_api(url="")
def list_users(self):
return {"users": []}
@get_api(url="/{id}")
async def get_user(self, id: int = Path()):
return self.user_service.get_user(id)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
url |
str |
"" |
URL prefix for all routes |
@component¶
Generic component decorator for classes that are neither services nor controllers.
from cullinan.core import component
# Simple usage
@component
class CacheManager:
pass
# With scope
@component(scope="prototype")
class RequestHandler:
pass
# With custom name
@component(name="myHelper")
class Helper:
pass
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
Class name | Custom component name |
scope |
str |
"singleton" |
Scope: "singleton", "prototype", "request" |
@provider¶
Marks a class as a dependency provider (factory).
from cullinan.core.decorators import provider
@provider
class DatabaseConnectionProvider:
def get(self):
return create_connection()
@provider(name="customProvider")
class CustomProvider:
pass
Injection Markers¶
Inject¶
Inject a dependency by type annotation.
from cullinan.core.decorators import Inject
@service
class UserService:
# Required injection (default)
email_service: EmailService = Inject()
# Optional injection
cache: CacheService = Inject(required=False)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
required |
bool |
True |
Raise error if dependency not found |
InjectByName¶
Inject a dependency by explicit name.
from cullinan.core.decorators import InjectByName
@service
class UserService:
# Explicit name
email = InjectByName("EmailService")
# Auto-infer from attribute name (user_repo -> UserRepo)
user_repo = InjectByName()
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
None |
Dependency name (auto-infer if None) |
required |
bool |
True |
Raise error if dependency not found |
Lazy¶
Lazy injection - resolve dependency on first access. Useful for breaking circular dependencies.
from cullinan.core.decorators import Lazy
@service
class ServiceA:
# Break circular dependency
service_b: 'ServiceB' = Lazy()
@service
class ServiceB:
service_a: 'ServiceA' = Lazy()
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
None |
Optional explicit dependency name |
Conditional Decorators¶
@ConditionalOnProperty¶
Register component based on configuration property.
from cullinan.core.decorators import service
from cullinan.core.conditions import ConditionalOnProperty
@service
@ConditionalOnProperty("feature.email", having_value="true")
class EmailService:
pass
@service
@ConditionalOnProperty("cache.enabled", match_if_missing=True)
class CacheService:
pass
@ConditionalOnClass¶
Register component only if a class/module is available.
from cullinan.core.conditions import ConditionalOnClass
@service
@ConditionalOnClass("redis.Redis")
class RedisCacheService:
pass
@ConditionalOnMissingBean¶
Register component only if specified bean is not already registered.
from cullinan.core.conditions import ConditionalOnMissingBean
@service
@ConditionalOnMissingBean("CustomEmailService")
class DefaultEmailService:
pass
@ConditionalOnBean¶
Register component only if specified bean exists.
from cullinan.core.conditions import ConditionalOnBean
@service
@ConditionalOnBean("DatabaseConnection")
class DatabaseService:
pass
@Conditional¶
Generic conditional with custom function.
from cullinan.core.conditions import Conditional
def is_production(ctx):
return ctx.get_property("env") == "production"
@service
@Conditional(is_production)
class ProductionOnlyService:
pass
Complete Example¶
from cullinan.controller import controller, get_api
from cullinan.core import service, ApplicationContext, PendingRegistry
from cullinan.core.decorators import Inject, InjectByName
from cullinan.core.conditions import ConditionalOnClass
from cullinan.params import Path
# Reset for clean state
PendingRegistry.reset()
# Define services
@service
class EmailService:
def send(self, to: str, content: str):
return f"Email sent to {to}"
@service
class UserService:
email_service: EmailService = Inject()
def get_user(self, id: int):
return {"id": id, "name": f"User{id}"}
def notify_user(self, id: int):
user = self.get_user(id)
return self.email_service.send(user["name"], "Hello!")
# Define controller
@controller(url="/api/users")
class UserController:
user_service: UserService = Inject()
@get_api(url="/{id}")
async def get_user(self, id: int = Path()):
return self.user_service.get_user(id)
# Optional JSON processor (only if json module available)
@service
@ConditionalOnClass("json")
class JsonProcessor:
def process(self, data):
import json
return json.dumps(data)
# Create and start application
ctx = ApplicationContext()
ctx.refresh()
# Use services
user_svc = ctx.get("UserService")
print(user_svc.get_user(1))
# Output: {'id': 1, 'name': 'User1'}
# Cleanup
ctx.shutdown()
Two-Phase Registration¶
Decorators use a two-phase registration mechanism:
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Module Loading │ │ ApplicationContext │ │ Runtime Use │
│ │ │ refresh() │ │ │
│ @service collects │────▶│ Process pending │────▶│ ctx.get("Name") │
│ metadata to │ │ registrations │ │ returns instance │
│ PendingRegistry │ │ Freeze registry │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
PendingRegistry¶
All decorator metadata is collected in PendingRegistry:
from cullinan.core import PendingRegistry
# Check registered components before refresh
pending = PendingRegistry.get_instance()
print(f"Pending registrations: {pending.count}")
# After refresh, registry is frozen
ctx.refresh()
assert pending.is_frozen # True
Best Practices¶
-
Use decorators without parentheses when possible:
@serviceis cleaner than@service() -
Use
Injectfor type-based injection: Provides better IDE support -
Use
Lazyfor circular dependencies: Breaks the cycle explicitly -
Put conditions after component decorator: Order matters!
@service # First @ConditionalOnClass("redis") # Second class RedisService: pass -
Reset PendingRegistry in tests:
def setup_method(self): PendingRegistry.reset()
Migration from v0.83¶
| v0.83 | v0.90 |
|---|---|
@service (from cullinan.service) |
@service (from cullinan.core) |
@controller(url=...) (from cullinan.controller) |
@controller(url=...) (from cullinan.core) |
| Manual service registry | ApplicationContext.refresh() |
# Before (v0.83)
from cullinan.service import service
from cullinan.controller import controller
# After (v0.90)
from cullinan.core import service, controller