Skip to content

Decorators

Decorators add cross-cutting behavior without modifying application components.

from clean_ioc import ContainerBuilder, Tag


class MessageSender:
    def send(self, message: str): ...


class EmailMessageSender(MessageSender):
    def send(self, message: str):
        return f"email:{message}"


class LoggingMessageSender(MessageSender):
    def __init__(self, child: MessageSender):
        self.child = child

    def send(self, message: str):
        print("sending")
        return self.child.send(message)


builder = ContainerBuilder()
builder.register(MessageSender, EmailMessageSender)
logging_id = builder.register_decorator(MessageSender, LoggingMessageSender)
container = builder.build()

The returned ID identifies the decorator definition across every compiled occurrence. The decorated argument is inferred from its service annotation. Missing, ambiguous, and invalid explicit arguments are reported during build(); set decorated_arg="child" to select one explicitly. Typed callable return annotations are also checked against the decorated service when compatibility can be determined.

For policies generated from registered source components, see decorator templates.

Ordering

Treat position as a decorator z-index: higher values are further outside and lower values are closer to the core component.

TRANSACTION_POSITION = 100
RESILIENCE_POSITION = 500
OBSERVABILITY_POSITION = 1000

builder.register_decorator(Service, TransactionDecorator, position=TRANSACTION_POSITION)
builder.register_decorator(Service, RetryDecorator, position=RESILIENCE_POSITION)
builder.register_decorator(Service, MetricsDecorator, position=OBSERVABILITY_POSITION)

Resolution produces MetricsDecorator(RetryDecorator(TransactionDecorator(core))). Equal positions retain registration order from outside to inside. Named constants let independently authored bundles agree on broad layers while still choosing local values within a layer.

Static selection

Use a component predicate with when=:

import clean_ioc.component_filters as cf

builder.register_decorator(
    MessageSender,
    RetryDecorator,
    when=cf.has_tag("network", "remote"),
)

Decorator predicates can inspect generic mappings, parents, and dependency descendants. Every predicate sees the completed undecorated core subtree; dependencies introduced by decorators are excluded from that decision.

when= is the only V2 decorator-selection predicate. V1's registration_filter and decorator_node_filter parameters should be combined into it during migration.

IDs, metadata, and builder customization

Decorators may have their own names and tags for inspection and reusable bundle conventions:

metrics_id = builder.register_decorator(
    Service,
    MetricsDecorator,
    name="service-metrics",
    tags=[Tag("concern", "observability")],
    position=OBSERVABILITY_POSITION,
)

builder.patch_decorator(Service, metrics_id, position=1100)

Use remove_decorator(Service, metrics_id) before build to suppress a definition. A ScopeBuilder may patch or remove an inherited decorator for plans it owns. Existing parent-owned singletons remain anchored to their root activation plan and are not rewired.

Compiled decorator dependencies

Dependencies other than the decorated argument compile like ordinary component edges. The decorator inherits the core component's lifespan and participates in the same cleanup owner.

Decorators may be classes or typed callables. Async decorator activation is supported through resolve_async().

Open-generic policies

Register an open service type once:

from typing import Generic, TypeVar


TCommand = TypeVar("TCommand")


class LoggingHandlerDecorator(Generic[TCommand]):
    def __init__(self, child: CommandHandler[TCommand]):
        self.child = child


builder.register_decorator(CommandHandler, LoggingHandlerDecorator)

The compiler specializes this definition for every closed CommandHandler[T] plan it encounters. This includes handlers supplied by subclass discovery, explicit closed registrations, open or closed factories, and fallback registrations. Generic callable decorators are supported by the same register_decorator() API.

Native and backported type aliases may be used for the decorated service, decorator class, wrapped argument, and callable result annotation. Matching and generic specialization use the expanded canonical types; position ordering and decorator IDs are unchanged.

Graph text lists decorators outside-to-inside and includes their positions. Component inspection and semantic manifests also expose decorator position, name, tags, dependencies, async requirements, and cleanup ownership.