Skip to content

Commit

Permalink
Fix resource bugs and add more tests for overriding and resources (#26)
Browse files Browse the repository at this point in the history
* Fix resource bugs and add more tests for overriding and resources
  • Loading branch information
nightblure authored Dec 2, 2024
1 parent 953320f commit 4fe3660
Show file tree
Hide file tree
Showing 25 changed files with 423 additions and 114 deletions.
14 changes: 1 addition & 13 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 'v4.5.0'
hooks:
- id: check-toml
- id: check-json
- id: end-of-file-fixer
- id: pretty-format-json
args:
- '--autofix'
- id: trailing-whitespace
exclude: '.bumpversion.cfg'

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
rev: v0.8.1
hooks:
- id: ruff
entry: ruff check src tests --fix --exit-non-zero-on-fix --show-fixes
Expand Down
2 changes: 1 addition & 1 deletion docs/dev/migration-from-dependency-injector.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ and eliminates its shortcomings, which will make migrating very easy.
⚠️ **IMPORTANT**

[Injection](https://github.com/nightblure/injection) **does not implement** **some** [providers](https://python-dependency-injector.ets-labs.org/providers/index.html)
(Resource, List, Dict, Aggregate and etc.) because the developer considered them to be **rarely used** in practice.
(List, Dict and etc.) because the developer considered them to be **rarely used** in practice.
In this case, you don't need to do the migration, but if you really want to use my package,
I'd love to see your [issues](https://github.com/nightblure/injection/issues) and/or [merge requests](https://github.com/nightblure/injection/pulls)!

Expand Down
6 changes: 6 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
providers/object
providers/provided_instance
.. toctree::
:maxdepth: 1
:caption: Dependency injection
injection/injection.md
.. toctree::
:maxdepth: 1
:caption: Integration with web frameworks
Expand Down
6 changes: 6 additions & 0 deletions docs/injection/injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Dependency injection

soon...

## Auto injection

31 changes: 18 additions & 13 deletions docs/providers/coroutine.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,28 @@ Can be resolved only with using the `async_resolve` method.
## Example

```python3
import asyncio
from typing import Tuple
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

class DIContainer(DeclarativeContainer):
provider = providers.Coroutine(coroutine, arg1=1, arg2=2)
async def coroutine(arg1: int, arg2: int) -> Tuple[int, int]:
return arg1, arg2

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

async def main() -> None:
arg1, arg2 = await DIContainer.provider.async_resolve(arg1=500, arg2=600)
assert (arg1, arg2) == (500, 600)
class DIContainer(DeclarativeContainer):
provider = providers.Coroutine(coroutine, arg1=1, arg2=2)

asyncio.run(main())

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


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


asyncio.run(main())
```
2 changes: 1 addition & 1 deletion docs/providers/factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Also supports **asynchronous** dependencies.
```python3
import asyncio
from dataclasses import dataclass
from typing import Tuple

from injection import DeclarativeContainer, providers

Expand All @@ -35,7 +36,6 @@ async def main() -> None:

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

assert instance1 is not instance2

asyncio.run(main())
Expand Down
62 changes: 61 additions & 1 deletion docs/providers/resource.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,63 @@
# Resource

soon...
**Resource provider** provides a component with **initialization** and **closing**.
**Resource providers** supports next **initializers**:
* **sync** and **async** **generators**;
* **inheritors** of `ContextManager` and `AsyncContextManager` classes;
* functions wrapped into `@contextmanager` and `@asynccontextmanager` **decorators**.

## Working scope
Resource provider can works with two scopes: **singleton** and **function-scope**.

**Function-scope** requires to set parameter of `Resource` provider `function_scope=True`.
**Function-scope** resources can works only with `@inject` decorator!

## Example
```python
from typing import Tuple, Iterator, AsyncIterator

from injection import DeclarativeContainer, Provide, inject, providers


def sync_func() -> Iterator[str]:
yield "sync_func"


async def async_func() -> AsyncIterator[str]:
yield "async_func"


class DIContainer(DeclarativeContainer):
sync_resource = providers.Resource(sync_func)
async_resource = providers.Resource(async_func)

sync_resource_func_scope = providers.Resource(sync_func, function_scope=True)
async_resource_func_scope = providers.Resource(async_func, function_scope=True)


@inject
async def func_with_injections(
sync_value: str = Provide[DIContainer.sync_resource],
async_value: str = Provide[DIContainer.async_resource],
sync_func_scope_value: str = Provide[DIContainer.sync_resource_func_scope],
async_func_scope_value: str = Provide[DIContainer.async_resource_func_scope]
) -> Tuple[str, str, str, str]:
return sync_value, async_value, sync_func_scope_value, async_func_scope_value


async def main() -> None:
values = await func_with_injections()

assert values == ("sync_func", "async_func", "sync_func", "async_func")

assert DIContainer.sync_resource.initialized
assert DIContainer.async_resource.initialized

# Resources with function scope were closed after dependency injection
assert not DIContainer.sync_resource_func_scope.initialized
assert not DIContainer.async_resource_func_scope.initialized


if __name__ == "__main__":
await main()
```
1 change: 0 additions & 1 deletion docs/providers/singleton.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ if __name__ == "__main__":

assert instance1 is instance2
assert instance1.field == 15

```

## Resetting memoized object
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/transient.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Also supports **asynchronous** dependencies.
```python3
import asyncio
from dataclasses import dataclass
from typing import Tuple

from injection import DeclarativeContainer, providers

Expand All @@ -36,7 +37,6 @@ async def main() -> None:

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

assert instance1 is not instance2

asyncio.run(main())
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ ignore = [
[tool.pytest.ini_options]
pythonpath = ["src"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = [
"ignore::DeprecationWarning:pkg_resources.*",
]
Expand Down Expand Up @@ -166,7 +167,7 @@ build-backend = "hatchling.build"
[tool.coverage.run]
omit = [
"src/injection/__version__.py",
"*/tests/*"
"*/tests/*",
]

[tool.coverage.report]
Expand All @@ -175,7 +176,8 @@ exclude_lines = [
"if TYPE_CHECKING:",
"sys.version_info",
"raise NotImplementedError",
"ImportError"
"ImportError",
"# pragma: no cover"
]

[tool.mypy]
Expand Down
77 changes: 64 additions & 13 deletions src/injection/base_container.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import inspect
from collections import defaultdict
from contextlib import contextmanager
Expand Down Expand Up @@ -138,30 +139,80 @@ def init_resources(cls) -> None:

@classmethod
async def init_resources_async(cls) -> None:
for provider in cls.get_resource_providers():
if provider.async_mode:
await provider.async_resolve()
await asyncio.gather(
*[
provider.async_resolve()
for provider in cls.get_resource_providers()
if provider.async_mode
],
)

@classmethod
async def init_all_resources(cls) -> None:
resource_providers = cls.get_resource_providers()

await asyncio.gather(
*[
provider.async_resolve()
for provider in resource_providers
if provider.async_mode
],
)

for provider in resource_providers:
if not provider.async_mode:
provider()

@classmethod
def close_resources(cls) -> None:
for provider in cls.get_resource_providers():
if not provider.async_mode:
if provider.initialized and 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()
async def close_async_resources(cls) -> None:
await asyncio.gather(
*[
provider.async_close()
for provider in cls.get_resource_providers()
if provider.initialized and provider.async_mode
],
)

@classmethod
async def close_function_scope_async_resources(cls) -> None:
await asyncio.gather(
*[
provider.async_close()
for provider in cls.get_resource_providers()
if provider.initialized
and provider.async_mode
and provider.function_scope
],
)

@classmethod
def close_function_scope_resources(cls) -> None:
for provider in cls.get_resource_providers():
if not provider.async_mode and provider.function_scope:
if (
provider.initialized
and provider.function_scope
and not provider.async_mode
):
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()
async def close_all_resources(cls) -> None:
resource_providers = cls.get_resource_providers()

await asyncio.gather(
*[
provider.async_close()
for provider in resource_providers
if provider.initialized and provider.async_mode
],
)

for provider in resource_providers:
if provider.initialized and not provider.async_mode:
provider.close()
52 changes: 28 additions & 24 deletions src/injection/inject/auto_inject.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,35 +72,39 @@ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:


def auto_inject(
f: Callable[P, T],
target_container: Optional[_ContainerType] = None,
) -> Callable[P, T]:
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Decorate callable with injecting decorator. Inject objects by types"""

if target_container is None:
container_subclasses = DeclarativeContainer.__subclasses__()
def wrapper(f: Callable[P, T]) -> Callable[P, T]:
nonlocal target_container

if len(container_subclasses) > 1:
msg = (
f"Found {len(container_subclasses)} containers, please specify "
f"the required container explicitly in the parameter 'target_container'"
)
raise Exception(msg)
if target_container is None:
container_subclasses = DeclarativeContainer.__subclasses__()

target_container = container_subclasses[0]
if len(container_subclasses) > 1:
msg = (
f"Found {len(container_subclasses)} containers, please specify "
f"the required container explicitly in the parameter 'target_container'"
)
raise Exception(msg)

signature = inspect.signature(f)
target_container = container_subclasses[0] # pragma: no cover

if inspect.iscoroutinefunction(f):
func_with_injected_params = _get_async_injected(
f=f,
signature=signature,
target_container=target_container,
)
return cast(Callable[P, T], func_with_injected_params)
signature = inspect.signature(f)

return _get_sync_injected(
f=f,
signature=signature,
target_container=target_container,
)
if inspect.iscoroutinefunction(f):
func_with_injected_params = _get_async_injected(
f=f,
signature=signature,
target_container=target_container,
)
return cast(Callable[P, T], func_with_injected_params)
else:
return _get_sync_injected(
f=f,
signature=signature,
target_container=target_container,
)

return wrapper
3 changes: 2 additions & 1 deletion src/injection/inject/inject.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
result = await f(*args, **kwargs)

for container in _get_all_di_containers():
await container.close_function_scope_resources_async()
await container.close_function_scope_async_resources()
container.close_function_scope_resources()

return result

Expand Down
Loading

0 comments on commit 4fe3660

Please sign in to comment.