Skip to content

Commit

Permalink
Merge pull request #283 from reagento/feature/redo_quickstart
Browse files Browse the repository at this point in the history
More real quickstart
  • Loading branch information
Tishka17 authored Oct 21, 2024
2 parents b8ab6d1 + c1611fd commit b728546
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 197 deletions.
222 changes: 64 additions & 158 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ 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 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.

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). We want to keep DI in place, not soiling you code with global variables and additional specifiers in all places.

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.
Expand All @@ -36,76 +38,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 +140,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 +174,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()
```
2 changes: 2 additions & 0 deletions docs/integrations/index.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.. include:: <isonum.txt>

.. _integrations:

Using with frameworks
*******************************

Expand Down
Loading

0 comments on commit b728546

Please sign in to comment.