injector icon indicating copy to clipboard operation
injector copied to clipboard

[question] string-based annotations? equivalent to guice Names.named() and @Named

Open benjaminjbachman opened this issue 5 years ago • 3 comments

I know it's generally better to use static annotations (eg https://github.com/alecthomas/injector/issues/69), but I found @Named annotations very useful in guice for configuration injection. I'm able to reproduce this feature using type but did I miss an existing implementation in the package? didn't want to reinvent the wheel, even if its only two lines of code.

from injector import Module, Injector, Binder, inject
from dataclasses import dataclass
from collections import defaultdict


def named(name: str, *, seen=defaultdict(lambda: type("", (), {}))):
    return seen[name]


@inject
@dataclass
class MyClass:
    myval: named("mine")
    myotherval: named("somethingelse")


def conf(binder: Binder):
    binder.bind(named("mine"), to="test1")
    binder.bind(named("somethingelse"), to="test2")


myclass: MyClass = Injector([conf]).get(MyClass)
print(myclass.myval)
print(myclass.myotherval)

From here I'd write a module that parses a config file and does binder.bind(named(key),to=value) to make it easy for classes to get values from the config to an injected class.

benjaminjbachman avatar Jan 22 '20 02:01 benjaminjbachman

No, you're right, something like this is not supported at the moment.

The closest you can get right now is with type aliases I think:


Mine = NewType('Mine', str)
SomethingElse = NewType('SomethingElse', str)

@inject
@dataclass
class MyClass:
    myval: Mine
    myotherval: SomethingElse


def conf(binder: Binder):
    binder.bind(Mine, to="test1")
    binder.bind(SomethingElse, to="test2")

# ...

Granted, the types/aliases/names still need to be declared statically up front. If you want to be completely dynamic in config reading I think your other best choice is to have provider methods that receive configuration and construct your classes:

class MyModule(Module):
    @provider
    def provide_myclass(self, config: Config) -> MyClass:
        return MyClass(config['mine'], config['something_else'])

In both of those cases you don't lose type safety if you use any linters that test it, so there's that. :)

Injector has initial support for PEP 593 -- Flexible function and variable annotations so something like Guice's @Named could be implemented now, just no one did it yet.

jstasiak avatar Jan 22 '20 10:01 jstasiak

See https://github.com/alecthomas/injector/issues/174 for a potential (not supported at the moment) way to use Annotated to achieve this.

jstasiak avatar Jan 05 '21 14:01 jstasiak

@jstasiak, just a nit-pick. technically NewType isn't a type alias, it creates a new, distinct type https://docs.python.org/3/library/typing.html#newtype. It's what meta-classes sometimes use under the hood and it's roughly equivalent to what defining a class does under the hood

The following are roughly equivalent (including how it sets the parent class pointer)

class Mine(str): ...

Mine = NewType("Mine", str)

A type alias would be defined as Mine = str https://docs.python.org/3/library/typing.html#type-aliases

strangemonad avatar Mar 22 '23 18:03 strangemonad