Skip to content

Commit

Permalink
Merge pull request #61 from reagento/feature/provider_methods
Browse files Browse the repository at this point in the history
add provider methods
  • Loading branch information
Tishka17 authored Feb 20, 2024
2 parents ab23b9e + 26bada9 commit 4caaf8e
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 62 deletions.
91 changes: 70 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions docs/provider/alias.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions docs/provider/decorate.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
85 changes: 76 additions & 9 deletions docs/provider/index.rst
Original file line number Diff line number Diff line change
@@ -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:

Expand Down
4 changes: 3 additions & 1 deletion docs/provider/provide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
58 changes: 39 additions & 19 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading

0 comments on commit 4caaf8e

Please sign in to comment.