Skip to content

Commit

Permalink
Merge pull request #65 from reagento/develop
Browse files Browse the repository at this point in the history
v0.4
  • Loading branch information
Tishka17 authored Feb 20, 2024
2 parents c5da559 + 34929c7 commit 1d296a8
Show file tree
Hide file tree
Showing 55 changed files with 822 additions and 463 deletions.
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# project generated files
__pycache__/
/docs-build/
/dist/
/build/
/.ruff_cache/
/.tox/
.coverage
*.egg-info

# common utilities
/venv
/.venv
.mypy_cache/
.idea/
.vscode/
113 changes: 83 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,97 @@ 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()
```
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.

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)
```

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.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. Container holds dependencies cache and is used to retrieve them. Here, you can use `.get` method to access APP-scoped dependencies:

```python
from dishka import make_container
container = make_container(provider) # it has Scope.APP
a = container.get(A) # `A` has Scope.APP, so it is accessible here

@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.
5. You can enter and exit `REQUEST` scope multiple times after that:

```python
with make_container(MyProvider()) as container: # enter Scope.APP
with container() as request_container: # enter Scope.REQUEST
...
from dishka import make_container
container = make_container(provider)
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`
```

5. Call `get` to get dependency and use context manager to get deeper through scopes
6. Close container in the end:

```python
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
container.close()
```

6. Add decorators and middleware for your framework (_would be described soon_)
7. If you are using supported framework add decorators and middleware for it.

```python
from dishka.integrations.fastapi import (
Depends, inject, setup_dishka,
)

See [examples](examples)
@router.get("/")
@inject
async def index(a: Annotated[A, Depends()]) -> str:
...

...

setup_dishka(container, app)
```

### Concepts

Expand All @@ -82,7 +135,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 Expand Up @@ -147,20 +200,20 @@ class MyProvider(Provider):
async def get_a(self) -> A:
return A()

async with make_async_container(MyProvider()) as container:
a = await container.get(A)
container = make_async_container(MyProvider())
a = await container.get(A)
```

* Having some data connected with scope which you want to use when solving dependencies? Set it when entering scope. These classes can be used as parameters of your `provide` methods
```python
with make_container(MyProvider(), context={App: app}) as container:
with container(context={RequestClass: request_instance}) as request_container:
pass
container = make_async_container(MyProvider(), context={App: app})
with container(context={RequestClass: request_instance}) as request_container:
pass
```

* Having to many dependencies? Or maybe want to replace only part of them in tests keeping others? Create multiple `Provider` classes
```python
with make_container(MyProvider(), OtherProvider()) as container:
container = make_container(MyProvider(), OtherProvider())
```

* Tired of providing `scope==` for each depedency? Set it inside your `Provider` class and all dependencies with no scope will use it.
Expand Down
21 changes: 13 additions & 8 deletions docs/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ Standard scopes are:

You decide when to enter and exit them, but it is done one by one. If you entered ``APP`` scope then the next step deeper will enter ``REQUEST`` scope.

.. note::
``APP`` scope can be used for lazy initialisation of singletons, while ``REQUEST`` scope is good for processing events like HTTP-requests or messenger updates. It is unlikely that you will need other scopes


In dishka dependencies are lazy - they are created when you first request them. If the same dependency is requested multiple time within one scope then the same instance is returned (you can disable it for each dependency separately). A created dependency is kept until you exit the scope. And in that moment it is not just dropped away, but corresponding finalization steps are done. You can enter same scope multiple times concurrently so to have multiple instances of objects you can work simultaneously.

Each object can depend on other objects from the same or previous scopes. So, if you have ``Config`` with scope of *APP* and ``Connection`` with scope of *REQUEST* you cannot have an *APP*-scoped object with requires a connection, but you can have *REQUEST*-scoped object which requires a ``Connection`` or a ``Config`` (or even both).
Each object can depend on other objects from the same or previous scopes. So, if you have ``Config`` with scope of *APP* and ``Connection`` with scope of *REQUEST* you cannot have an *APP*-scoped object which requires a connection, but you can have *REQUEST*-scoped object which requires a ``Connection`` or a ``Config`` (or even both).

If you are developing web application, you would enter ``APP`` scope on startup, and you would ``REQUEST`` scope in each HTTP-request.

Expand All @@ -55,11 +59,13 @@ According to scopes order container can be used to get dependencies from its and

.. code-block:: python
with make_container(provider1, provider2) as app_container: # enter APP scope
config = app_container.get(Config) # APP-scoped object
with container() as request_container: # enter REQUEST scope
connection = request_container.get(Connection) # REQUEST-scoped object
config = request_container.get(Config) # APP-scoped object
app_container = make_container(provider1, provider2) # enter APP scope
config = app_container.get(Config) # APP-scoped object
with container() as request_container: # enter REQUEST scope
connection = request_container.get(Connection) # REQUEST-scoped object
config = request_container.get(Config) # APP-scoped object
Async container is working in the same manner, but you should use async context manager and await the result of get

Expand All @@ -75,8 +81,7 @@ Provider
class MyProvider(Provider):
pass
with make_container(MyProvider()) as container:
pass
container = make_container(MyProvider())
There are 3 special functions:
Expand Down
70 changes: 48 additions & 22 deletions docs/container/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,68 @@ Container can be synchronous or asynchronous.
* *Async* container can use any type of dependency sources: both sync and async are supported. Sync methods are called directly and no executors are used, so avoid network I/O in synchronous functions
* *Sync* container can use only synchronous dependency sources.

To create a top level container you should call ``make_container`` (or ``make_async_container``) and use it as a context manager. Pass there one or more providers.
To create a top level container you should call ``make_container`` (or ``make_async_container``). Pass there one or more providers.

.. code-block:: python
from dishka import make_container
with make_container(provider) as container:
...
container = make_container(provider)
And async version correspondingly:

.. code-block:: python
from dishka import make_container
async with make_async_container(provider) as container:
...
container = await make_async_container(provider)
If you have not provided your own *scopes* enum, then default one will be used. Root container is attached to the first scope: Scope.APP by default.

To enter the next scope you should call container as a function and enter context manager (same sync or async):
To enter the next scope you should call container as a function and enter context manager:

.. code-block:: python
with container() as nested_container:
pass
Container as needed for retrieving objects. To do it you need call ``get(DependencyType)`` (and ``await`` it for async container).
or if you created *async* container:

.. code-block:: python
async with container() as nested_container:
pass
Container is needed for retrieving objects. To do it you need to call ``get(DependencyType)`` (and ``await`` it for async container).
All retrieved dependencies are stored inside container of corresponding scope until you exit that scope. So, you if you call ``get`` multiple times you will receive the same instance. The rule is followed for indirect dependencies as well. Multiple dependencies of the same scope have their own cache.

.. code-block:: python
with make_container(provider) as container:
a = container.get(A)
a = container.get(A) # same instance
container = make_container(provider)
a = container.get(A)
a = container.get(A) # same instance
And async:

.. code-block:: python
container = make_async_container(provider)
a = await container.get(A)
a = await container.get(A) # same instance
Whe you exit the scope, dependency cache is cleared. Finalization of dependencies is done if you used generator factories.

APP-level container is not a context manager, so call ``.close()`` on your app termination

.. code-block:: python
container.close()
And async:

.. code-block:: python
await container.close()
Whe you exit the scope, dependency cache is cleared. Finalization of dependencies is done if you used generator factories
Thread/task safety
==========================
Expand All @@ -57,32 +84,30 @@ For example, if you have declared ``SessionPool`` as an APP-scoped dependency an

To prevent such a condition you need to protect any session whose children can be used concurrently: to pass ``lock_factory`` when creating a container. Do not mix up threading and asyncio locks: they are not interchangeable, use the proper one.


.. code-block:: python
import threading
with make_container(provider, lock_factory=threading.Lock) as container:
with container(lock_factory=threading.Lock) as nested_container:
...
container = make_container(provider, lock_factory=threading.Lock):
with container(lock_factory=threading.Lock) as nested_container:
...
.. code-block:: python
import asyncio
async with make_async_container(provider, lock_factory=asyncio.Lock) as container:
async with container(lock_factory=asyncio.Lock) as nested_container:
...
container = await make_async_container(provider, lock_factory=asyncio.Lock)
async with container(lock_factory=asyncio.Lock) as nested_container:
...
.. note::
Do not worry, lock is set by default for top level (``Scope.APP``) container. So, if you are not using other scopes concurrently you do not need any changes. (E.g. if you are not using multiple ``Scope.ACTION`` containers at a same time within one ``Scope.REQUEST`` container)

Context data
====================

Often, you scopes are assigned with some external events: HTTP-requests, message from queue, callbacks from framework. You can use those objects when creating dependencies. The difference from normal factories is that they are not created inside some ``Provder``, but passed to the scope:
Often, your scopes are assigned with some external events: HTTP-requests, message from queue, callbacks from framework. You can use those objects when creating dependencies. The difference from normal factories is that they are not created inside some ``Provider``, but passed to the scope:

.. code-block:: python
Expand All @@ -93,8 +118,9 @@ Often, you scopes are assigned with some external events: HTTP-requests, message
def a(self, request: Request) -> A:
return A(data=request.contents)
container = make_container(MyProvider())
while True:
request = connection.recv()
with container(context={Request:request}) as request_container:
a = request_container.get(A)
Loading

0 comments on commit 1d296a8

Please sign in to comment.