Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion injector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,23 @@ def provider(function: CallableT) -> CallableT:
return function


def named_provider(name: str) -> CallableT:
"""Decorator for :class:`Module` methods, creating annoted type a of a type.

>>> class MyModule(Module):
... @named_provider("first")
... def provide_name(self) -> str:
... return 'Bob'

"""

def decorator(function: CallableT):
_mark_named_provider_function(function, name, allow_multi=False)
return function

return decorator
Comment on lines +1370 to +1384
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that this alternative way of declaring a provider is worth the extra maintenance it introduces. Do you have any arguments behind introducing it?

Copy link
Contributor Author

@SatwikcoolAgrawal SatwikcoolAgrawal Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for adding this was to replicate the @Named annotated functionality from Guice — allowing us to bind and inject a specific instance among multiple instances of the same interface.
Also, considering how @multiprovider works, I wanted to avoid using a dictionary lookup pattern to retrieve the instance by key.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I'm still not convinced though.

It has been a while since Java was my primary language, but isn't the presence of @Named in Guice motivated by the absence of a language supported way of declaring annotated types? Since that is supported natively in Python, I think this only adds a less intuitive way to declare the same thing.

I however still happy to merge the other fix in this PR if you're willing to split it.

Copy link
Contributor Author

@SatwikcoolAgrawal SatwikcoolAgrawal Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the only purpose this would serve is to replicate the same pattern used in Guice, which can already be achieved using Annotated. So, there isn’t much of a use case for adding this.
We can close this PR.

You can check the split PR created for the other fix here:
#286

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!



def multiprovider(function: CallableT) -> CallableT:
"""Like :func:`provider`, but for multibindings. Example usage::

Expand All @@ -1390,7 +1407,7 @@ def provide_strs_also(self) -> List[str]:
def _mark_provider_function(function: Callable, *, allow_multi: bool) -> None:
scope_ = getattr(function, '__scope__', None)
try:
annotations = get_type_hints(function)
annotations = get_type_hints(function, include_extras=True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what allows for a @provider to return things like Annotated[str, 'first']?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, adding include_extras=True ensures that metadata inside Annotated types is preserved when fetching type hints. Without it, only the base type (e.g., str) was returned, causing the internal _punch_through_alias check for _AnnotatedAlias to never trigger.

Example:

from typing import Annotated, get_type_hints

def f() -> Annotated[str, 'first']:
    ...

print(get_type_hints(f))
# {'return': <class 'str'>}

print(get_type_hints(f, include_extras=True))
# {'return': typing.Annotated[str, 'first']}

The internal logic in _punch_through_alias already handles checking for _AnnotatedAlias:

elif isinstance(type_, _AnnotatedAlias) and getattr(type_, '__metadata__', None) is not None:
    return type_.__origin__

but the metadata was being stripped before, so this change fixes that.

except NameError:
return_type = '__deferred__'
else:
Expand All @@ -1399,6 +1416,20 @@ def _mark_provider_function(function: Callable, *, allow_multi: bool) -> None:
function.__binding__ = Binding(return_type, inject(function), scope_) # type: ignore


def _mark_named_provider_function(function: Callable, name: str, *, allow_multi: bool) -> None:
scope_ = getattr(function, '__scope__', None)
try:
annotations = get_type_hints(function, include_extras=True)
except NameError:
return_type = '__deferred__'
else:
raw_return_type = annotations['return']
return_type = Annotated[raw_return_type, name]

_validate_provider_return_type(function, cast(type, return_type), allow_multi)
function.__binding__ = Binding(return_type, inject(function), scope_)


def _validate_provider_return_type(function: Callable, return_type: type, allow_multi: bool) -> None:
origin = _get_origin(_punch_through_alias(return_type))
if origin in {dict, list} and not allow_multi:
Expand Down
33 changes: 33 additions & 0 deletions injector_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
Error,
UnknownArgument,
InvalidInterface,
named_provider,
)


Expand Down Expand Up @@ -2032,3 +2033,35 @@ class MyClass:
injector = Injector([configure])
instance = injector.get(MyClass)
assert instance.foo == 123


def test_module_provider_with_annotated():
class MyModule(Module):
@provider
def provide_first(self) -> Annotated[str, 'first']:
return 'Bob'

@provider
def provide_second(self) -> Annotated[str, 'second']:
return 'Iger'

module = MyModule()
injector = Injector(module)
assert injector.get(Annotated[str, 'first']) == 'Bob'
assert injector.get(Annotated[str, 'second']) == 'Iger'


def test_module_named_provider():
class MyModule(Module):
@named_provider('first')
def provide_first(self) -> str:
return 'Bob'

@named_provider('second')
def provide_second(self) -> str:
return 'Iger'

module = MyModule()
injector = Injector(module)
assert injector.get(Annotated[str, 'first']) == 'Bob'
assert injector.get(Annotated[str, 'second']) == 'Iger'