Factory is deprecated but stateful factory docs still uses it
Closed this issue · 6 comments
In the docs on deprecated factory you have an example starting after the words: "It’s also possible to have a stateful factory using a class."
However, the example still uses @factory
. I'm using a stateful scope handler (from you) which I'm trying to migrate but can't see how:
@factory(scope=VISIT_SCOPE)
class VisitHandler:
def __init__(self):
self.__visit = None
def __call__(self) -> Visit:
assert self.__visit is not None
return self.__visit
def set_customer(self, customer: Customer) -> None:
self.__visit = Visit(customer=customer)
world.scopes.reset(VISIT_SCOPE)
I suspect the answer is a stateful provider
Currently, there's no particularly good alternative to a stateful factory:
from antidote import world, injectable, inject, lazy
CUSTOMER_SCOPE = world.scopes.new(name='customer')
class Customer:
...
@injectable
class StateHolder:
def __init__(self):
self.__customer: Customer | None = None
@property
def customer(self) -> Customer:
assert self.__customer is not None
return self.__customer
@customer.setter
def customer(self, value: Customer) -> None:
self.__customer = value
world.scopes.reset(CUSTOMER_SCOPE)
@lazy(scope=CUSTOMER_SCOPE)
def customer(state_holder: StateHolder = inject.me()) -> Customer:
return state_holder.customer
The V2 makes the scope handling better, but we still need the @injectable
.
from antidote import state, injectable, inject
class Customer:
...
@injectable
class StateHolder:
def __init__(self):
self.__customer: Customer | None = None
@property
def customer(self) -> Customer:
assert self.__customer is not None
return self.__customer
@customer.setter
def customer(self, value: Customer) -> None:
self.__customer = value
customer.update()
@state
def customer(previous: Customer | None, state_holder: StateHolder = inject.me()) -> Customer:
return state_holder.customer
An alternative could be Antidote exposing a "StateHolder"-like class out of the box like:
# peusdo-code
from antidote import State, world
class Customer:
...
customer = State[Customer]()
customer.update(Cusomter())
assert isinstance(world[customer], Customer)
It doesn't address all the use cases of a stateful factory, but I'm not sure those a needed either. There are two problems with a stateful factory:
- static typing isn't simple, previously Antidote used
dependency @ factory
orinject.get(dependency, source=factory)
syntax - test isolation is complex as the stateful factory now holds some state which certainly needs to be taken into account when using
world.test.clone()
. The current@factory
implementation doesn't.
What do you think of it? Would a State
work for your use cases?
Thanks for (as usual) a comprehensive response.
First, I think it's fine to ignore V1 and discuss V2. My interest is in the book thing and I had planned to re-do it for V2 before coming to see you. 🤞
I don't think a generic, out-of-the-box State
is yet needed. Antidote will be a framework-framework so framework authors should write their own. Antidote should just make it easy to do so.
Your points at the end about "problems with a stateful factory"...were those two bullets about an OOTB State
or also for custom StateHolder
?
Thanks for (as usual) a comprehensive response.
It's my pleasure. :)
The bullet points were for the previous @factory
-style stateful class. The last point, with world.test.clone
is handled in the same way for a @factory
-class or a StateHolder
, it's the instance of the class that is managed by Antidote. So you only keep the state when copying singletons. "Problem" is a bit exaggerated. But it's not obvious to me what is generic enough to be provided by Antidote OOTB.
When not using scopes/deterministic dependencies, the simplest way to have a stateful factory is to simply inject the "factory" itself:
@inject
def f(state: StateHolder = inject.me()) -> None:
customer = state.customer
I don't think a generic, out-of-the-box State is yet needed. Antidote will be a framework-framework so framework authors should write their own. Antidote should just make it easy to do so.
I do want Antidote to be also used directly for an application though, not only for framework authors. :) Thanks for the input, I'll put this on hold for now.
Your initial example would now be solved with a ScopeGlobalVar
in the V2:
from dataclasses import dataclass
from antidote import ScopeGlobalVar
class Customer:
pass
@dataclass
class Visit:
customer: Customer
visit = ScopeGlobalVar[Visit]()
visit.set(Visit(customer=Customer()))
For the stateful factory part, in the V2 you would use a @lazy.method
:
from antidote import lazy, injectable, world
@injectable
class Stateful:
def __init__(self):
self.state = {}
@lazy.method
def build(self, key: str) -> object:
return [self.state[key]]
world[Stateful].state['Alice'] = 'Bob'
assert world[Stateful.build('Alice')] == ['Bob']
Yep, this is really great. I'll close this.