question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Aggregate provider (aka use Selector as FactoryAggregate)

See original GitHub issue

I found a selector provider, but I think it doesn’t satisfy my needs. I woud like to write a CLI script (i’m using click library) which reads configs of different types (json / yaml). I want users to specify a config type in an input and choose an appropriate reader with specified value. Also I want a click to validate an input, so it can show error if user specifies invalid / unknown type of the config. The last thing can be made by using a click.argument with type click.Choice.

Below is a code with Selector provider usage.

import abc
import typing as t
from contextlib import closing
from pathlib import Path

import click
from dependency_injector.containers import DeclarativeContainer
from dependency_injector.providers import Configuration, Selector, Singleton


class Reader(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def read(self, source: t.TextIO) -> object:
        raise NotImplementedError


class YamlReader(Reader):
    def read(self, source: t.TextIO) -> object:
        return "yaml config"


class JsonReader(Reader):
    def read(self, source: t.TextIO) -> object:
        return "json config"


class CLIContainer(DeclarativeContainer):
    # I have to declare a mapping with all available readers separately, to use it in `click.Choice` type.
    READERS = {
        "yaml": Singleton(YamlReader),
        "json": Singleton(JsonReader),
    }

    # I have to declare a configuration to use it with `Selector`
    config = Configuration()
    reader = Selector(config.reader_type, **READERS)


@click.command("cli")
@click.argument("type", type=click.Choice(sorted(CLIContainer.READERS)))  # pass all available reader types.
@click.argument("config", type=click.Path(exists=True, path_type=Path))
@click.pass_context
def cli(context: click.Context, type: str, config: Path) -> None:
    container = context.obj = CLIContainer()

    # I have to specify a value in a config, instead of passing it to a `container.reader` provider. Also I have to
    # set a value in a config via a name of the config value is used in a selector provider.
    container.config.set("reader_type", type)

    reader = container.reader()
    click.echo(reader.read(context.with_resource(closing(config.open("r")))))


if __name__ == "__main__":
    cli()

What I want is to write a less code and to use mypy checks / IDE suggestions to avoid mistakes. That’s why a came up with an implementation of KeySelector provider in my providers.py module. See the code below.

import typing as t

from dependency_injector.providers import Callable, Provider, deepcopy

T = t.TypeVar("T")


class KeySelector(Provider, t.Generic[T]):
    __slots__ = (
        "__providers_by_key",
        "__inner",
    )

    def __init__(self, providers_by_key: t.Mapping[str, Provider[T]], *args: t.Any, **kwargs: t.Any) -> None:
        self.__providers_by_key = providers_by_key
        self.__inner = Callable(self.__providers_by_key.get, *args, **kwargs)
        super().__init__()

    def __deepcopy__(self, memo: t.Dict[int, t.Any]) -> "KeySelector":
        copied = memo.get(id(self))
        if copied is not None:
            return copied

        copied = self.__class__(
            deepcopy(self.__providers_by_key, memo),
            *deepcopy(self.__inner.args, memo),
            **deepcopy(self.__inner.kwargs, memo),
        )
        self._copy_overridings(copied, memo)

        return copied

    @property
    def related(self) -> t.Iterable[Provider]:
        """Return related providers generator."""
        yield self.__inner
        yield from super().related

    def _provide(self, args: t.Sequence[t.Any], kwargs: t.Mapping[str, t.Any]) -> T:
        key, *args = args
        return self.__inner(key)(*args, **kwargs)

    def keys(self) -> t.Collection[str]:
        return self.__providers_by_key.keys()

Then I can use it in my CLI as I want.

import abc
import typing as t
from contextlib import closing
from pathlib import Path

import click
from dependency_injector.containers import DeclarativeContainer
from dependency_injector.providers import Singleton

from .providers import KeySelector


class Reader(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def read(self, source: t.TextIO) -> object:
        raise NotImplementedError


class YamlReader(Reader):
    def read(self, source: t.TextIO) -> object:
        return "yaml config"


class JsonReader(Reader):
    def read(self, source: t.TextIO) -> object:
        return "json config"


class CLIContainer(DeclarativeContainer):
    reader = KeySelector({
        "yaml": Singleton(YamlReader),
        "json": Singleton(JsonReader),
    })


@click.command("cli")
@click.argument("type", type=click.Choice(sorted(CLIContainer.reader.keys())))  # pass all available reader types.
@click.argument("config", type=click.Path(exists=True, path_type=Path))
@click.pass_context
def cli(context: click.Context, type: str, config: Path) -> None:
    container = context.obj = CLIContainer()

    # Now I can get an instance by passing a key to my provider. It's more simple to me.
    reader = container.reader(type)
    click.echo(reader.read(context.with_resource(closing(config.open("r")))))


if __name__ == "__main__":
    cli()

Maybe my suggestion can be enhanced and can be added to a library, I guess? 😃

P.S. For know I don’t know how to implement override method for KeySelector provider. Maybe KeySelector may just provide an interface of Mapping type, so all providers can be overriden directly via a key selector["json"].override(...).

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:7 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
zerlokcommented, Jun 11, 2022

Hi, @rmk135 . I found a new Aggregate provider starting from release 4.38.0 . I think this is what I want. I can’t check it right now, but I think this is it. Great work, thank you! I think this issue can be closed.

0reactions
rmk135commented, Jan 8, 2022

I’m still in doubt about the right name for that provider. I like KeySelector, but wonder if there is anything sharper. Please let me know if you have any ideas.

I guess I go ahead with the name Aggregate. This seems like the clearest statement of what this provider is intended for: to aggregate several other providers and represent them as a whole while still providing access to the parts.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to use FactoryAggregate in a container - aka Selector ...
Can I use FactoryAggregate in a DeclarativeContainer, where I choose the proper factory via config? The following code does not work from ...
Read more >
Create a Selector to Aggregate Data from our Redux Store
A selector is a perfect place to do that calculation. Another place it could be done is in a useMemo hook in your...
Read more >
Aggregations | Elasticsearch Guide [8.5] | Elastic
To return the aggregation type, use the typed_keys query parameter. GET /my-index-000001/_search?typed_keys ...
Read more >
Deriving Data with Selectors - Redux
A "selector function" is any function that accepts the Redux store state (or part of the state) as an argument, and returns data...
Read more >
Stop using Page Objects and Start using App Actions - Cypress
Page objects have two main benefits: They keep all page element selectors in one place; They standardize how tests interact with the page....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found