Repository: reagento/dishka
Pull request #50 (2024-02-16 19:56:16)

provider scope


Patch:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..b2c41d0
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,4 @@
+[report]
+exclude_lines = 
+    pragma: not covered
+    @overload
diff --git a/README.md b/README.md
index 013ea13..ef84a44 100644
--- a/README.md
+++ b/README.md
@@ -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()
+```
\ No newline at end of file
diff --git a/docs/provider/index.rst b/docs/provider/index.rst
index 9138f14..9b97bcc 100644
--- a/docs/provider/index.rst
+++ b/docs/provider/index.rst
@@ -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::
 
diff --git a/docs/provider/provide.rst b/docs/provider/provide.rst
index 7707b83..599ccba 100644
--- a/docs/provider/provide.rst
+++ b/docs/provider/provide.rst
@@ -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()
diff --git a/examples/real_world/myapp/ioc.py b/examples/real_world/myapp/ioc.py
index 12bf70b..8f24a0a 100644
--- a/examples/real_world/myapp/ioc.py
+++ b/examples/real_world/myapp/ioc.py
@@ -18,14 +18,12 @@ from .use_cases import (
 
 # 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
@@ -39,4 +37,6 @@ class AdaptersProvider(Provider):
 
 
 class InteractorProvider(Provider):
-    product = provide(AddProductsInteractor, scope=Scope.REQUEST)
+    scope = Scope.REQUEST
+
+    product = provide(AddProductsInteractor)
diff --git a/src/dishka/dependency_source.py b/src/dishka/dependency_source.py
index f3988f5..162e69c 100644
--- a/src/dishka/dependency_source.py
+++ b/src/dishka/dependency_source.py
@@ -44,7 +44,7 @@ class Factory:
             dependencies: Sequence[Any],
             source: Any,
             provides: Type,
-            scope: Optional[BaseScope],
+            scope: BaseScope | None,
             type: FactoryType,
             is_to_bound: bool,
     ):
@@ -56,6 +56,7 @@ class Factory:
         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:
@@ -66,7 +67,7 @@ class Factory:
             dependencies=self.dependencies,
             source=source,
             provides=self.provides,
-            scope=self.scope,
+            scope=scope,
             type=self.type,
             is_to_bound=False,
         )
@@ -119,7 +120,7 @@ def make_factory(
 @overload
 def provide(
         *,
-        scope: BaseScope,
+        scope: BaseScope = None,
         provides: Any = None,
 ) -> Callable[[Callable], Factory]:
     ...
@@ -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]:
     """
diff --git a/src/dishka/provider.py b/src/dishka/provider.py
index 80a327b..1eb34f7 100644
--- a/src/dishka/provider.py
+++ b/src/dishka/provider.py
@@ -3,6 +3,7 @@ from typing import Any, List
 
 from .dependency_source import Alias, Decorator, DependencySource, Factory
 from .exceptions import InvalidGraphError
+from .scope import BaseScope
 
 
 def is_dependency_source(attribute: Any) -> bool:
@@ -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 = {}
diff --git a/tox.ini b/tox.ini
index 6878a2f..2952547 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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 =


Test patch:
diff --git a/examples/real_world/tests/test_add_products.py b/examples/real_world/tests/test_add_products.py
index 8ec0959..81a15c8 100644
--- a/examples/real_world/tests/test_add_products.py
+++ b/examples/real_world/tests/test_add_products.py
@@ -27,25 +27,27 @@ from myapp.use_cases import (
 
 # 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"])
diff --git a/tests/unit/container/test_decorator.py b/tests/unit/container/test_decorator.py
index 4ed84de..635796e 100644
--- a/tests/unit/container/test_decorator.py
+++ b/tests/unit/container/test_decorator.py
@@ -33,6 +33,36 @@ def test_simple():
         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)
diff --git a/tests/unit/container/test_resolve.py b/tests/unit/container/test_resolve.py
index 230d018..357f2bc 100644
--- a/tests/unit/container/test_resolve.py
+++ b/tests/unit/container/test_resolve.py
@@ -8,6 +8,7 @@ from dishka import (
     provide,
 )
 from ..sample_providers import (
+    A_VALUE,
     ClassA,
     async_func_a,
     async_gen_a,
@@ -15,6 +16,7 @@ from ..sample_providers import (
     sync_func_a,
     sync_gen_a,
     sync_iter_a,
+    value_factory,
 )
 
 
@@ -66,3 +68,20 @@ async def test_async(factory, closed):
         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
diff --git a/tests/unit/sample_providers.py b/tests/unit/sample_providers.py
index abfb3e1..2b29221 100644
--- a/tests/unit/sample_providers.py
+++ b/tests/unit/sample_providers.py
@@ -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:
@@ -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,
+)
diff --git a/tests/unit/test_provider.py b/tests/unit/test_provider.py
index 578fe02..f0ea535 100644
--- a/tests/unit/test_provider.py
+++ b/tests/unit/test_provider.py
@@ -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