Skip to content

Commit

Permalink
Merge pull request #285 from reagento/develop
Browse files Browse the repository at this point in the history
v1.4
  • Loading branch information
Tishka17 authored Oct 21, 2024
2 parents b59711e + 58bd49c commit 7ef8701
Show file tree
Hide file tree
Showing 134 changed files with 4,374 additions and 1,080 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/frameworks-latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- "3.10"
- "3.11"
- "3.12"
- "3.13.0-rc.1"
- "3.13"

steps:
- uses: actions/checkout@v4
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ jobs:
- name: Run tests
run: |
tox
- name: Build doc
run: |
pip install -r requirements_doc.txt
sphinx-build -M html docs docs-build
1 change: 1 addition & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ lint.ignore = [
"UP038",
"TCH001",
"SIM103",
"ISC003",
]

[lint.per-file-ignores]
Expand Down
231 changes: 69 additions & 162 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ Cute DI framework with scopes and agreeable API.

### Purpose

This library is targeting to provide only an IoC-container but make it really useful. If you are tired manually passing objects to create others objects which are only used to create more objects - we have a solution. Not all project require an IoC-container, but check what we have.
This library is targeting to provide only an IoC-container but tries to make it really useful.
If you are tired of passing objects manually to create other objects which are only used to create more objects - we have a solution.
Not all projects require an IoC-container, but check what we have.

Unlike other instruments we are not trying to solve tasks not related to dependency injection. We want to keep DI in place, not soiling you code with global variables and additional specifiers in all places.
Unlike other instruments we are not trying to solve tasks not related to [dependency injection](https://dishka.readthedocs.io/en/latest/di_intro.html).
Instead, we want to keep DI in place, not soiling your code with global variables and additional specifiers scattered everywhere.

Main ideas:
* **Scopes**. Any object can have lifespan of the whole app, single request or even more fractionally. Many frameworks do not have scopes or have only 2 of them. Here you can have as many scopes as you need.
* **Finalization**. Some dependencies like database connections must be not only created, but carefully released. Many framework lack this essential feature
* **Modular providers**. Instead of creating lots of separate functions or contrariwise a big single class, you can split your factories into several classes, which makes them simpler reusable.
* **Clean dependencies**. You do not need to add custom markers to the code of dependencies so to allow library to see them. All customization is done within providers code and only borders of scopes have to deal with library API.
* **Finalization**. Some dependencies like database connections must not only be created, but carefully released. Many framework lack this essential feature
* **Modular providers**. Instead of creating lots of separate functions or contrariwise a big single class, you can split your factories into several classes, which makes them simpler to reuse.
* **Clean dependencies**. You do not need to add custom markers to the code of dependencies just to allow library to see them. All customization is done within providers code and only the borders of scopes have to deal with library API.
* **Simple API**. You need minimum of objects to start using library. You can easily integrate it with your task framework, examples provided.
* **Speed**. It is fast enough so you not to worry about. It is even faster than many of the analogs.
* **Speed**. It is fast enough so you do not have to worry about it. It is even faster than many of the analogs.

See more in [technical requirements](https://dishka.readthedocs.io/en/latest/requirements/technical.html)

Expand All @@ -36,76 +39,100 @@ See more in [technical requirements](https://dishka.readthedocs.io/en/latest/req
pip install dishka
```

2. Create `Provider` instance. It is only used to setup all factories providing your objects.
2. Write your classes, fill type hints. Imagine, you have two classes: Service (kind of business logic) and DAO (kind of data access) and some external api client:

```python
class DAO(Protocol):
...

class Service:
def __init__(self, dao: DAO):
...

class DAOImpl(DAO):
def __init__(self, connection: Connection):
...

class SomeClient:
...
```

4. Create Provider instance. It is only used to setup all factories providing your objects.

```python
from dishka import 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 are created only once in application lifetime, and `scope=Scope.REQUEST` for those which should be recreated for each processing request/event/etc.

5. Setup how to provide dependencies.

We use `scope=Scope.APP` for dependencies which are created only once in application lifetime,
and `scope=Scope.REQUEST` for those which should be recreated for each processing request/event/etc.
To read more about scopes, refer [documentation](https://dishka.readthedocs.io/en/latest/advanced/scopes.html)

```python
from dishka import Provider, Scope

def get_a() -> A:
return A()
service_provider = Provider(scope=Scope.REQUEST)
service_provider.provide(Service)
service_provider.provide(DAOImpl, provides=DAO)
service_provider.provide(SomeClient, scope=Scope.APP) # override provider scope
```

def get_b(a: A) -> B:
return B(a)
To provide connection we might need to write some custom code:

provider = Provider()
provider.provide(get_a, scope=Scope.APP)
provider.provide(get_b, scope=Scope.REQUEST)
```python
from dishka import Provider, provide, Scope

class ConnectionProvider(Provider):
@provide(Scope=Scope.REQUEST)
def new_connection(self) -> Connection:
conn = sqlite3.connect()
yield conn
conn.close()
```

This can be also rewritten using classes:
6. Create main `Container` instance passing providers, and step into `APP` scope.

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

provider = MyProvider()
from dishka import make_container

container = make_container(service_provider, ConnectionProvider())
```

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:
7. 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
client = container.get(SomeClient) # `SomeClient` has Scope.APP, so it is accessible here
client = container.get(SomeClient) # same instance of `SomeClient`
```

5. You can enter and exit `REQUEST` scope multiple times after that:

8. You can enter and exit `REQUEST` scope multiple times after that using context manager:

```python
from dishka import make_container
container = make_container(provider)
# subcontainer to access more short-living objects
with container() as request_container:
b = request_container.get(B) # `B` has Scope.REQUEST
a = request_container.get(A) # `A` is accessible here too
service = request_container.get(Service)
service = request_container.get(Service) # same service instance
# at this point connection will be closed as we exited context manager

# new subcontainer to have a new lifespan for request processing
with container() as request_container:
b = request_container.get(B) # another instance of `B`
a = request_container.get(A) # the same instance of `A`
service = request_container.get(Service) # new service instance
```

6. Close container in the end:

9. Close container in the end:

```python
container.close()
```

7. If you are using supported framework add decorators and middleware for it.
10. If you are using supported framework add decorators and middleware for it.
For more details see [integrations doc](https://dishka.readthedocs.io/en/latest/integrations/index.html)

```python
from dishka.integrations.fastapi import (
Expand All @@ -114,11 +141,10 @@ from dishka.integrations.fastapi import (

@router.get("/")
@inject
async def index(a: FromDishka[A]) -> str:
async def index(service: FromDishka[Service]) -> str:
...

...

setup_dishka(container, app)
```

Expand Down Expand Up @@ -149,122 +175,3 @@ If `provide` is used with some class then that class itself is treated as a fact
**Component** - is an isolated group of providers within the same container identified by a string. When dependency is requested it is searched only within the same component as its dependant, unless it is declared explicitly.

This allows you to have multiple parts of application build separately without need to think if the use same types.

### Tips

* Add method and mark it with `@provide` decorator. It can be sync or async method returning some value.

```python
class MyProvider(Provider):
@provide(scope=Scope.REQUEST)
def get_a(self) -> A:
return A()
```
* Want some finalization when exiting the scope? Make that method generator:

```python
class MyProvider(Provider):
@provide(scope=Scope.REQUEST)
def get_a(self) -> Iterable[A]:
a = A()
yield a
a.close()
```
* Do not have any specific logic and just want to create class using its `__init__`? then add a provider attribute using `provide` as function passing that class.

```python
class MyProvider(Provider):
a = provide(A, scope=Scope.REQUEST)
```
* Want to create a child class instance when parent is requested? add a `source` attribute to `provide` function with a parent class while passing child as a first parameter

```python
class MyProvider(Provider):
a = provide(source=AChild, scope=Scope.REQUEST, provides=A)
```
* Having multiple interfaces which can be created as a same class? Use `AnyOf`:

```python
from dishka import AnyOf

class MyProvider(Provider):
@provide
def p(self) -> AnyOf[A, AProtocol]:
return A()
```

Use alias if you want to add them in another `Provider`:

```python
class MyProvider2(Provider):
p = alias(source=A, provides=AProtocol)
```

In both cases it works the same way as

```python
class MyProvider2(Provider):
@provide(scope=<Scope of A>)
def p(self, a: A) -> AProtocol:
return a
```


* Want to apply decorator pattern and do not want to alter existing provide method? Use `decorate`. It will construct object using earlie defined provider and then pass it to your decorator before returning from the container.

```python
class MyProvider(Provider):
@decorate
def decorate_a(self, a: A) -> A:
return ADecorator(a)
```
Decorator function can also have additional parameters.

* Want to go `async`? Make provide methods asynchronous. Create async container. Use `async with` and await `get` calls:

```python
class MyProvider(Provider):
@provide(scope=Scope.APP)
async def get_a(self) -> A:
return 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. But you need to specify them in provider as retrieved form context.

```python
from dishka import from_context, Provider, provide, Scope

class MyProvider(Provider):
scope = Scope.REQUEST

app = from_context(App, scope=Scope.APP)
request = from_context(RequestClass)

@provide
def get_a(self, request: RequestClass, app: App) -> A:
...

container = make_container(MyProvider(), context={App: app})
with container(context={RequestClass: request_instance}) as request_container:
pass
```

* Having too many dependencies? Or maybe want to replace only part of them in tests keeping others? Create multiple `Provider` classes

```python
container = make_container(MyProvider(), OtherProvider())
```

* Tired of providing `scope` for each dependency? 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()
```
26 changes: 26 additions & 0 deletions docs/advanced/generics.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Generic types
=====================

You can use dishka with TypeVars and Generic-classes.

.. note::

Though generics are supported, there are some limitations:

* You cannot use TypeVar bounded to a Generic type
* Generic-decorators are only applied to concrete factories or factories with more narrow TypeVars

Creating objects with @provide
************************************

You can create generic factories, use ``type[T]`` to access resolved value of ``TypeVar``. Typevar can have bound or constraints, which are checked.
For example, here we have a factory providing instances of generic class ``A``. Note that ``A[int]`` and ``A[bool]`` are different types and cached separately.

.. literalinclude:: ./generics_examples/provide.py

Decorating objects with @decorate
***************************************

You can also make Generic decorator. Here it is used to decorate any type.

.. literalinclude:: ./generics_examples/decorate.py
33 changes: 33 additions & 0 deletions docs/advanced/generics_examples/decorate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from collections.abc import Iterator
from typing import TypeVar

from dishka import make_container, Provider, provide, Scope, decorate

T = TypeVar("T")


class MyProvider(Provider):
scope = Scope.APP

@provide
def make_int(self) -> int:
return 1

@provide
def make_str(self) -> str:
return "hello"

@decorate
def log(self, a: T, t: type[T]) -> Iterator[T]:
print("Requested", t, "with value", a)
yield a
print("Requested release", a)


container = make_container(MyProvider())
container.get(int) # Requested <class 'int'> with value 1
container.get(str) # Requested <class 'str'> with value hello
container.close()
# Requested release object hello
# Requested release object 1

Loading

0 comments on commit 7ef8701

Please sign in to comment.