Skip to content

Collection injection requires a unique qualifier per implementation, which does not scale past a handful #145

Description

@Max-Moro

First: thanks for shipping collection injection in 2.12.0 — it replaces a hand-rolled index in our project, and the Sequence[T] shape is exactly right.

This is about one ergonomic constraint that shows up once a collection gets large.

The constraint

Every implementation beyond the first needs its own unique qualifier:

@injectable(as_type=Handler)                      # ok — the default one
class A(Handler): ...

@injectable(as_type=Handler)                      # DuplicateServiceRegistrationError
class B(Handler): ...

@injectable(as_type=Handler, qualifier="b")       # ok
class B(Handler): ...

That is fine for the two-or-three-cache example in the docs. Our collections are bigger:

collection implementations
AST node handlers 27
pipe stages 21
template plugins 16

For the 27-member one, that is 26 hand-written qualifier strings whose only job is to be distinct. They are not checked by a type checker, they carry no meaning, and nothing reads them back — the consumer indexes the collection by type(instance).

For comparison, neither Spring's List<T> nor .NET's IEnumerable<T> asks for a name; registering two implementations against the same interface is the normal way to build a collection there.

Why this is not just cosmetic

The qualifier is the only thing distinguishing members, so it becomes load-bearing metadata that exists solely to satisfy the registration mechanism. A duplicate is caught at container build (good), but a typo still costs a debugging round-trip, and the strings have to stay unique across modules that never reference each other.

Workaround (works today)

A decorator that derives the qualifier from the class name:

def handler(lifetime="singleton"):
    def deco(cls):
        return injectable(as_type=Handler, qualifier=cls.__name__, lifetime=lifetime)(cls)
    return deco

This is what we are adopting, and it is fine — but it suggests the qualifier is doing no work in this use case.

Suggested direction

Allow multiple as_type=T registrations without a qualifier, since the collection already distinguishes them positionally. container.get(T) with several unqualified candidates would still have to raise (ambiguous), but Sequence[T] / Mapping[Hashable, T] would not need to.

If that is too disruptive to the current interface semantics, an opt-in marker (collection=True, or a distinct as_member_of=) would achieve the same without touching how as_type behaves today.

Context

I do not want to overstate the case: the workaround above is cheap and we are unblocked either way. Filing it because "how many implementations is this shape meant for" seemed worth a data point from a real consumer, and 27 was enough for the constraint to become visible.

Happy to open a PR if a direction appeals to you.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions