From cb74e5983ada93698a7261d662236310918b5d3c Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Wed, 24 Jan 2024 00:00:00 +0100 Subject: [PATCH] rename mony field so they have clearer meaning --- README.md | 34 ++++++++------ examples/di/with_dishka.py | 2 +- examples/sync_simple.py | 2 +- src/dishka/async_container.py | 10 ++--- src/dishka/container.py | 8 ++-- src/dishka/provider.py | 85 +++++++++++++++++++---------------- src/dishka/registry.py | 8 ++-- tests/test_container.py | 37 ++++++++++++--- tests/test_privider.py | 10 ++--- 9 files changed, 118 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 535f105a..a8707610 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -## DIshka - DI by Tishka17 +## DIshka (from russian "small DI") -Minimal DI framework with scopes +Small DI framework with scopes and agreeable API. ### Purpose @@ -14,6 +14,7 @@ Main ideas: * **Modular providers**. Instead of creating lots of separate functions or contrariwise a big single class, you can split your factories into several classes, which makes them simpler reusable. * **Clean dependencies**. You do not need to add custom markers to the code of dependencies so to allow library to see them. All customization is done within providers code and only borders of scopes have to deal with library API. * **Simple API**. You need minimum of objects to start using library. You can easily integrate it with your task framework, examples provided. +* **Speed**. It is fast enough so you not to worry about. It is even faster than many of the analogs. ### Quickstart @@ -55,27 +56,32 @@ with make_container(provider) as container: **Provider** is a collection of functions which really provide some objects. * Add method and mark it with `@provide` decorator. It can be sync or async method returning some value. ```python - @provide(scope=Scope.REQUEST) - def get_a(self) -> A: - return A() + class MyProvider(Provider): + @provide(scope=Scope.REQUEST) + def get_a(self) -> A: + return A() ``` * Want some finalization when exiting the scope? Make that method generator: ```python - @provide(scope=Scope.REQUEST) - def get_a(self) -> Iterable[A]: - a = A() - yield a - a.close() + class MyProvider(Provider): + @provide(scope=Scope.REQUEST) + def get_a(self) -> Iterable[A]: + a = A() + yield a + a.close() ``` * Do not have any specific logic and just want to create class using its `__init__`? then add a provider attribute using `provide` as function passing that class. ```python - a = provide(A, scope=Scope.REQUEST) + class MyProvider(Provider): + a = provide(A, scope=Scope.REQUEST) ``` * Want to create a child class instance when parent is requested? add a `dependency` attribute to `provide` function with a parent class while passing child as a first parameter ```python - a = provide(AChild, scope=Scope.REQUEST, dependency=A) + class MyProvider(Provider): + a = provide(source=AChild, scope=Scope.REQUEST, provides=A) ``` -* Having multiple interfaces which can be created as a same class? Use alias: +* Having multiple interfaces which can be created as a same class with defined provider? Use alias: ```python - p = alias(AProtocol, A) + class MyProvider(Provider): + p = alias(source=A, provides=AProtocol) ``` \ No newline at end of file diff --git a/examples/di/with_dishka.py b/examples/di/with_dishka.py index 65ea97a2..0011688c 100644 --- a/examples/di/with_dishka.py +++ b/examples/di/with_dishka.py @@ -5,7 +5,7 @@ class MyProvider(Provider): - a = provide(A1, scope=MyScope.REQUEST, dependency=A) + a = provide(A1, scope=MyScope.REQUEST, provides=A) c1 = provide(CA, scope=MyScope.REQUEST) c2 = provide(CAA, scope=MyScope.REQUEST) c3 = provide(CAAA, scope=MyScope.REQUEST) diff --git a/examples/sync_simple.py b/examples/sync_simple.py index 16298b5e..ae1ceaee 100644 --- a/examples/sync_simple.py +++ b/examples/sync_simple.py @@ -21,7 +21,7 @@ def __init__(self, a: int): self.a = a get_a = provide(A, scope=Scope.REQUEST) - get_basea = alias(A, dependency=BaseA) + get_basea = alias(source=A, provides=BaseA) @provide(scope=Scope.APP) def get_int(self) -> int: diff --git a/src/dishka/async_container.py b/src/dishka/async_container.py index f381100e..4c0c1539 100644 --- a/src/dishka/async_container.py +++ b/src/dishka/async_container.py @@ -75,19 +75,19 @@ async def _get_self( for dependency in dep_provider.dependencies ] if dep_provider.type is ProviderType.GENERATOR: - generator = dep_provider.callable(*sub_dependencies) + generator = dep_provider.source(*sub_dependencies) self.exits.append(Exit(dep_provider.type, generator)) return next(generator) elif dep_provider.type is ProviderType.ASYNC_GENERATOR: - generator = dep_provider.callable(*sub_dependencies) + generator = dep_provider.source(*sub_dependencies) self.exits.append(Exit(dep_provider.type, generator)) return await anext(generator) elif dep_provider.type is ProviderType.ASYNC_FACTORY: - return await dep_provider.callable(*sub_dependencies) + return await dep_provider.source(*sub_dependencies) elif dep_provider.type is ProviderType.FACTORY: - return dep_provider.callable(*sub_dependencies) + return dep_provider.source(*sub_dependencies) elif dep_provider.type is ProviderType.VALUE: - return dep_provider.callable + return dep_provider.source else: raise ValueError(f"Unsupported type {dep_provider.type}") diff --git a/src/dishka/container.py b/src/dishka/container.py index 6e239724..f373a8ed 100644 --- a/src/dishka/container.py +++ b/src/dishka/container.py @@ -67,13 +67,13 @@ def _get_self( for dependency in dep_provider.dependencies ] if dep_provider.type is ProviderType.GENERATOR: - generator = dep_provider.callable(*sub_dependencies) + generator = dep_provider.source(*sub_dependencies) self.exits.append(generator) return next(generator) elif dep_provider.type is ProviderType.FACTORY: - return dep_provider.callable(*sub_dependencies) + return dep_provider.source(*sub_dependencies) elif dep_provider.type is ProviderType.VALUE: - return dep_provider.callable + return dep_provider.source else: raise ValueError(f"Unsupported type {dep_provider.type}") @@ -110,6 +110,8 @@ def close(self): class ContextWrapper: + __slots__ = ("container", ) + def __init__(self, container: Container): self.container = container diff --git a/src/dishka/provider.py b/src/dishka/provider.py index 5bf73916..7ee390da 100644 --- a/src/dishka/provider.py +++ b/src/dishka/provider.py @@ -29,24 +29,28 @@ class ProviderType(Enum): VALUE = "value" +def _identity(x: Any) -> Any: + return x + + class DependencyProvider: __slots__ = ( - "dependencies", "callable", "result_type", "scope", "type", + "dependencies", "source", "provides", "scope", "type", "is_to_bound", ) def __init__( self, - dependencies: Sequence, - callable: Callable, - result_type: Type, + dependencies: Sequence[Any], + source: Any, + provides: Type, scope: Optional[BaseScope], type: ProviderType, is_to_bound: bool, ): self.dependencies = dependencies - self.callable = callable - self.result_type = result_type + self.source = source + self.provides = provides self.scope = scope self.type = type self.is_to_bound = is_to_bound @@ -55,13 +59,13 @@ def __get__(self, instance, owner): if instance is None: return self if self.is_to_bound: - callable = self.callable.__get__(instance, owner) + source = self.source.__get__(instance, owner) else: - callable = self.callable + source = self.source return DependencyProvider( dependencies=self.dependencies, - callable=callable, - result_type=self.result_type, + source=source, + provides=self.provides, scope=self.scope, type=self.type, is_to_bound=False, @@ -69,9 +73,9 @@ def __get__(self, instance, owner): def aliased(self, target: Type): return DependencyProvider( - dependencies=self.dependencies, - callable=self.callable, - result_type=target, + dependencies=[self.provides], + source=_identity, + provides=target, scope=self.scope, type=self.type, is_to_bound=self.is_to_bound, @@ -79,35 +83,35 @@ def aliased(self, target: Type): def make_dependency_provider( - dependency: Any, + provides: Any, scope: Optional[BaseScope], - func: Callable, + source: Callable, ): - if isclass(func): - hints = get_type_hints(func.__init__, include_extras=True) + if isclass(source): + hints = get_type_hints(source.__init__, include_extras=True) hints.pop("return", None) - possible_dependency = func + possible_dependency = source is_to_bind = False else: - hints = get_type_hints(func, include_extras=True) + hints = get_type_hints(source, include_extras=True) possible_dependency = hints.pop("return", None) is_to_bind = True - if isclass(func): + if isclass(source): provider_type = ProviderType.FACTORY - elif isasyncgenfunction(func): + elif isasyncgenfunction(source): provider_type = ProviderType.ASYNC_GENERATOR if get_origin(possible_dependency) is AsyncIterable: possible_dependency = get_args(possible_dependency)[0] else: # async generator possible_dependency = get_args(possible_dependency)[0] - elif isgeneratorfunction(func): + elif isgeneratorfunction(source): provider_type = ProviderType.GENERATOR if get_origin(possible_dependency) is Iterable: possible_dependency = get_args(possible_dependency)[0] else: # generator possible_dependency = get_args(possible_dependency)[1] - elif iscoroutinefunction(func): + elif iscoroutinefunction(source): provider_type = ProviderType.ASYNC_FACTORY else: provider_type = ProviderType.FACTORY @@ -115,50 +119,53 @@ def make_dependency_provider( return DependencyProvider( dependencies=list(hints.values()), type=provider_type, - callable=func, + source=source, scope=scope, - result_type=dependency or possible_dependency, + provides=provides or possible_dependency, is_to_bound=is_to_bind, ) class Alias: - def __init__(self, target, result_type): - self.target = target - self.result_type = result_type + __slots__ = ("source", "provides") + + def __init__(self, source, provides): + self.source = source + self.provides = provides def alias( - target: Type, - dependency: Any = None, + *, + source: Type, + provides: Type, ): return Alias( - target=target, - result_type=dependency, + source=source, + provides=provides, ) def provide( - func: Union[None, Callable] = None, + source: Union[None, Callable, Type] = None, *, scope: BaseScope = None, - dependency: Any = None, + provides: Any = None, ): - if func is not None: - return make_dependency_provider(dependency, scope, func) + if source is not None: + return make_dependency_provider(provides, scope, source) def scoped(func): - return make_dependency_provider(dependency, scope, func) + return make_dependency_provider(provides, scope, func) return scoped class Provider: def __init__(self): - self.dependencies = {} + self.dependency_providers = {} self.aliases = [] for name, attr in vars(type(self)).items(): if isinstance(attr, DependencyProvider): - self.dependencies[attr.result_type] = getattr(self, name) + self.dependency_providers[attr.provides] = getattr(self, name) elif isinstance(attr, Alias): self.aliases.append(attr) diff --git a/src/dishka/registry.py b/src/dishka/registry.py index 74527d5b..d17af9c3 100644 --- a/src/dishka/registry.py +++ b/src/dishka/registry.py @@ -12,7 +12,7 @@ def __init__(self, scope: BaseScope): self.scope = scope def add_provider(self, provider: DependencyProvider): - self._providers[provider.result_type] = provider + self._providers[provider.provides] = provider def get_provider(self, dependency: Any): return self._providers.get(dependency) @@ -21,15 +21,15 @@ def get_provider(self, dependency: Any): def make_registry(*providers: Provider, scope: BaseScope) -> Registry: registry = Registry(scope) for provider in providers: - for dependency_provider in provider.dependencies.values(): + for dependency_provider in provider.dependency_providers.values(): if dependency_provider.scope is scope: registry.add_provider(dependency_provider) for provider in providers: for alias in provider.aliases: - dependency_provider = registry.get_provider(alias.target) + dependency_provider = registry.get_provider(alias.source) if dependency_provider: registry.add_provider( - dependency_provider.aliased(alias.result_type), + dependency_provider.aliased(alias.provides), ) return registry diff --git a/tests/test_container.py b/tests/test_container.py index 21ff9edc..53678c4e 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -3,6 +3,7 @@ from dishka import ( Provider, Scope, + alias, make_async_container, make_container, provide, @@ -70,9 +71,7 @@ def get_int(self) -> int: def test_cache_sync(): class MyProvider(Provider): - def __init__(self): - super().__init__() - self.value = 0 + value = 0 @provide(scope=Scope.REQUEST) def get_int(self) -> int: @@ -91,9 +90,7 @@ def get_int(self) -> int: @pytest.mark.asyncio async def test_cache_async(): class MyProvider(Provider): - def __init__(self): - super().__init__() - self.value = 0 + value = 0 @provide(scope=Scope.REQUEST) async def get_int(self) -> int: @@ -107,3 +104,31 @@ async def get_int(self) -> int: async with container() as state: assert await state.get(int) == 2 assert await state.get(int) == 2 + + +@pytest.fixture() +def alias_provider(): + class MyProvider(Provider): + value = 0 + + @provide(scope=Scope.APP) + def get_int(self) -> int: + self.value += 1 + return self.value + + float = alias(source=int, provides=float) + + return MyProvider() + + +def test_alias_sync(alias_provider): + with make_container(alias_provider) as container: + assert container.get(int) == 1 + assert container.get(float) == 1 + + +@pytest.mark.asyncio +async def test_alias_async(alias_provider): + async with make_async_container(alias_provider) as container: + assert await container.get(int) == 1 + assert await container.get(float) == 1 diff --git a/tests/test_privider.py b/tests/test_privider.py index 612df89e..d9c1d164 100644 --- a/tests/test_privider.py +++ b/tests/test_privider.py @@ -15,15 +15,15 @@ def test_provider_init(): class MyProvider(Provider): - a = alias(int, bool) - b = provide(lambda: False, scope=Scope.APP, dependency=bool) + a = alias(source=int, provides=bool) + b = provide(lambda: False, scope=Scope.APP, provides=bool) @provide(scope=Scope.REQUEST) def foo(self, x: bool) -> str: return f"{x}" provider = MyProvider() - assert len(provider.dependencies) == 2 + assert len(provider.dependency_providers) == 2 assert len(provider.aliases) == 1 @@ -40,9 +40,9 @@ def foo(self, x: bool) -> str: ) def test_parse_provider(factory, provider_type, is_to_bound): dep_provider = provide(factory, scope=Scope.REQUEST) - assert dep_provider.result_type == ClassA + assert dep_provider.provides == ClassA assert dep_provider.dependencies == [int] assert dep_provider.is_to_bound == is_to_bound assert dep_provider.scope == Scope.REQUEST - assert dep_provider.callable == factory + assert dep_provider.source == factory assert dep_provider.type == provider_type