ets-labs/python-dependency-injector

Is it possible to provide default factory for FactoryAggregate and/or Selector?

Closed this issue · 1 comments

What am I trying to achieve:

from dependency_injector.containers import DeclarativeContainer
from dependency_injector import providers
from dependency_injector.errors import NoSuchProviderError

class Obj:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f'<Obj name={self.name}>'

class C(DeclarativeContainer):
    
    agg_factory = providers.FactoryAggregate(
        option1=providers.Factory(Obj, 'option1'),
        option2=providers.Factory(Obj, 'option2'),
        # default (any) option
        _=providers.Factory(Obj, "I'm default")
    )
    
    def callable_provider(option):
        if option in ('option1', 'option2'):
            return Obj(name=option)
        return Obj("I'm default")
    
    callable = providers.Callable(
        callable_provider
    )
    
container = C()
print(container.agg_factory('option1')) 
try:
    container.agg_factory('random')  # NoSuchProviderError
except NoSuchProviderError:
    print('=(')

# this works but it's not as "clean" as FactoryAggregate solution
print(container.callable('option1'))
print(container.callable('random'))

Code above results in:

<Obj name=option1>
=(
<Obj name=option1>
<Obj name=I'm default>

So, is it possible use Callable power for FactoryAggregate or Selector to make container.agg_factory('random') return '_' option?

Made custom provider according to https://python-dependency-injector.ets-labs.org/providers/custom.html and

from dependency_injector.containers import DeclarativeContainer
from dependency_injector import providers
from dependency_injector.errors import NoSuchProviderError

class Obj:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f'<Obj name={self.name}>'


class DefaultFactoryAggregate(providers.Provider):
    DEFAULT_OPTION = '_'
    __slots__ = ('_agg_factory')

    def __init__(self, provider_dict=None, **provider_kwargs):
        self._agg_factory = providers.FactoryAggregate(provider_dict, **provider_kwargs)
        super().__init__()
        
    @property
    def related(self):
        """Return related providers generator."""
        yield from [self._agg_factory]
        yield from super().related

    def __deepcopy__(self, memo):
        copied = memo.get(id(self))
        if copied is not None:
            return copied

        copied = self.__class__(
            self._agg_factory.providers,
        )
        self._copy_overridings(copied, memo)

        return copied

    def _provide(self, args, kwargs):
        try:
            return self._agg_factory(*args, **kwargs)
        except NoSuchProviderError:
            kwargs.pop('factory_name', None)
            args = (self.DEFAULT_OPTION, ) + args[1:]
            return self._agg_factory(*args, **kwargs)


class C(DeclarativeContainer):
    agg_factory = DefaultFactoryAggregate(
        option1=providers.Factory(Obj, 'option1'),
        option2=providers.Factory(Obj, 'option2'),
        _=providers.Factory(Obj, "I'm default")
    )

container = C()
print(container.agg_factory('option1'))
print(container.agg_factory(factory_name='option2'))
print(container.agg_factory('123'))
print(container.agg_factory(None))
<Obj name=option1>
<Obj name=option2>
<Obj name=I'm default>
<Obj name=I'm default>

Inherited version much shorter:

class InheritedDefaultFactoryAggregate(providers.FactoryAggregate):
    DEFAULT_OPTION = '_'
    
    def _provide(self, args: tuple, kwargs: dict):
        try:
            return super()._provide(args, kwargs)
        except NoSuchProviderError:
            kwargs.pop('factory_name', None)
            args = (self.DEFAULT_OPTION,) + args[1:]
            return super()._provide(args, kwargs)

but Prefer delegation over inheritance documentation says =) Have no idea why actually...