From a Jan 10 conversation
tl;dr Rewrite and refactor decorator and injector support to support function DI and clarify usage
Generic Decorator
We want a decorator that can register factory functions as well as dataclasses. Along the way, we want clean up naming and allow overriding how this “creator” happens.
Say you have the following:
def login_factory(container):
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
registry.register_factory(login_factory, LoginService)
We’d like, as the explicit case for a decorated registration:
@wired_factory(for_=LoginService, creator=wired_function_creator)
def login_factory(container):
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
The changes:
- We no longer have to write a
registry.register_function line
- The name
factory gets expanded to wired_factory to make it explicit
- It turns the result of the
creator callable into a wired factory
- The
creator callable can now handle function-based services instead of just dataclass-based services, through use of specific "creators"
Or, a shorthand notation:
@wired_factory()
def login_factory(container) -> LoginService:
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
In this case:
for_ is deduced from the type annotation for the return type
creator is deduced from the wrapped target…if it is a function, we use wired_function_creator
Of course you can use an imperative form. In fact, we already have one: registry.register_factory. We could handle the explicit case above:
def login_factory(container):
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
registry.register_factory(login_factory, LoginService, creator=wired_function_creator)
In existing cases, we don’t need a creator. There’s no prep work for the callable. However, if we change to support DI on functions, we’ll want a creator when registering functions.
Here is a shorthand version of this imperative form:
def login_factory(container) -> LoginService:
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
registry.register_factory(login_factory)
Again, this version:
- Looks at the argument
login_factory and sees it is a function, thus uses creator=wired_function_creator
- Looks at the type annotation return value of
login_factory and uses that type as the second argument
The Calling Pattern
This change makes the two-step dance more explicit.
creator is a callable that does some work and returns a callable…yes, it is a factory factory
- That returned callable is registered
It also makes the two-step dance extensible. I have wanted a “custom injector”. The injector is, in essence, the factory factory. In this case:
@wired_factory(creator=wired_dataclass_creator)
@dataclass
class Greeter:
settings: Settings
name: str = 'Mary'
def __call__(self, customer):
punctuation = self.settings.punctuation
return f'Hello {customer} my name is {self.name}{punctuation}'
Thus wired_dataclass_creator is the “injector”. That is, it returns a callable that, when called, will construct the dataclass instance using DI.
If one wanted to customize the injection process, one could make a variation of wired_dataclass_creator. One could automate/hide usage of it by making a custom decorator.
Function DI
Above we discussed:
def login_factory(container):
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
registry.register_factory(login_factory, LoginService, creator=wired_function_creator)
At first glance this seems unnecessary: the wired_function_creator callable would do nothing more than just return login_factory.
But what if we wanted to do DI on functions? For example:
def login_factory(dbsession: Session):
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
registry.register_factory(login_factory, LoginService, creator=wired_function_creator)
In this case, we do need a factory-factory callable registered as the service. wired_function_creator would look at the login_factory signature and see if it wanted just the container. If so, it would immediately return login_factory and mimic current behavior.
Otherwise, it would presume this function wanted DI. It would return a lambda or function that, during service lookup/construction, would be handed the container and do the DI dance.
This could be simplified in the imperative form:
def login_factory(dbsession: Session) -> LoginService:
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
registry.register_factory(login_factory)
…and the decorated form:
@wired_factory()
def login_factory(dbsession: Session) -> LoginService:
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
Extensible Creators
These creator callables are important. They might also be custom. They are a perfect place to experiment with app-specific or site-specific policies (for example, caching.)
In the case of explicitly naming the creator to use, in the decorator or imperative form, this is already handled. Call the creator you want in that case.
Or is it? Perhaps you want a general case for the creator, but have some context-specific cases as exception. For this, one should treat “creator” as itself a service:
def login_factory(dbsession: Session):
dbsession = container.get(name='dbsession')
return LoginService(dbsession)
registry.register_factory(login_factory, LoginService, creator=WiredFunctionCreator)
With this, WiredFunctionCreator is a service in the registry which will return the correct creator for this situation. You then have access to wired’s extension/customization/overriding patterns:
- Register a creator for a particular context
- Register a creator for a particular name
- Use ordering (whichever registration happens last) to allow overriding
This would mean wired would need to “scan” for creators before doing a scan for factories. We could make easier by having a basic registry usage, where this was done all in one step, or a more-advanced two-step process of setting up a registry.
Injector Service
With the above, injection is part of the creator step. Albeit a big, hairy part. People might want a custom creator, but not touch the injector. Or vice versa. People might want a different injector for a specific service.
Thus, behind the scenes, creator=WiredFunctionCreator will lookup a WiredFunctionInjector when it wants to do the injection part. The context will the for_ the creator is being used to construct.
Along the way, the current injector will be refactored to make each lookup more granular, to make it easier to write your own injector by re-using pieces of the existing injector. It will also be easier to test (all lookups are currently inlined in a big for loop.)
From a Jan 10 conversation
tl;dr Rewrite and refactor decorator and injector support to support function DI and clarify usage
Generic Decorator
We want a decorator that can register factory functions as well as dataclasses. Along the way, we want clean up naming and allow overriding how this “creator” happens.
Say you have the following:
We’d like, as the explicit case for a decorated registration:
The changes:
registry.register_functionlinefactorygets expanded towired_factoryto make it explicitcreatorcallable into a wired factorycreatorcallable can now handle function-based services instead of just dataclass-based services, through use of specific "creators"Or, a shorthand notation:
In this case:
for_is deduced from the type annotation for the return typecreatoris deduced from the wrapped target…if it is a function, we usewired_function_creatorOf course you can use an imperative form. In fact, we already have one:
registry.register_factory. We could handle the explicit case above:In existing cases, we don’t need a creator. There’s no prep work for the callable. However, if we change to support DI on functions, we’ll want a creator when registering functions.
Here is a shorthand version of this imperative form:
Again, this version:
login_factoryand sees it is a function, thus usescreator=wired_function_creatorlogin_factoryand uses that type as the second argumentThe Calling Pattern
This change makes the two-step dance more explicit.
creatoris a callable that does some work and returns a callable…yes, it is a factory factoryIt also makes the two-step dance extensible. I have wanted a “custom injector”. The injector is, in essence, the factory factory. In this case:
Thus
wired_dataclass_creatoris the “injector”. That is, it returns a callable that, when called, will construct the dataclass instance using DI.If one wanted to customize the injection process, one could make a variation of
wired_dataclass_creator. One could automate/hide usage of it by making a custom decorator.Function DI
Above we discussed:
At first glance this seems unnecessary: the
wired_function_creatorcallable would do nothing more than just returnlogin_factory.But what if we wanted to do DI on functions? For example:
In this case, we do need a factory-factory callable registered as the service.
wired_function_creatorwould look at thelogin_factorysignature and see if it wanted just the container. If so, it would immediately returnlogin_factoryand mimic current behavior.Otherwise, it would presume this function wanted DI. It would return a lambda or function that, during service lookup/construction, would be handed the container and do the DI dance.
This could be simplified in the imperative form:
…and the decorated form:
Extensible Creators
These creator callables are important. They might also be custom. They are a perfect place to experiment with app-specific or site-specific policies (for example, caching.)
In the case of explicitly naming the creator to use, in the decorator or imperative form, this is already handled. Call the creator you want in that case.
Or is it? Perhaps you want a general case for the creator, but have some context-specific cases as exception. For this, one should treat “creator” as itself a service:
With this,
WiredFunctionCreatoris a service in the registry which will return the correct creator for this situation. You then have access to wired’s extension/customization/overriding patterns:This would mean wired would need to “scan” for creators before doing a scan for factories. We could make easier by having a basic registry usage, where this was done all in one step, or a more-advanced two-step process of setting up a registry.
Injector Service
With the above, injection is part of the
creatorstep. Albeit a big, hairy part. People might want a custom creator, but not touch the injector. Or vice versa. People might want a different injector for a specific service.Thus, behind the scenes,
creator=WiredFunctionCreatorwill lookup aWiredFunctionInjectorwhen it wants to do the injection part. The context will thefor_the creator is being used to construct.Along the way, the current injector will be refactored to make each lookup more granular, to make it easier to write your own injector by re-using pieces of the existing injector. It will also be easier to test (all lookups are currently inlined in a big for loop.)