Filtering
Filtering controls which registration is selected when multiple registrations match a service type.
from clean_ioc import Container, DependencySettings, Lifespan, RemoveDependencySetting, Tag
from clean_ioc.registration_filters import has_tag, with_id, with_name
import clean_ioc.node_filters as nf
Visual model
Filtering happens during registration selection inside resolution. At a high level:
flowchart TD
A[Resolve service type] --> B[Collect registrations]
B --> C[Apply default or custom registration filter]
C --> D[Apply parent_node_filter checks]
D --> E[Take first match from registration order]
E --> F[Build instance and continue resolving children]
Default behavior
By default, resolve(...) uses unnamed registrations only.
container = Container()
container.register(int, instance=1)
container.register(int, instance=2, name="Two")
print(container.resolve(int)) # 1
print(container.resolve(int, filter=with_name("Two"))) # 2
Selection order is LIFO (last registration checked first):
flowchart LR
R3[Register named int Two] --> Q
R2[Register unnamed int one] --> Q
Q[Resolve int using default filter] --> U[Unnamed registrations only]
U --> Pick[Pick unnamed registration]
Registration ID filters
register(...) returns the registration's unique ID. Use with_id(...) when that exact registration must be selected:
container = Container()
registration_id = container.register(str, instance="selected")
value = container.resolve(str, filter=with_id(registration_id))
print(value) # selected
Patching a registration
Patch a registration before its first resolution when configuration needs to be layered after registration. The service type and registration ID identify the registration; dependency settings, lifespan, and tags are the only mutable fields.
class Client:
def __init__(self, endpoint: str = "http://localhost", timeout: int = 5):
self.endpoint = endpoint
self.timeout = timeout
client_registration_id = container.register(
Client,
dependency_config={"endpoint": "https://old.example", "timeout": 30},
tags=[Tag("environment", "development")],
)
container.patch_registration(
Client,
client_registration_id,
dependency_config={"endpoint": RemoveDependencySetting},
lifespan=Lifespan.singleton,
tags=[Tag("environment", "production")],
)
client = container.resolve(Client)
print(client.endpoint) # http://localhost
print(client.timeout) # 30
Dependency configuration is shallow-merged by parameter name. RemoveDependencySetting restores normal injection or a declared default for that parameter. Tags are merged by name, with later values replacing earlier ones. Patching after the registration has created an instance raises RuntimeError; a type/ID pair not owned by that scope raises KeyError.
Name and tag filters
container = Container()
container.register(str, instance="prod", tags=[Tag("env", "prod")])
container.register(str, instance="dev", tags=[Tag("env", "dev")])
value = container.resolve(str, filter=has_tag("env", "dev"))
print(value) # dev
Top-down filtering with dependency_config
Top-down means the parent registration constrains which child registration is allowed for a given argument.
flowchart TD
G[Greeter registration] --> M[Dependency config for message]
M --> F1[Filter by name hello]
F1 --> S1[Select string registration named hello]
class Greeter:
def __init__(self, message: str):
self.message = message
container = Container()
container.register(str, instance="Hello", name="hello")
container.register(str, instance="Goodbye", name="bye")
container.register(
Greeter,
name="hello_greeter",
dependency_config={"message": DependencySettings(filter=with_name("hello"))},
)
container.register(
Greeter,
name="bye_greeter",
dependency_config={"message": DependencySettings(filter=with_name("bye"))},
)
print(container.resolve(Greeter, filter=with_name("hello_greeter")).message)
print(container.resolve(Greeter, filter=with_name("bye_greeter")).message)
Bottom-up filtering with parent_node_filter
Bottom-up means the child registration decides if it is eligible by inspecting the current parent node.
flowchart TD
P[Resolve NeedsB] --> AReq[Needs dependency A]
AReq --> RB[Registration mapping A to B with parent filter NeedsB]
AReq --> RC[Registration mapping A to C with parent filter NeedsC]
RB --> OK[Filter passes]
RC --> NO[Filter fails]
OK --> BInst[Build B instance]
class A:
pass
class B(A):
pass
class C(A):
pass
class NeedsB:
def __init__(self, a: A):
self.a = a
class NeedsC:
def __init__(self, a: A):
self.a = a
container = Container()
container.register(A, B, parent_node_filter=nf.implementation_type_is(NeedsB))
container.register(A, C, parent_node_filter=nf.implementation_type_is(NeedsC))
container.register(NeedsB)
container.register(NeedsC)
print(type(container.resolve(NeedsB).a).__name__) # B
print(type(container.resolve(NeedsC).a).__name__) # C