ets-labs / python-dependency-injector

Dependency injection framework for Python
https://python-dependency-injector.ets-labs.org/
BSD 3-Clause "New" or "Revised" License
3.89k stars 304 forks source link

Is there a Provider that provides a List and Selector from a configuration? #690

Closed andresi closed 1 year ago

andresi commented 1 year ago

I have a class that takes a list of items in it's constructor. Each item can be a different class, and I want to define the items in a configuration yaml file, like this:

items:
     - itemA
     - itemB
     - itemC

I can use Selector to map itemA to ClassA, etc. But I can't find a way to then map this into a List provider. Is there some other Provider that does this?

andresi commented 1 year ago

Ok, I don't know if this was already supported by one of the existing providers, but I built one:

class ListSelectorProvider(providers.Provider):
    def __init__(self, selectors, **providers):
        """Initialize provider."""
        self.__selectors = None
        self.set_selectors(selectors)

        self.__providers = {}
        self.set_providers(**providers)

        super(ListSelectorProvider, self).__init__()

    def __deepcopy__(self, memo):
        """Create and return full copy of provider."""
        copied = memo.get(id(self))
        if copied is not None:
            return copied

        copied = self.__class__(
            providers.deepcopy(self.__selectors, memo),
            **providers.deepcopy(self.__providers, memo)
        )

        self._copy_overridings(copied, memo)

        return copied

    def __getattr__(self, name):
        """Return provider."""
        if name.startswith("__") and name.endswith("__"):
            raise AttributeError(
                "'{cls}' object has no attribute "
                "'{attribute_name}'".format(cls=self.__class__.__name__, attribute_name=name))
        if name not in self.__providers:
            raise AttributeError("ListSelectorProvider has no \"{0}\" provider".format(name))

        return self.__providers[name]

    def __str__(self):
        """Return string representation of provider.
        :rtype: str
        """

        return "<{provider}({selectors}, {providers}) at {address}>".format(
            provider=".".join((self.__class__.__module__, self.__class__.__name__)),
            selector=self.__selectors,
            providers=", ".join((
                "{0}={1}".format(name, provider)
                for name, provider in self.__providers.items()
            )),
            address=hex(id(self)),
        )

    @property
    def selectors(self):
        """Return selectors."""
        return self.__selectors

    def set_selectors(self, selectors):
        """Set selectors."""
        self.__selectors = selectors
        return self

    @property
    def providers(self):
        """Return providers."""
        return dict(self.__providers)

    def set_providers(self, **providers):
        """Set providers."""
        self.__providers = providers
        return self

    @property
    def related(self):
        """Return related providers generator."""
        yield from filter(providers.is_provider, [self.__selectors])
        yield from self.providers.values()
        yield from super().related

    def _provide(self, args, kwargs):
        """Return single instance."""
        selectors_value = self.__selectors()

        if selectors_value is None or len(selectors_value) == 0:
            raise providers.Error("ListSelectorProvider value is undefined")

        provides = []
        for selector_value in selectors_value:
            if selector_value not in self.__providers:
                raise providers.Error("ListSelectorProvider has no \"{0}\" provider".format(selector_value))
            provides.append(self.__providers[selector_value](*args, **kwargs))

        return provides
gitisz commented 11 months ago

@andresi can you share an implementation of ListSelectorProvider? I just found this after posting https://github.com/ets-labs/python-dependency-injector/issues/762 which seems similar.

andresi commented 11 months ago

Let's say your config looks like this:

items:
     - itemA
     - itemB
     - itemC

Then you use like this:

 items = ListSelectorProvider(
        config.items,
        itemA=providers.Factory(ItemA),
        itemB=providers.Factory(ItemB),
        itemC=providers.Factory(ItemC)
   )
gitisz commented 11 months ago

A lot simpler than I was imagining ;) Thanks!