YAML service definitions¶
Whole registries can be defined in YAML — wiring as configuration,
without a rebuild. Loading requires PyYAML (install
action0-service[yaml]):
registry.load_yaml("conf/services.yaml")
registry.validate() # catch config mistakes at boot, not at first use
load_yaml() accepts a file
path or an open text stream, registers every definition in document
order, and returns the created
Definition objects. Pass
replace=True to overwrite colliding registrations — e.g. to layer a
site-specific file over a default one.
The format¶
The document is a mapping of service names to definitions. Within a definition, a handful of reserved keys configure the registration — every other key is a constructor parameter:
mailer.bulk:
factory: myapp.mail.SmtpMailer # dotted path — required
scope: singleton # optional (default: singleton)
provides: myapp.mail.Mailer # optional, defaults to the class
default: true # optional: wins type lookups
eager: false # optional: built by warmup()
api_key: !ENV ${MAILER_KEY} # everything else: init params
db: !ref database # inject another service by name
retry_policy: # nested mapping with "factory":
factory: myapp.util.Retry # built fresh as an anonymous object
attempts: 3
factoryThe dotted path of the class or factory callable, required. The longest importable module prefix is imported and the rest resolved with
getattr, so nested classes work.scopeA scope key as a string:
singleton(the default),transient,thread,context, or a custom scope registered on the target registry.providesDotted path of the type to register under; defaults to the factory class itself (or a plain factory function’s return annotation).
default,eagerBooleans, both
falseby default — whether the definition wins ambiguous type lookups, and whetherwarmup()builds it. Unlike programmatic registration, YAML services are always named (their mapping key), so none is a default implementation unless marked.profilesA string or list of strings limiting the definition to those profiles; it is only visible in registries whose active profiles intersect. YAML mapping keys must be unique, so dev/prod variants of the same service name live in separate files (or separate
load_yaml()calls) — loading both into one registry works when their profiles are disjoint.paramsAn escape hatch: a mapping passed through as constructor parameters without reserved-key screening — for constructors whose parameter is itself called
factory,scope, and so on. Everything underparamsbehaves exactly like a flat parameter key; declaring the same parameter both flat and underparamsis an error.
Constructor parameters that are missing from the YAML follow the normal injection rules — annotated dependencies are still resolved from the registry.
!ENV — environment substitution¶
The !ENV tag substitutes ${VAR} and ${VAR:-fallback} inside a
scalar from the process environment, at load time. A variable
without fallback that is not set raises
DefinitionError.
Substitution is purely textual and the result is always a string —
it is not re-parsed as YAML, so a secret like yes or 0123 cannot
change type behind your back. If the constructor wants an int,
convert in the constructor or use a factory.
database:
factory: myapp.db.Database
dsn: !ENV ${DATABASE_DSN:-sqlite://}
!ref — service references¶
!ref name injects the service registered under name, resolved at
build time — the referenced service may be defined later in the
file, in another file, or programmatically. It is the YAML spelling of
Ref and works nested inside lists
and mappings.
Anonymous nested factories¶
A mapping inside the parameters that contains a factory key becomes
an anonymous, unregistered definition: it is built fresh (transient)
every time the owning service is built, and its own parameters follow
the same rules. Inside nested factories only factory and params are
reserved. Use them for helper objects that don’t deserve a registry
entry of their own — like the retry_policy above.
Anchors, merges, and templates¶
Standard YAML anchors and merge keys work as usual for sharing configuration between definitions. Entries whose key starts with a dot are templates: parsed, so their anchors can be referenced — but not registered:
.mailer: &mailer
factory: myapp.mail.SmtpMailer
timeout: 30
mailer.bulk:
<<: *mailer
api_key: !ENV ${BULK_MAIL_KEY}
mailer.newsletter:
<<: *mailer
api_key: !ENV ${NEWSLETTER_KEY}
Lazy loading¶
By default every factory and provides path is imported while the
file loads. For a large file that can mean importing your whole
application at boot even though most services are never used in a given
process. lazy=True defers the imports:
registry.load_yaml("conf/services.yaml", lazy=True)
Loading then imports nothing; each definition resolves its dotted paths on first use instead:
Building the service — a by-name
get("db")imports only that service’s factory (plus, transitively, its dependencies), nothing else.Type-based lookups — resolving by type needs the real
providestypes, so the first type query (aget(SomeType), injection by annotation,get_all, …) imports all still-lazy definitions of the consulted registry layer.validate()imports everything — a broken dotted path becomes a validation problem naming the service, so calling it at boot keeps the fail-fast behavior while still skipping unused imports in processes that don’t validate.warmup()imports and builds theeagerdefinitions.
Nested anonymous factories stay lazy too: their paths are imported when
the owning service is built. Without validate(), an unimportable path
surfaces as DefinitionError at
first use.
Security¶
Parsing uses a yaml.SafeLoader subclass, so documents
cannot instantiate arbitrary Python objects during parsing — but
factory paths are imported and called when services are built.
Only load files you trust.