Looking services up¶
Services are requested with
get() — by type, by name,
or both. find() is the
non-raising variant, and
get_all() returns every
matching service.
from action0.service import Registry
class Database:
def __init__(self, dsn: str = "sqlite://"):
self.dsn = dsn
class Postgres(Database):
pass
registry = Registry()
registry.register(Postgres, name="pg")
registry.get("pg") # by name
registry.get(Database) # by type — subclass-aware
registry.get(Database, name="pg") # typed and named
Type lookups are subclass-aware¶
A type request matches every registration whose provided type is the
requested type or a subclass of it. Registering Postgres therefore
also answers get(Database) — the same substitution rule the type
checker applies to your code.
When several registrations match, the registry picks a winner in this order:
If exactly one candidate is marked default, it wins. Unnamed registrations are default implicitly; a named one can opt in with
default=True.Otherwise, if exactly one candidate (among the defaults, if there are several) provides the requested type exactly — not a subclass — it wins.
Otherwise the request is ambiguous and
AmbiguousServiceErroris raised, listing the candidates.
Ambiguity is deliberately an error rather than a guess: request the
service by name, or mark exactly one candidate with default=True.
Structural lookups: protocols¶
A runtime-checkable typing.Protocol can be requested like
any class. The match is structural: every registration whose provided
type satisfies the protocol is a candidate — no inheritance required.
Winner selection and ambiguity work exactly as for nominal lookups, and
protocol annotations on constructor parameters
inject the same way.
from typing import Protocol
from typing import runtime_checkable
@runtime_checkable
class Speaker(Protocol):
def speak(self) -> str: ...
class Dog: # no Speaker base class anywhere
def speak(self) -> str:
return "woof"
registry = Registry()
registry.register(Dog)
registry.get(Speaker).speak() # 'woof'
A protocol also works as the registration type:
register(Dog, provides=Speaker) verifies that Dog satisfies the
protocol and files the service under it, and
register_instance(dog, provides=Speaker) does the same for a
ready-made object.
Two protocol shapes cannot be matched structurally; both are reported as explicit errors rather than silent misses:
A protocol without
typing.runtime_checkable()refusesissubclasschecks entirely — looking one up raisesServiceErrortelling you to add the decorator.A protocol with non-method members (say
dsn: str) supportsisinstancebut notissubclass, so it cannot be matched against other registrations — such a lookup raisesServiceErrortoo. It can still be served:register_instance(obj, provides=ThatProtocol)verifies the object withisinstance(which does check data members), and that registration answers requests for exactly that protocol, or by name.
Name lookups¶
get("name") returns the service registered under that exact name —
no type involved. The combined form get(Type, name="name") resolves
by name first and then verifies the result provides the requested type,
raising ServiceNotFoundError when
it does not. Use it when you want both the disambiguation of a name and
the type safety of a typed request (it also gives type checkers the
correct return type).
find: absence is not an error¶
find() returns None
where get would raise
ServiceNotFoundError — for optional
integrations:
if (metrics := registry.find("metrics")) is not None:
metrics.increment("boot")
Ambiguity still raises: an ambiguous request is a configuration problem, not an absence.
get_all: every matching service¶
get_all() returns one
instance per matching definition — parent registrations first, then
local ones, in registration order. This is the plugin pattern: register
several handlers under different names, collect them all by their
common base type:
class Exporter:
pass
class CsvExporter(Exporter):
pass
class JsonExporter(Exporter):
pass
plugins = Registry()
plugins.register(CsvExporter, name="csv")
plugins.register(JsonExporter, name="json")
[type(e).__name__ for e in plugins.get_all(Exporter)]
# ['CsvExporter', 'JsonExporter']
Introspection¶
A registry behaves like a small collection of its own definitions
(parents excluded): Type in registry / "name" in registry test
whether a lookup would find something, len(registry) counts
definitions, iteration and
definitions() yield the
Definition objects in
registration order.