Add generic typing to Container.make() so type checkers resolve concrete types - #219
Conversation
…te types Container.make() was unannotated, so pyright/basedpyright inferred `(name: Unknown, *arguments: Unknown) -> (Unknown | Any | None)` and every resolved service degraded to Unknown at the call site. Add overloads: a class key returns that class (`make(Foo) -> Foo`), a string key stays `Any` since it carries no static type information. The `| None` in the old inferred return came from `dict.get` on the swaps lookup, not from a reachable code path — make() raises on a missing key — so the typed signature is non-optional and call sites need no narrowing. Also annotate `_instance` / `set_instance` / `instance` so `Container.instance().make(...)` resolves too. Typing-only: no runtime logic changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EohxFkN7w7tuq71KmkUp1B
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Arbitration: PR #219 vs #221 — KEEP #219Judged on correctness only. Evidence below; all commands re-run locally. 1. No undisclosed runtime change (cleared for both PRs)
raise MissingContainerBindingNotFound("{0} key was not found in the container".format(name))and dispatches on a membership check ( The On the "bound 2. Typing-only: confirmedNo edits to the bodies of 3.
|
Verification detailType checker: before / after
Sample call site used for the measurement: # pyright: strict
from fastapi_startkit.application import app
from fastapi_startkit.container import Container
class MyService:
def greet(self) -> str:
return "hi"
def use() -> None:
c = Container()
svc = c.make(MyService)
reveal_type(svc)
cfg = c.make("config")
reveal_type(cfg)
reveal_type(app().make)
The "before" run reproduced the exact reported diagnostic: Full-repo pyright
30 pre-existing errors fixed as a side effect, none introduced. The remaining 622 are pre-existing and unrelated to Typing assertionsThis is what actually validates the pytest + coverage
Container suite on its own: Note: the task brief quoted LintDownstream call sitesAll Note on
|
Problem
Container.make()was unannotated, so pyright/basedpyright inferred:That fired on every
app.make(...)call site, and every resolved service degraded toUnknown— no completion, no checking downstream.Change (typing-only)
Container.make()now has two overloads:make(SomeClass)→SomeClassmake("config")/make("db")→Any— a string key carries no static type information, so callers keep annotating the binding themselves (config: Config = app.make("config")). This is what every existing call site already does, so there is no churn.Also annotated
_instance/set_instance()/instance(), soContainer.instance().make(...)resolves as well (previouslyinstance()inferredNonefrom_instance = None).On the
| Nonein the old return typeDeliberately dropped. It came from
self.swaps.get(name)—dict.getisOptional— inside a branch already guarded byname in self.swaps, so it was never reachable asNone.make()raisesMissingContainerBindingNotFoundon a miss rather than returningNone. KeepingOptionalwould have forced anassertor narrowing at every call site for a value that cannot beNone; not keeping it costs nothing.Applicationinheritsmake()fromContainerand does not override it, so it picks up the overloads automatically. No.pyistub mirrors themakesignature (Hash.make/Response.make_headersare unrelated facade methods), so no stubs needed updating.No runtime logic changed — the diff is imports, a TypeVar, overloads, annotations and a docstring.
Verification
pyright, sample call site (
c.make(MyService),c.make("config")),# pyright: strict:svcUnknown | Any | NoneMyServicecfgUnknown | Any | NoneAnymake(name: Unknown, *arguments: Unknown) -> (Unknown | Any | None)Overload[(name: type[T], *arguments: Any) -> T, (name: str, *arguments: Any) -> Any]reportUnknownMemberTypeFull-repo pyright (
uv run pyright, project config): 652 errors → 622 errors, warnings unchanged at 113. 30 pre-existing errors fixed, none introduced.Tests:
uv run pytest --ignore=tests/masoniteorm/postgres --cov→ 2192 passed, 7 skipped, coverage 84.40% (threshold 80).New:
tests/core/test_container_typing.py—assert_type()assertions formake(Class), unboundmake(Class),make("str")andContainer.instance(), each paired with a runtime assertion. Statically verified withuv run pyright tests/core/test_container_typing.py→ 0 errors.Downstream
example/andapplication/call sites all use string keys, so none are affected.🤖 Generated with Claude Code
https://claude.ai/code/session_01EohxFkN7w7tuq71KmkUp1B