Skip to content

Commit

Permalink
Implement resource provider, async injections, fix mypy errors, add f…
Browse files Browse the repository at this point in the history
…actory provider (#25)
  • Loading branch information
nightblure authored Dec 1, 2024
1 parent 6fe360d commit 26a1210
Show file tree
Hide file tree
Showing 45 changed files with 1,260 additions and 357 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
test:
hatch test --cover --all --randomize

#test-ci:
#pdm run pytest -rA tests --cov=src --cov-report term-missing --cov-report=xml --asyncio-mode=auto
test3.8:
hatch test -i python="3.8" --cover --randomize

test-py:
hatch test -i python="$(v)" --cover --randomize
Expand Down Expand Up @@ -50,4 +50,4 @@ release-minor:
make release

mypy:
pdm run mypy src
pdm run mypy src tests
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Easy dependency injection for all, works with Python 3.8-3.12. Main features and
* support dependency injection via `Annotated` in `FastAPI`;
* the code is fully typed and checked with [mypy](https://github.com/python/mypy);
* **no third-party dependencies**;
* support **async injections**;
* no **wiring**;
* the life cycle of objects (**scope**) is implemented by **providers**;
* **overriding** dependencies for testing;
Expand Down
3 changes: 0 additions & 3 deletions docs/dev/migration-from-dependency-injector.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,3 @@ To **migrate**, follow these **steps**:
* `from dependency_injector.containers import DeclarativeContainer` -> `from injection import DeclarativeContainer`;

2. **Replace argument unpacking in the `override_providers` method call with direct argument passing**: `some_container.override_providers(**overrides)` -> `some_container.override_providers(overrides)`;


3. **Replace the Factory provider with Transient**: `providers.Factory` -> `providers.Transient`'.
2 changes: 1 addition & 1 deletion docs/integration-with-web-frameworks/litestar.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Using with Litestar
# Litestar

In order to successfully inject dependencies into Litestar request handlers,
make sure that the following points are completed:
Expand Down
3 changes: 0 additions & 3 deletions docs/providers/callable.md

This file was deleted.

25 changes: 14 additions & 11 deletions docs/providers/coroutine.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
# Coroutine

**Coroutine** provider creates a coroutine.
Can be resolved only with using the `async_resolve` method.

## Example

```python3
import asyncio
import asyncio
from typing import Tuple

from injection import DeclarativeContainer, providers
from injection import DeclarativeContainer, providers

async def coroutine(arg1: int, arg2: int) -> Tuple[int, int]:
return arg1, arg2

async def coroutine(arg1, arg2):
await asyncio.sleep(0.1)
return arg1, arg2
class DIContainer(DeclarativeContainer):
provider = providers.Coroutine(coroutine, arg1=1, arg2=2)

arg1, arg2 = asyncio.run(DIContainer.provider.async_resolve())
assert (arg1, arg2) == (1, 2)

class DIContainer(DeclarativeContainer):
provider = providers.Coroutine(coroutine, arg1=1, arg2=2)

async def main() -> None:
arg1, arg2 = await DIContainer.provider.async_resolve(arg1=500, arg2=600)
assert (arg1, arg2) == (500, 600)

if __name__ == "__main__":
arg1, arg2 = asyncio.run(DIContainer.provider())
assert (arg1, arg2) == (1, 2)
asyncio.run(main())
```
42 changes: 42 additions & 0 deletions docs/providers/factory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Factory

**Factory** works exactly same as **Transient** provider.

Also supports **asynchronous** dependencies.

## Example

```python3
import asyncio
from dataclasses import dataclass

from injection import DeclarativeContainer, providers


@dataclass
class SomeClass:
field: Tuple[int, int]


async def coroutine_func(arg1: int, arg2: int) -> Tuple[int, int]:
return arg1, arg2


class DIContainer(DeclarativeContainer):
coroutine = providers.Coroutine(coroutine_func, arg1=1, arg2=2)
sync_factory = providers.Factory(SomeClass, field=(10, 20))
async_factory = providers.Factory(SomeClass, field=coroutine)


async def main() -> None:
instance = await DIContainer.async_factory.async_resolve()
assert instance.field == (1, 2)


instance1 = DIContainer.sync_factory()
instance2 = DIContainer.sync_factory()

assert instance1 is not instance2

asyncio.run(main())
```
3 changes: 3 additions & 0 deletions docs/providers/resource.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Resource

soon...
30 changes: 23 additions & 7 deletions docs/providers/transient.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
# Transient

**Transient** provider creates and returns a new object for each call.
**Transient** provider creates and returns a **new object for each call**.
You can pass any **callable** object as the first parameter.

Also supports **asynchronous** dependencies.

## Example

```python3
import asyncio
from dataclasses import dataclass

from injection import DeclarativeContainer, providers


@dataclass
class SomeClass:
field: str
field: Tuple[int, int]


async def coroutine_func(arg1: int, arg2: int) -> Tuple[int, int]:
return arg1, arg2


class DIContainer(DeclarativeContainer):
provider = providers.Transient(SomeClass, field="str_value")
coroutine = providers.Coroutine(coroutine_func, arg1=1, arg2=2)
sync_transient = providers.Transient(SomeClass, field=(10, 20))
async_transient = providers.Transient(SomeClass, field=coroutine)


async def main() -> None:
instance = await DIContainer.async_transient.async_resolve()
assert instance.field == (1, 2)


instance1 = DIContainer.sync_transient()
instance2 = DIContainer.sync_transient()

if __name__ == "__main__":
instance1 = DIContainer.provider()
instance2 = DIContainer.provider()
assert instance1 is not instance2

assert instance1 is not instance2
asyncio.run(main())
```
9 changes: 9 additions & 0 deletions src/injection/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@
from injection.inject.auto_inject import auto_inject
from injection.inject.inject import inject
from injection.provide import Provide

__all__ = [
"DeclarativeContainer",
"Provide",
"__version__",
"auto_inject",
"inject",
"providers",
]
64 changes: 50 additions & 14 deletions src/injection/base_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
DuplicatedFactoryTypeAutoInjectionError,
UnknownProviderTypeAutoInjectionError,
)
from injection.providers import Singleton
from injection.providers import Resource, Singleton
from injection.providers.base import BaseProvider
from injection.providers.base_factory import BaseFactoryProvider

Expand All @@ -25,7 +25,7 @@ def instance(cls) -> "DeclarativeContainer":
return cls.__instance

@classmethod
def __get_providers(cls) -> Dict[str, BaseProvider[Any]]:
def _get_providers(cls) -> Dict[str, BaseProvider[Any]]:
if cls.__providers is None:
cls.__providers = {
member_name: member
Expand All @@ -35,14 +35,16 @@ def __get_providers(cls) -> Dict[str, BaseProvider[Any]]:
return cls.__providers

@classmethod
def _get_providers_generator(cls) -> Iterator[BaseProvider[Any]]:
for _, member in inspect.getmembers(cls):
if isinstance(member, BaseProvider):
yield member
def get_providers(cls) -> List[BaseProvider[Any]]:
return list(cls._get_providers().values())

@classmethod
def get_providers(cls) -> List[BaseProvider[Any]]:
return list(cls.__get_providers().values())
def get_resource_providers(cls) -> List[Resource[Any]]:
return [
provider
for provider in cls.get_providers()
if isinstance(provider, Resource)
]

@classmethod
@contextmanager
Expand All @@ -66,7 +68,7 @@ def override_providers(
*,
reset_singletons: bool = False,
) -> Iterator[None]:
current_providers = cls.__get_providers()
current_providers = cls._get_providers()
current_provider_names = set(current_providers.keys())
given_provider_names = set(providers_for_overriding.keys())

Expand Down Expand Up @@ -95,24 +97,22 @@ def override_providers(

@classmethod
def reset_singletons(cls) -> None:
providers_gen = cls._get_providers_generator()
providers_gen = cls.get_providers()

for provider in providers_gen:
if isinstance(provider, Singleton):
provider.reset()

@classmethod
def reset_override(cls) -> None:
providers = cls.__get_providers()

for provider in providers.values():
for provider in cls.get_providers():
provider.reset_override()

@classmethod
def resolve_by_type(cls, type_: Type[Any]) -> Any:
provider_factory_to_providers = defaultdict(list)

for provider in cls._get_providers_generator():
for provider in cls.get_providers():
if not issubclass(type(provider), BaseFactoryProvider):
continue

Expand All @@ -129,3 +129,39 @@ def resolve_by_type(cls, type_: Type[Any]) -> Any:
return provider()

raise UnknownProviderTypeAutoInjectionError(str(type_))

@classmethod
def init_resources(cls) -> None:
for provider in cls.get_resource_providers():
if not provider.async_mode:
provider()

@classmethod
async def init_resources_async(cls) -> None:
for provider in cls.get_resource_providers():
if provider.async_mode:
await provider.async_resolve()

@classmethod
def close_resources(cls) -> None:
for provider in cls.get_resource_providers():
if not provider.async_mode:
provider.close()

@classmethod
async def close_resources_async(cls) -> None:
for provider in cls.get_resource_providers():
if provider.async_mode:
await provider.async_close()

@classmethod
def close_function_scope_resources(cls) -> None:
for provider in cls.get_resource_providers():
if not provider.async_mode and provider.function_scope:
provider.close()

@classmethod
async def close_function_scope_resources_async(cls) -> None:
for provider in cls.get_resource_providers():
if provider.async_mode and provider.function_scope:
await provider.async_close()
Loading

0 comments on commit 26a1210

Please sign in to comment.