Skip to content

Commit

Permalink
Merge pull request #50 from reagento/feature/provider_scope
Browse files Browse the repository at this point in the history
provider scope
  • Loading branch information
Tishka17 authored Feb 16, 2024
2 parents fb929b8 + 976542b commit 5a19d6e
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 20 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[report]
exclude_lines =
pragma: not covered
@overload
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,14 @@ with make_container(MyProvider(), context={App: app}) as container:
```python
with make_container(MyProvider(), OtherProvider()) as container:
```

* Tired of providing `scope==` for each depedency? Set it inside your `Provider` class and all dependencies with no scope will use it.
```python

class MyProvider(Provider):
scope=Scope.APP

@provide
async def get_a(self) -> A:
return A()
```
17 changes: 16 additions & 1 deletion docs/provider/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,23 @@ To create your own provider you inherit from ``Provider`` class and instantiate
with make_container(MyProvider()) as container:
pass
You can also set default scope for factories within provider. It will affect only those factories which have no scope set explicitly.

Though it is a normal object, not all attributes are analyzed by ``Container``, but only those which are marked with special funcions:
* Inside class:

.. code-block:: python
class MyProvider(Provider):
scope=Scope.APP
* Or when instantiating it. This can be also useful for tests to override provider scope.

.. code-block:: python
with make_container(MyProvider(scope=Scope.APP)) as container:
pass
Though it is a normal object, not all attributes are analyzed by ``Container``, but only those which are marked with special functions:

.. toctree::

Expand Down
15 changes: 15 additions & 0 deletions docs/provider/provide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,18 @@ If it is used with class analyzes its ``__init__`` typehints to detect its depen
async with make_async_container(MyProvider()) as container:
a = await container.get(A)
* Tired of providing `scope==` for each depedency? Set it inside your `Provider` class and all factories with no scope will use it.

.. code-block:: python
class MyProvider(Provider):
scope=Scope.APP
@provide # uses provider scope
async def get_a(self) -> A:
return A()
@provide(scope=Scope.REQUEST) # has own scope
async def get_b(self) -> B:
return B()
18 changes: 9 additions & 9 deletions examples/real_world/myapp/ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@

# app dependency logic
class AdaptersProvider(Provider):
users = provide(
FakeUserGateway, scope=Scope.REQUEST, provides=UserGateway,
)
products = provide(
FakeProductGateway, scope=Scope.REQUEST, provides=ProductGateway,
)

@provide(scope=Scope.REQUEST)
scope = Scope.REQUEST

users = provide(FakeUserGateway, provides=UserGateway)
products = provide(FakeProductGateway, provides=ProductGateway)

@provide
def connection(self) -> Iterable[FakeDbConnection]:
uow = FakeDbConnection()
yield uow
Expand All @@ -39,4 +37,6 @@ def warehouse(self) -> WarehouseClient:


class InteractorProvider(Provider):
product = provide(AddProductsInteractor, scope=Scope.REQUEST)
scope = Scope.REQUEST

product = provide(AddProductsInteractor)
10 changes: 6 additions & 4 deletions examples/real_world/tests/test_add_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,27 @@

# app dependency logic
class AdaptersProvider(Provider):
@provide(scope=Scope.APP)
scope=Scope.APP

@provide
def users(self) -> UserGateway:
gateway = Mock()
gateway.get_user = Mock(return_value=User())
return gateway

@provide(scope=Scope.APP)
@provide
def products(self) -> ProductGateway:
gateway = Mock()
gateway.add_product = Mock()
return gateway

@provide(scope=Scope.APP)
@provide
def uow(self) -> UnitOfWork:
uow = Mock()
uow.commit = Mock()
return uow

@provide(scope=Scope.APP)
@provide
def warehouse(self) -> WarehouseClient:
warehouse = Mock()
warehouse.next_product = Mock(return_value=["a", "b"])
Expand Down
9 changes: 5 additions & 4 deletions src/dishka/dependency_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(
dependencies: Sequence[Any],
source: Any,
provides: Type,
scope: Optional[BaseScope],
scope: BaseScope | None,
type: FactoryType,
is_to_bound: bool,
):
Expand All @@ -56,6 +56,7 @@ def __init__(
self.is_to_bound = is_to_bound

def __get__(self, instance, owner):
scope = self.scope or instance.scope
if instance is None:
return self
if self.is_to_bound:
Expand All @@ -66,7 +67,7 @@ def __get__(self, instance, owner):
dependencies=self.dependencies,
source=source,
provides=self.provides,
scope=self.scope,
scope=scope,
type=self.type,
is_to_bound=False,
)
Expand Down Expand Up @@ -119,7 +120,7 @@ def make_factory(
@overload
def provide(
*,
scope: BaseScope,
scope: BaseScope = None,
provides: Any = None,
) -> Callable[[Callable], Factory]:
...
Expand All @@ -138,7 +139,7 @@ def provide(
def provide(
source: Callable | Type | None = None,
*,
scope: BaseScope,
scope: BaseScope | None = None,
provides: Any = None,
) -> Factory | Callable[[Callable], Factory]:
"""
Expand Down
5 changes: 4 additions & 1 deletion src/dishka/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from .dependency_source import Alias, Decorator, DependencySource, Factory
from .exceptions import InvalidGraphError
from .scope import BaseScope


def is_dependency_source(attribute: Any) -> bool:
Expand All @@ -22,12 +23,14 @@ class Provider:
The only intended usage of providers is to pass them when
creating a container
"""
scope: BaseScope | None = None

def __init__(self):
def __init__(self, scope: BaseScope | None = None):
self.factories: List[Factory] = []
self.aliases: List[Alias] = []
self.decorators: List[Decorator] = []
self._init_dependency_sources()
self.scope = self.scope or scope

def _init_dependency_sources(self) -> None:
processed_types = {}
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/container/test_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,36 @@ class DProvider(Provider):
assert isinstance(a.a, A)


def test_decorator():
class MyProvider(Provider):
a = provide(A, scope=Scope.APP)

class DProvider(Provider):
@decorate()
def foo(self, a: A) -> A:
return ADecorator(a)

with make_container(MyProvider(), DProvider()) as container:
a = container.get(A)
assert isinstance(a, ADecorator)
assert isinstance(a.a, A)


def test_decorator_with_provides():
class MyProvider(Provider):
a = provide(A, scope=Scope.APP)

class DProvider(Provider):
@decorate(provides=A)
def foo(self, a: A):
return ADecorator(a)

with make_container(MyProvider(), DProvider()) as container:
a = container.get(A)
assert isinstance(a, ADecorator)
assert isinstance(a.a, A)


def test_alias():
class MyProvider(Provider):
a2 = provide(A2, scope=Scope.APP)
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/container/test_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
provide,
)
from ..sample_providers import (
A_VALUE,
ClassA,
async_func_a,
async_gen_a,
async_iter_a,
sync_func_a,
sync_gen_a,
sync_iter_a,
value_factory,
)


Expand Down Expand Up @@ -66,3 +68,20 @@ def get_int(self) -> int:
assert a
assert a.dep == 100
assert a.closed == closed


def test_value():
class MyProvider(Provider):
factory = value_factory

with make_container(MyProvider()) as container:
assert container.get(ClassA) is A_VALUE


@pytest.mark.asyncio
async def test_value_async():
class MyProvider(Provider):
factory = value_factory

async with make_async_container(MyProvider()) as container:
assert await container.get(ClassA) is A_VALUE
14 changes: 14 additions & 0 deletions tests/unit/sample_providers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import AsyncGenerator, AsyncIterable, Generator, Iterable

from dishka import Scope
from dishka.dependency_source import Factory, FactoryType


class ClassA:
def __init__(self, dep: int) -> None:
Expand Down Expand Up @@ -37,3 +40,14 @@ async def async_gen_a(self, dep: int) -> AsyncGenerator[ClassA, None]:
a = ClassA(dep)
yield a
a.closed = True


A_VALUE = ClassA(42)
value_factory = Factory(
provides=ClassA,
source=A_VALUE,
dependencies=[],
type=FactoryType.VALUE,
scope=Scope.APP,
is_to_bound=False,
)
32 changes: 32 additions & 0 deletions tests/unit/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,35 @@ def test_parse_factory(source, provider_type, is_to_bound):
assert factory.scope == Scope.REQUEST
assert factory.source == source
assert factory.type == provider_type


def test_provider_class_scope():
class MyProvider(Provider):
scope = Scope.REQUEST

@provide()
def foo(self, x: bool) -> str:
return f"{x}"

provider = MyProvider()
assert provider.foo.scope == Scope.REQUEST


def test_provider_instance_scope():
class MyProvider(Provider):
@provide()
def foo(self, x: bool) -> str:
return f"{x}"

provider = MyProvider(scope=Scope.REQUEST)
assert provider.foo.scope == Scope.REQUEST


def test_provider_instance_braces():
class MyProvider(Provider):
@provide
def foo(self, x: bool) -> str:
return f"{x}"

provider = MyProvider(scope=Scope.REQUEST)
assert provider.foo.scope == Scope.REQUEST
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ env_list =
telebot-415,

[pytest]
addopts = --cov=dishka --cov-append -v
addopts = --cov=dishka --cov-append --cov-report term-missing -v

[testenv]
deps =
Expand Down

0 comments on commit 5a19d6e

Please sign in to comment.