Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4688e06
Update README
Tishka17 Jan 23, 2024
cb74e59
rename mony field so they have clearer meaning
Tishka17 Jan 23, 2024
a13d5d9
Merge pull request #8 from Tishka17/fix/naming
Tishka17 Jan 23, 2024
81abd73
dynamic deps and more tests
Tishka17 Jan 24, 2024
590a8c4
Merge pull request #11 from reagento/feature/dynamic_deps
Tishka17 Jan 24, 2024
58a5fea
unify containers
Tishka17 Jan 24, 2024
5994e0b
Merge pull request #12 from reagento/refactor/containers
Tishka17 Jan 24, 2024
3415bf1
Some initiual work for decorators
Tishka17 Jan 25, 2024
23e7d1d
working decorator
Tishka17 Jan 26, 2024
65433f7
Merge branch 'develop' into feature/decorator
Tishka17 Jan 26, 2024
93fbf77
clean code
Tishka17 Jan 26, 2024
92d136c
Test alias decorator
Tishka17 Jan 26, 2024
462df99
fix formatting and naming
Tishka17 Jan 26, 2024
3ec8941
update readme
Tishka17 Jan 26, 2024
f38f16b
fix links in pyproject
Tishka17 Jan 26, 2024
d9c3822
Merge pull request #13 from reagento/feature/decorator
Tishka17 Jan 26, 2024
6616540
Update readme
Tishka17 Jan 26, 2024
932f927
More details on providers and scopes
Tishka17 Jan 27, 2024
06410f6
technical reqs
Tishka17 Jan 27, 2024
befdb8a
Update README.md
Tishka17 Jan 27, 2024
b581f1d
Merge pull request #19 from reagento/Tishka17-patch-1
Tishka17 Jan 27, 2024
045e271
links, explicit context arg, fix typos
Tishka17 Jan 27, 2024
55958f7
freeze reqs
Tishka17 Jan 27, 2024
7654b2c
Merge pull request #18 from reagento/feature/more_readme
Tishka17 Jan 27, 2024
92e7ff0
some docstrings
Tishka17 Jan 28, 2024
482a94f
rename depedency provider variants
Tishka17 Jan 28, 2024
c5684ce
Merge pull request #21 from reagento/refactor/naming
Tishka17 Jan 28, 2024
574d1be
test double decorator, fix as_factory naming
Tishka17 Jan 28, 2024
48085ac
test double decorator, fix as_factory naming
Tishka17 Jan 28, 2024
5d51b77
Merge branch 'develop' of github.com:Tishka17/dishka into develop
Tishka17 Jan 28, 2024
e1799aa
more typing on decorators
Tishka17 Jan 28, 2024
07c902d
use inspect to collecte dependency sources
Tishka17 Jan 29, 2024
8e571da
fix get_methods naming
Tishka17 Jan 29, 2024
3a89466
Merge pull request #22 from reagento/refactor/dependency_sources_list
Tishka17 Jan 29, 2024
abf1073
lock factory
Tishka17 Jan 29, 2024
65899b9
exceptions
Tishka17 Jan 29, 2024
ee8f185
fix dependency
Tishka17 Jan 29, 2024
6e45bba
make exits private
Tishka17 Jan 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 155 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,160 @@
## DIshka - DI by Tishka17
## DIshka (from russian "small DI")

Minimal DI framework with scopes
Small DI framework with scopes and agreeable API.

### Purpose

This library is targeting to provide only an IoC-container. If you are tired manually passing objects to create others objects which are only used to create more object - we have a solution. Otherwise, you do not probably need a 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.

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.
* **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.

See more in [technical requirements](docs/technical_requirements.md)

### Quickstart

1. Create Scopes enum
2. Create Provider subclass.
3. Mark methods which actually create depedencies with `@provide` decorator
4. Do not forget typehints
5. Create Container instance passing providers
6. Call `get` to get dependency and use context manager to get deeper through scopes
7. Add decorators and middleware for your framework
1. Create Provider subclass.
```python
from dishka import Provider
class MyProvider(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.
```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)
```
4. Create Container instance passing providers, and step into `APP` scope. Or deeper if you need.
```python
with make_container(MyProvider()) as container: # enter Scope.APP
with container() as request_container: # enter Scope.REQUEST
...
```

5. Call `get` to get dependency and use context manager to get deeper through scopes
```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
```

6. Add decorators and middleware for your framework (_would be described soon_)

See [examples](examples)

### Concepts

**Dependency** is what you need for some part of your code to work. They are just object which you do not create in place and probably want to replace some day. At least for tests.
Some of them can live while you application is running, others are destroyed and created on each request. Dependencies can depend on other objects, which are their dependencies.

**Scope** is a lifespan of a dependency. Standard scopes are:

`APP` -> `REQUEST` -> `ACTION` -> `STEP`.

You decide when to enter and exit them, but it is done one by one. You set a scope for your dependency when you configure how to create it. If the same dependency is requested multiple time within one scope without leaving it, then the same instance is returned.

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

You can provide your own Scopes class if you are not satisfied with standard flow.

**Container** is what you use to get your dependency. You just call `.get(SomeType)` and it finds a way to get you an instance of that type. It does not create things itself, but manages their lifecycle and caches. It delegates objects creation to providers which are passed during creation.


**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`.

`@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.

If `provide` is used with some class then that class itself is treated as a factory (`__init__` is analyzed for parameters). But do not forget to assing that call to some attribute otherwise it will be ignored.



### 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 `dependency` 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 with defined provider? Use alias:
```python
class MyProvider(Provider):
p = alias(source=A, provides=AProtocol)
```
it works the same way as
```python
class MyProvider(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()

async with make_async_container(MyProvider()) as container:
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
```

See [examples](examples/sync_simple.py)
* 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:
```
55 changes: 55 additions & 0 deletions docs/technical_requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## Technical requirements for IoC-container

#### 1. Scopes

1. Library should support various number of scopes
2. All dependencies are attached to scopes before any of them can be created
3. There should be default set of scopes
4. Scopes are ordered. Order is defined when declaring scopes.
5. Scope can be entered and exited.
6. Scope can be entered not earlier than enter into previous one.
7. Same scope can be entered multiple times concurrently.
8. If the same dependency is requested more than one time within the scope the same instance is returned. Cache is not shared between concurrent instances of same scope
9. Dependency can require other dependencies of the same or previous scope.

#### 2. Concurrency

1. Containers should be allowed to use with multithreading or asyncio. Not required to support both within same object.
2. Dependency creation using async functions should be supported if container is configured to run in asyncio
3. Concurrent entrance of scopes must not break requirement of single instance of dependency. Type of concurrency model can be configured when creating container
4. User of container may be allowed to switch synchronization on or off for performance tuning

#### 3. Clean dependencies

1. Usage of container must not require modification of objects we are creating
2. Container must not require to be global variable.
4. Container can require code changes on the borders of scopes (e.g. application start, middlewares, request handlers)

#### 4. Lifecycle

1. Dependencies which require some cleanup must be cleaned up on the scope exit
2. Dependencies which do not require cleanup should somehow be supported

#### 5. Context data

1. It should be allowed to pass some data when entering the scope
2. Context data must be accessible when creating dependencies

#### 6. Modularity

1. There can be multiple containers within same code base for different purposes
2. There must be a way to assemble a container from some reusable parts.
3. Assembling of container should be done in runtime in local scope

#### 7. Usability

1. There should be a way to create dependency based on its `__init__`
2. When creating a dependency there should be a way to decide which subtype is used and request only its dependencies
3. There should be a way to reuse same object for multiple requested types
4. There should be a way to decorate dependency just adding new providers

#### 8. Integration

1. Additional helpers should be provided for some popular frameworks. E.g: flask, fastapi, aiogram, celery, apscheduler
2. These helpers should be optional
3. Additional integrations should be done without changing library code
2 changes: 1 addition & 1 deletion examples/aiogram_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ async def start(
async def main():
# real main
logging.basicConfig(level=logging.INFO)
async with make_async_container(MyProvider(), with_lock=True) as container:
async with make_async_container(MyProvider()) as container:
bot = Bot(token=API_TOKEN)
dp = Dispatcher()
for observer in dp.observers.values():
Expand Down
4 changes: 1 addition & 3 deletions examples/async_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ async def get_str(self, dep: int) -> AsyncGenerator[str, None]:


async def main():
async with make_async_container(
MyProvider(1), with_lock=True,
) as container:
async with make_async_container(MyProvider(1)) as container:
print(await container.get(int))

async with container() as c_request:
Expand Down
File renamed without changes.
Loading