On this page

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๏ƒ

All intermediate examples ยท Level contents ยท Full contents

Canonical page source