Constructor, explicit, and collection DI๏
Beginner โ Intermediate. Annotations, explicit targets, collections.
Annotate a constructor parameter. Melder builds the graph.
What an annotation selects depends on what it is. A class selects the spells of that type โ a class
bound bare, or an existing object of that class โ and nothing else: a category that happens to share the
class's name does not turn its members into providers. A Protocol selects the spells bound under it as
their contract. A TYPE_CHECKING-only import, which leaves a string at runtime, resolves by name to a
type or a contract, never to a string category. list[...] gathers the group the annotation's kind names:
every spell of the class, every implementer of the Protocol, or โ for a string โ every member of the
category or type of that name.
import melder as md
class Database:
"""A small stand-in for a database; this example performs no I/O."""
def query(self) -> str:
return "42 rows"
class ReportService:
def __init__(self, database: Database) -> None: # โ injected
self.database = database
def run(self) -> str:
return "report over " + self.database.query()
book = md.Spellbook()
book.bind(spell=Database, existence="unique")
book.bind(spell=ReportService, existence="unique")
with book.conjure() as conduit:
report: ReportService = conduit.meld("ReportService")
assert isinstance(report.database, Database)
print(report.run())
This complete example prints report over 42 rows: melding the consumer first
constructs its registered dependency and injects it into the constructor.
When an annotation isn't specific enough โ two implementations of one shape, or
a named binding โ declare the target with SpellMap as the parameter default:
Excerpt. PrimaryStore must already be defined and registered in this book.
class Consumer:
def __init__(self, store=md.SpellMap(PrimaryStore)) -> None:
self.store = store
SpellMap takes the full address: a concrete spell, a spellframe, a
binding_name, or a combination. It obeys an exactly-one law โ a target
that matches zero spells or several fails at conjure(), not at runtime.
When a single Protocol-annotated parameter has several registered implementations,
conjure() refuses with an AMBIGUOUS_PROVIDER
report that lists every candidate's address and the fix โ a SpellMap
default, a meld override, or one provider โ never a guess.
Registering the same class twice is instead refused as a duplicate spell identity
at registration; another binding name does not make it a different spell.
Ask for a list and get every implementation โ the plugin pattern in one annotation:
Excerpt. Define the Handler Protocol and both implementing classes first;
bind the dispatcher as well before conjuring. The complete
collection-injection lesson
includes those definitions and prints ['email', 'sms'] from its injected handlers.
class Dispatcher:
def __init__(self, handlers: list[Handler]) -> None: # โ all of them
self.handlers = handlers
book.bind(spell=EmailHandler, existence="unique",
spellframe=Handler, binding_name="email")
book.bind(spell=SmsHandler, existence="unique",
spellframe=Handler, binding_name="sms")
book.bind(spell=Dispatcher, existence="unique")
Runnable examples๏
Constructor di by annotation โ Intermediate 15
Spellmap defaults โ Intermediate 16
Collection di โ Intermediate 17
All intermediate examples ยท Level contents ยท Full contents