Repository: reagento/dishka
Pull request #61 (2024-02-19 20:32:42)

add provider methods
* [x] doc


Patch:
diff --git a/README.md b/README.md
index b613a0b..da8783a 100644
--- a/README.md
+++ b/README.md
@@ -24,44 +24,93 @@ See more in [technical requirements](https://dishka.readthedocs.io/en/latest/req
 
 ### Quickstart
 
-1. Create Provider subclass. 
+1. Install dishka
+
+```shell
+pip install dishka
+```
+
+2. Create `Provider` instance. It is only used co setup all factories providing your objects.
+
 ```python
 from dishka import Provider
-class MyProvider(Provider):
-   ...
+
+provider = Provider()
+```
+
+3. Register functions which provide dependencies. Do not forget to place correct typehints for parameters and result. We use `scope=Scope.APP` for dependencies which ar created only once in applicaiton lifetime, and `scope=Scope.REQUEST` for those which should be recreated for each processing request/event/etc.
+
+```python
+from dishka import Provider, Scope
+
+def get_a() -> A:
+   return A()
+
+def get_b(a: A) -> B:
+   return B(a)
+
+provider = Provider()
+provider.provide(get_a, scope=Scope.APP)
+provider.provide(get_b, scope=Scope.REQUEST)
 ```
-2. Mark methods which actually create dependencies with `@provide` decorator with carefully arranged scopes. Do not forget to place correct typehints for parameters and result.
-Here we describe how to create instances of A and B classes, where B class requires itself an instance of A.
+
+This can be also rewritten using classes:
+
 ```python
 from dishka import provide, Provider, Scope
-class MyProvider(Provider):
-   @provide(scope=Scope.APP)
-   def get_a(self) -> A:
-      return A()
 
-   @provide(scope=Scope.REQUEST)
-   def get_b(self, a: A) -> B:
-      return B(a)
+class MyProvider(Provider):
+  @provide(scope=Scope.APP)
+  def get_a(self) -> A:
+     return A()
+  
+  @provide(scope=Scope.REQUEST)
+  def get_b(self, a: A) -> B:
+     return B(a)
+
+provider = MyProvider()
 ```
-4. Create Container instance passing providers, and step into `APP` scope. Or deeper if you need.
+
+4. Create Container instance passing providers, and step into `APP` scope. Container holds dependencies cache and is used to retrieve them. Here, you can use `.get` method to access APP-scoped dependencies:
+
 ```python
-with make_container(MyProvider()) as container:  # enter Scope.APP
-     with container() as request_container:   # enter Scope.REQUEST
-          ...
+from dishka import make_container
+with make_container(provider) as container:  # enter Scope.APP
+     a = container.get(A)  # `A` has Scope.APP, so it is accessible here
+
 ```
+5. You can enter and exit `REQUEST` scope multiple times after that:
 
-5. Call `get` to get dependency and use context manager to get deeper through scopes
 ```python
+from dishka import make_container
 with make_container(MyProvider()) as container:
-     a = container.get(A)  # `A` has Scope.APP, so it is accessible here
      with container() as request_container:
           b = request_container.get(B)  # `B` has Scope.REQUEST
           a = request_container.get(A)  # `A` is accessible here too
+
+     with container() as request_container:
+          b = request_container.get(B)  # another instance of `B`
+          a = request_container.get(A)  # the same instance of `A`
 ```
 
-6. Add decorators and middleware for your framework (_would be described soon_)
+6. If you are using supported framework add decorators and middleware for it.
 
-See [examples](examples)
+```python
+from dishka.integrations.fastapi import (
+    Depends, inject, DishkaApp,
+)
+
+@router.get("/")
+@inject
+async def index(a: Annotated[A, Depends()]) -> str:
+    ...
+
+...
+app = DishkaApp(
+    providers=[MyProvider()],
+    app=app,
+)
+```
 
 ### Concepts
 
@@ -82,7 +131,7 @@ You can provide your own Scopes class if you are not satisfied with standard flo
 
 
 **Provider** is a collection of functions which really provide some objects. 
-Provider itself is a class with some attributes and methods. Each of them is either result of `provide`, `alias` or `decorate`.
+Provider itself is a class with some attributes and methods. Each of them is either result of `provide`, `alias` or `decorate`. They can be used as provider methods, functions to assign attributes or method decorators.
 
 `@provide` can be used as a decorator for some method. This method will be called when corresponding dependency has to be created. Name of the method is not important: just check that it is different form other `Provider` attributes. Type hints do matter: they show what this method creates and what does it require. All method parameters are treated as other dependencies and created using container.
 
diff --git a/docs/provider/alias.rst b/docs/provider/alias.rst
index 18707c1..85eea7a 100644
--- a/docs/provider/alias.rst
+++ b/docs/provider/alias.rst
@@ -5,6 +5,8 @@
 
 ``alias`` is used to allow retrieving of the same object by different type hints. E.g. you have configured how to provide ``A`` object and want to use it as AProtocol: ``container.get(A)==container.get(AProtocol)``.
 
+Provider object has also a ``.alias`` method with the same logic.
+
 .. code-block:: python
 
     class MyProvider(Provider):
diff --git a/docs/provider/decorate.rst b/docs/provider/decorate.rst
index 695c936..606a492 100644
--- a/docs/provider/decorate.rst
+++ b/docs/provider/decorate.rst
@@ -5,8 +5,11 @@
 
 ``decorate`` is used to modify or wrap an object which is already configured in another ``Provider``.
 
+Provider object has also a ``.decorate`` method with the same logic.
+
 If you want to apply decorator pattern and do not want to alter existing provide method, then it is a place for ``decorate``. It will construct object using earlier defined provider and then pass it to your decorator before returning from the container.
 
+
 .. code-block:: python
 
     class MyProvider(Provider):
diff --git a/docs/provider/index.rst b/docs/provider/index.rst
index 9b97bcc..79a7029 100644
--- a/docs/provider/index.rst
+++ b/docs/provider/index.rst
@@ -1,33 +1,100 @@
 Provider
 ****************
 
-**Provider** is an object which members are used to construct dependencies. Providers are needed for container to create dependencies.
+**Provider** is an object which members are used to construct dependencies. ``Provider`` contains different factories and other entities and then is used to create a ``Container``. You can have multiple providers in one application and combine them in different ways to make it more modular.
 
-To create your own provider you inherit from ``Provider`` class and instantiate it when creating a container:
+To configure provider you can either inherit and use decorators on you methods or just create an instance and use its methods.
+
+For example, imagine you have two classes: connection which is retrieved from external library and a gateway which requires such a connection.
+
+.. code-block:: python
+
+    class Connection:
+        pass
+
+    class Gateway:
+        def __init__(self, conn: Connection):
+            pass
+
+You can configure ``Provider`` with code like this:
 
 .. code-block:: python
 
+    from dishka import make_container, Provider, Scope
+
+    def get_connection() -> Iterable[Connection]:
+        conn = connect(uri)
+        yield conn
+        conn.close()
+
+    provider = Provider(scope=Scope.APP)
+    provider.provide(get_connection)
+    provider.provide(Gateway)
+
+    with make_container(provider) as container:
+        ...
+
+Or using inheritance:
+
+.. code-block:: python
+
+    from dishka import make_container, Provider, provide, Scope
+
     class MyProvider(Provider):
+        @provide
+        def get_connection(self) -> Iterable[Connection]:
+            conn = connect(uri)
+            yield conn
+            conn.close()
+
+        gateway = provider.provide(Gateway)
+
+    with make_container(MyProvider(scope=Scope.APP)) as container:
         pass
 
-    with make_container(MyProvider()) as container:
+You class-based provider can have ``__init__`` method and methods access ``self`` as usual. It can be useful for passing configuration:
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        def __init__(self, uri: str, scope: Scope):
+            super().__init__(scope=scope)  # do not forget `super`
+            self.uri = uri
+
+        @provide
+        def get_connection(self) -> Iterable[Connection]:
+            conn = connect(self.uri)  #
+            yield conn
+            conn.close()
+
+        gateway = provide(Gateway)
+
+    provider = MyProvider(uri=os.getenv("DB_URI"), scope=Scope.APP)
+    with make_container(provider) 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.
 
-* Inside class:
+Dependencies have scope and there are three places to set it (according to their priority):
+
+* When registering single factory passing to ``provide`` method
 
 .. code-block:: python
 
     class MyProvider(Provider):
-        scope=Scope.APP
+        gateway = provide(Gateway, scope=Scope.APP)
 
-* Or when instantiating it. This can be also useful for tests to override provider scope.
+* When instantiating provider:
 
 .. code-block:: python
 
-    with make_container(MyProvider(scope=Scope.APP)) as container:
-        pass
+    provider = Provider(scope=Scope.APP)
+
+* Inside class:
+
+.. code-block:: python
+
+    class MyProvider(Provider):
+        scope=Scope.APP
 
 Though it is a normal object, not all attributes are analyzed by ``Container``, but only those which are marked with special functions:
 
diff --git a/docs/provider/provide.rst b/docs/provider/provide.rst
index 47c7614..ccc457c 100644
--- a/docs/provider/provide.rst
+++ b/docs/provider/provide.rst
@@ -3,7 +3,9 @@
 @provide
 ******************
 
-``provide`` function is used to declare a factory providing a dependency. It can be used with some class or as a method decorator. In second case it can be sync or async method. Also, it can support finalization of dependency if you make it a generator.
+``provide`` function is used to declare a factory providing a dependency. It can be used with some class or as a method decorator (either sync or async). It supports finalization of dependency if you make it a generator.
+
+Provider object has also a ``.provide`` method with the same logic.
 
 If it is used with class, it analyzes its ``__init__`` typehints to detect its dependencies. If it is used with method, it checks its parameters typehints and a result type. Last one describes what this method is used to create.
 
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 4e44962..179947a 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -8,50 +8,70 @@ Quickstart
     pip install dishka
 
 
-2. Create Provider subclass.
+2. Create Provider instance. It is only used co setup all factories providing your objects.
 
 .. code-block:: python
 
     from dishka import Provider
-    class MyProvider(Provider):
-       ...
 
-3. Mark methods which actually create dependencies with `@provide` decorator with carefully arranged scopes. Do not forget to place correct typehints for parameters and result.
-Here we describe how to create instances of A and B classes, where B class requires itself an instance of A.
+    provider = Provider()
+
+3. Register functions which provide dependencies. Do not forget to place correct typehints for parameters and result. We use ``scope=Scope.APP`` for dependencies which ar created only once in applicaiton lifetime, and ``scope=Scope.REQUEST`` for those which should be recreated for each processing request/event/etc.
+
+.. code-block:: python
+
+   from dishka import Provider, Scope
+
+   def get_a() -> A:
+       return A()
+
+   def get_b(a: A) -> B:
+       return B(a)
+
+   provider = Provider()
+   provider.provide(get_a, scope=Scope.APP)
+   provider.provide(get_b, scope=Scope.REQUEST)
+
+This can be also rewritten using class:
 
 .. code-block:: python
 
-    from dishka import provide, Provider, Scope
-    class MyProvider(Provider):
-       @provide(scope=Scope.APP)
-       def get_a(self) -> A:
-          return A()
+   from dishka import provide, Provider, Scope
+
+   class MyProvider(Provider):
+      @provide(scope=Scope.APP)
+      def get_a(self) -> A:
+         return A()
 
-       @provide(scope=Scope.REQUEST)
-       def get_b(self, a: A) -> B:
-          return B(a)
+      @provide(scope=Scope.REQUEST)
+      def get_b(self, a: A) -> B:
+         return B(a)
 
-4. Create Container instance passing providers, and step into `APP` scope. Or deeper if you need.
+   provider = MyProvider()
+
+4. Create Container instance passing providers, and step into ``APP`` scope. Container holds dependencies cache and is used to retrieve them. Here, you can use ``.get`` method to access APP-scoped dependencies:
 
 .. code-block:: python
 
     from dishka import make_container
-    with make_container(MyProvider()) as container:  # enter Scope.APP
-         with container() as request_container:   # enter Scope.REQUEST
-              ...
+    with make_container(provider) as container:  # enter Scope.APP
+         a = container.get(A)  # `A` has Scope.APP, so it is accessible here
 
 
-5. Call `get` to get dependency and use context manager to get deeper through scopes
+5. You can enter and exit ``REQUEST`` scope multiple times after that:
 
 .. code-block:: python
 
     from dishka import make_container
     with make_container(MyProvider()) as container:
-         a = container.get(A)  # `A` has Scope.APP, so it is accessible here
          with container() as request_container:
               b = request_container.get(B)  # `B` has Scope.REQUEST
               a = request_container.get(A)  # `A` is accessible here too
 
+         with container() as request_container:
+              b = request_container.get(B)  # another instance of `B`
+              a = request_container.get(A)  # the same instance of `A`
+
 
 6. If you are using supported framework add decorators and middleware for it.
 
diff --git a/src/dishka/dependency_source.py b/src/dishka/dependency_source.py
index 77e8bca..58b489e 100644
--- a/src/dishka/dependency_source.py
+++ b/src/dishka/dependency_source.py
@@ -13,6 +13,7 @@ from inspect import (
     iscoroutinefunction,
     isfunction,
     isgeneratorfunction,
+    ismethod,
     signature,
 )
 from typing import (
@@ -46,6 +47,8 @@ class FactoryType(Enum):
     ASYNC_FACTORY = "async_factory"
     VALUE = "value"
 
+def _is_bound_method(obj):
+    return ismethod(obj) and obj.__self__
 
 def _identity(x: Any) -> Any:
     return x
@@ -81,10 +84,12 @@ class Factory:
             return self
         if self.is_to_bound:
             source = self.source.__get__(instance, owner)
+            dependencies = self.dependencies[1:]
         else:
             source = self.source
+            dependencies = self.dependencies[:]
         return Factory(
-            dependencies=self.dependencies,
+            dependencies=dependencies,
             source=source,
             provides=self.provides,
             scope=scope,
@@ -175,14 +180,20 @@ def make_factory(
             params = signature(source).parameters
             factory_type = _guess_factory_type(source)
 
-        self = next(iter(params.values()))
         hints = get_type_hints(source, include_extras=True)
-        hints.pop(self.name, None)
+        self = next(iter(params.values()), None)
+        if self:
+            if self.name not in hints:
+                # add self to dependencies, so it can be easily removed
+                # if we will bind factory to provider instance
+                hints = {self.name: Any, **hints}
+            is_to_bind = True
+        else:
+            is_to_bind = False
         possible_dependency = hints.pop("return", None)
         dependencies = list(hints.values())
         if not provides:
             provides = _clean_result_hint(factory_type, possible_dependency)
-        is_to_bind = True
     elif isinstance(source, staticmethod):
         factory_type = _guess_factory_type(source.__wrapped__)
         hints = get_type_hints(source, include_extras=True)
@@ -192,14 +203,19 @@ def make_factory(
             provides = _clean_result_hint(factory_type, possible_dependency)
         is_to_bind = False
     elif callable(source):
+        if _is_bound_method(source):
+            to_check = source.__func__
+        else:
+            to_check = type(source).__call__
         factory = make_factory(
             provides=provides,
-            source=type(source).__call__,
+            source=to_check,
             cache=cache,
             scope=scope,
         )
         factory_type = factory.type
-        dependencies = factory.dependencies
+        if factory.is_to_bound:
+            dependencies = factory.dependencies[1:]  # remove `self`
         provides = factory.provides
         is_to_bind = False
     else:
@@ -230,7 +246,7 @@ def provide(
 def provide(
         source: Callable | classmethod | staticmethod | Type | None,
         *,
-        scope: BaseScope,
+        scope: BaseScope | None = None,
         provides: Any = None,
         cache: bool = True,
 ) -> Factory:
diff --git a/src/dishka/provider.py b/src/dishka/provider.py
index 1eb34f7..f695db1 100644
--- a/src/dishka/provider.py
+++ b/src/dishka/provider.py
@@ -1,7 +1,15 @@
 import inspect
-from typing import Any, List
+from typing import Any, Callable, List, Type
 
-from .dependency_source import Alias, Decorator, DependencySource, Factory
+from .dependency_source import (
+    Alias,
+    Decorator,
+    DependencySource,
+    Factory,
+    alias,
+    decorate,
+    provide,
+)
 from .exceptions import InvalidGraphError
 from .scope import BaseScope
 
@@ -50,3 +58,50 @@ class Provider:
             if isinstance(source, Decorator):
                 self.decorators.append(source)
             processed_types[source.provides] = name
+
+    def provide(
+            self,
+            source: Callable | Type,
+            *,
+            scope: BaseScope | None = None,
+            provides: Any = None,
+            cache: bool = True,
+    ) -> Factory:
+        if scope is None:
+            scope = self.scope
+        new_factory = provide(
+            source=source,
+            scope=scope,
+            provides=provides,
+            cache=cache,
+        )
+        self.factories.append(new_factory)
+        return new_factory
+
+    def alias(
+            self,
+            *,
+            source: Type,
+            provides: Type,
+            cache: bool = True,
+    ) -> Alias:
+        new_alias = alias(
+            source=source,
+            provides=provides,
+            cache=cache,
+        )
+        self.aliases.append(new_alias)
+        return new_alias
+
+    def decorate(
+            self,
+            source: Callable | Type,
+            *,
+            provides: Any = None,
+    ) -> Decorator:
+        new_decorator = decorate(
+            source=source,
+            provides=provides,
+        )
+        self.aliases.append(new_decorator)
+        return new_decorator


Test patch:
diff --git a/tests/unit/container/test_resolve.py b/tests/unit/container/test_resolve.py
index 357f2bc..08fd248 100644
--- a/tests/unit/container/test_resolve.py
+++ b/tests/unit/container/test_resolve.py
@@ -85,3 +85,29 @@ async def test_value_async():
 
     async with make_async_container(MyProvider()) as container:
         assert await container.get(ClassA) is A_VALUE
+
+
+class OtherClass:
+    def method(self) -> ClassA:
+        return A_VALUE
+
+    @classmethod
+    def classmethod(cls) -> ClassA:
+        return A_VALUE
+
+    @staticmethod
+    def staticmethod() -> ClassA:
+        return A_VALUE
+
+
+@pytest.mark.parametrize("method", [
+    OtherClass().method,
+    OtherClass().classmethod,
+    OtherClass().staticmethod,
+])
+def test_external_method(method):
+    provider = Provider(scope=Scope.APP)
+    provider.provide(method)
+
+    with make_container(provider) as container:
+        assert container.get(ClassA) is A_VALUE
diff --git a/tests/unit/sample_providers.py b/tests/unit/sample_providers.py
index 56791d7..eb75342 100644
--- a/tests/unit/sample_providers.py
+++ b/tests/unit/sample_providers.py
@@ -1,4 +1,4 @@
-from typing import AsyncGenerator, AsyncIterable, Generator, Iterable
+from typing import Any, AsyncGenerator, AsyncIterable, Generator, Iterable
 
 from dishka import Scope
 from dishka.dependency_source import Factory, FactoryType
@@ -10,7 +10,7 @@ class ClassA:
         self.closed = False
 
 
-def sync_func_a(self, dep: int) -> ClassA:
+def sync_func_a(self: Any, dep: int) -> ClassA:
     return ClassA(dep)
 
 
diff --git a/tests/unit/test_provider.py b/tests/unit/test_provider.py
index 1803e05..3724362 100644
--- a/tests/unit/test_provider.py
+++ b/tests/unit/test_provider.py
@@ -1,3 +1,5 @@
+from typing import Any
+
 import pytest
 
 from dishka import Provider, Scope, alias, provide
@@ -28,7 +30,6 @@ def test_provider_init():
 
 @pytest.mark.parametrize(
     "source, provider_type, is_to_bound", [
-        (ClassA, FactoryType.FACTORY, False),
         (sync_func_a, FactoryType.FACTORY, True),
         (sync_iter_a, FactoryType.GENERATOR, True),
         (sync_gen_a, FactoryType.GENERATOR, True),
@@ -38,6 +39,21 @@ def test_provider_init():
     ],
 )
 def test_parse_factory(source, provider_type, is_to_bound):
+    factory = provide(source, scope=Scope.REQUEST)
+    assert factory.provides == ClassA
+    assert factory.dependencies == [Any, int]
+    assert factory.is_to_bound == is_to_bound
+    assert factory.scope == Scope.REQUEST
+    assert factory.source == source
+    assert factory.type == provider_type
+
+
+@pytest.mark.parametrize(
+    "source, provider_type, is_to_bound", [
+        (ClassA, FactoryType.FACTORY, False),
+    ],
+)
+def test_parse_factory_cls(source, provider_type, is_to_bound):
     factory = provide(source, scope=Scope.REQUEST)
     assert factory.provides == ClassA
     assert factory.dependencies == [int]
@@ -123,3 +139,34 @@ def test_callable():
     provider = MyProvider(scope=Scope.REQUEST)
     assert provider.foo.provides == str
     assert provider.foo.dependencies == [int]
+
+
+def test_provide_as_method():
+    provider = Provider(scope=Scope.REQUEST)
+    foo = provider.provide(MyCallable())
+    assert foo.provides == str
+    assert foo.dependencies == [int]
+
+    foo = provider.provide(sync_func_a)
+    assert foo.provides == ClassA
+    assert foo.dependencies == [Any, int]
+
+    foo = provider.alias(source=int, provides=str)
+    assert foo.provides == str
+    assert foo.source == int
+
+    foo = provider.decorate(sync_func_a)
+    assert foo.provides == ClassA
+    assert foo.factory.dependencies == [Any, int]
+
+
+class OtherClass:
+    def method(self) -> str:
+        pass
+
+
+def test_provide_external_method():
+    provider = Provider(scope=Scope.REQUEST)
+    foo = provider.provide(OtherClass().method)
+    assert foo.provides == str
+    assert foo.dependencies == []