Injection#

The decorator, the parameter markers, and the sentinel default.

inject#

@nodrill.inject#
@nodrill.inject(*, from_=None)

Fill missing parameters from the current context at call time.

Parameters:

from_ – Opt into by-name mode. A string name or a class, where every parameter whose name matches an attribute of use(from_) is filled from it.

Raises:

TypeErrorfrom_ is neither a string nor a class, or the decorated object is not callable, or it is a class, a generator or an async-generator function, or a marker annotates *args or **kwargs.

Two styles, usable together on one function.

Marker style annotates individual parameters with FromCtx or from_ctx() and defaults them to injected.

By-name style, @inject(from_="app"), fills every eligible parameter from the attributes of one context object. self and cls are skipped, as are parameters that already carry a marker. In this mode a matching attribute overrides the parameter’s default. A required parameter that neither the caller nor the context supplied raises TypeError naming the parameter and the key.

An argument passed by the caller is never overridden, an explicit None included.

Applies to plain functions, async def coroutines, instance methods, classmethods and staticmethods, in either decorator order. A function with no injectable parameters is returned unwrapped.

The injection plan is built once, at decoration time, from inspect.signature() and typing.get_type_hints(), and compiles into a wrapper that mirrors the function’s own signature. The interpreter binds every call shape natively, whichever way an injected parameter is passed, and a call that misuses the signature fails before any resolution runs, in the interpreter’s own wording. Only the arity range in those messages differs, counting injectable parameters as optional. If hints cannot be resolved at decoration, because a string annotation names something defined later, the plan is deferred to the first call and cached. A name that never resolves raises NameError at call time, naming the function, unless no annotation on it asks for injection at all. A function nodrill has nothing to do for is called through untouched rather than broken over hints only a checker reads. Annotations that do carry a marker have to name things that exist at runtime, so the import cannot sit behind TYPE_CHECKING.

@inject
def report(cfg: FromCtx[Config] = injected) -> str:
    return cfg.url

@inject(from_="app")
def render(user_id: int, theme: str = "light") -> str: ...

FromCtx#

nodrill.FromCtx#

The parameter marker, in subscript form.

FromCtx[SomeClass] marks a parameter as use(SomeClass). At runtime it evaluates to Annotated[SomeClass, FromCtx()], while to a type checker it is SomeClass, so the body and any explicit argument check against the real type.

A union around the marker, FromCtx[SomeClass] | None, still injects, since the union only widens what an explicit argument may be. The same holds for the Optional that typing.get_type_hints() adds by itself on Python 3.10 when a marked parameter defaults to None.

The call form, FromCtx("app"), builds the same marker as from_ctx() but is rejected by pyright, which statically sees an Annotated alias and refuses to call it. Use from_ctx() in pyright-checked code.

from_ctx#

nodrill.from_ctx(key=None, attr=None)#

Build a parameter marker for use inside typing.Annotated.

Parameters:
  • key – A string name, a class, or a ref() naming one. None means the annotated type is the key, which requires that type to be a plain class.

  • attr – The attribute to read off the context object. Defaults to the parameter’s own name for string keys, and for a ref that turns out to name one, which is settled at the first call rather than at decoration.

Raises:

TypeErrorkey is neither a string, a class, a ref, nor None, or a bare marker annotates something that is not a plain class.

Three shapes.

Annotated[Config, from_ctx()]                    # use(Config)
Annotated[Engine, from_ctx("app")]               # use("app").<param name>
Annotated[Engine, from_ctx("app", attr="db")]    # use("app").db

FromCtx[Config] is shorthand for the first. Clean under both mypy and pyright, which is why it is the spelling used throughout these docs.

injected#

nodrill.injected: Any#

The default for an injectable parameter.

Typed Any, so a signature like def f(db: FromCtx[Db] = injected) stays satisfiable for type checkers when callers omit the argument. inject() treats a parameter still bound to it as one to resolve, the same as an omitted one.

If it ever reaches a function body it fails loudly. Attribute access, truthiness, calls, and the protocols a stray value tends to land in next, iteration, len, indexing, in, the orderings, arithmetic, with, await and async for, all raise with a message pointing at a missing @inject or a missing provider. == is left at the default, so containers and identity checks behave normally.

Rejected shapes#

Generator functions, sync and async, raise TypeError at decoration time. A generator body runs after the call, at the first next(), possibly under different providers, so anything resolved at call time would be silently stale. Call use() inside the body instead, and iterate while the provider block is open.

Variadic parameters cannot be injected. A marker on *args or **kwargs raises TypeError, and in by-name mode variadics are skipped.

Classes raise TypeError too. A class is callable, so it would decorate cleanly and then be replaced by a function, breaking isinstance() and subclassing. Decorate __init__ instead.