Finistere/antidote

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 or inject.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.