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.
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:
That is fine for the two-or-three-cache example in the docs. Our collections are bigger:
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'sIEnumerable<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:
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=Tregistrations without a qualifier, since the collection already distinguishes them positionally.container.get(T)with several unqualified candidates would still have to raise (ambiguous), butSequence[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 distinctas_member_of=) would achieve the same without touching howas_typebehaves 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.