Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callable provider does not infer return type via Provide #5

Merged
merged 6 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 12 additions & 3 deletions docs/introduction/concepts-and-key_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ In addition, this will provide an easy migration to the current framework with
[Dependency Injector](https://python-dependency-injector.ets-labs.org/index.html#)
(see [migration from Dependency Injector](https://injection.readthedocs.io/latest/dev/migration-from-dependency-injector.html)).

## Features
## Features and advantages

* support **Python 3.8-3.12**;
* works with **FastAPI, Flask, Litestar** and **Django REST Framework**;
* dependency injection via `Annotated` in FastAPI;
* **no third-party dependencies**;
* **multiple containers**;
* providers - delegate object creation and lifecycle management to providers;
* support Python 3.8-3.12;
* works with FastAPI, Flask, Django REST Framework, Litestar;
* **overriding** dependencies for tests without wiring;
* **100%** code coverage and very simple code;
* good [documentation](https://injection.readthedocs.io/latest/);
* intuitive and almost identical api with [dependency-injector](https://github.com/ets-labs/python-dependency-injector),
which will allow you to easily migrate to injection
(see [migration from dependency injector](https://injection.readthedocs.io/latest/dev/migration-from-dependency-injector.html));
49 changes: 25 additions & 24 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,36 @@ ignore = [

[tool.ruff.lint.extend-per-file-ignores]
"tests/test_integrations/**.py" = ["ANN001", "ANN201"]
"tests/**.py" = ["ALL"] # ["S101", "PLR2004", "PT011", "C409", "B017"]
"tests/**.py" = ["S101", "PLR2004", "PT011", "C409", "B017"]
"tests/test_integrations/test_drf/**.py" = ["E501", "S105", "EM101", "TRY003"]
"tests/container_objects.py" = ["PLR0913", "ARG001"]
"src/injection/__init__.py" = ["F401"]
"docs/conf.py" = ["A001"]

[tool.pytest.ini_options]
pythonpath = ["src"]
asyncio_mode = "auto"
filterwarnings = [
"ignore::DeprecationWarning:pkg_resources.*",
]

[tool.hatch.version]
path = "src/injection/__version__.py"

# FOR LOCAL TESTS (make test)
[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.8", "3.9", "3.10", "3.11", "3.12"]

[tool.hatch.envs.hatch-test]
randomize = true

[tool.hatch.build.targets.sdist]
only-include = ["src/injection"]

[tool.hatch.build.targets.wheel]
packages = ["src/injection"]


[tool.pdm]
distribution = false

Expand All @@ -107,29 +131,6 @@ test = [
"flask",
]

[tool.pytest.ini_options]
pythonpath = ["src"]
asyncio_mode = "auto"
filterwarnings = [
"ignore::DeprecationWarning:pkg_resources.*",
]

[tool.hatch.version]
path = "src/injection/__version__.py"

# FOR LOCAL TESTS (make test)
[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.8", "3.9", "3.10", "3.11", "3.12"]

[tool.hatch.envs.hatch-test]
randomize = true

[tool.hatch.build.targets.sdist]
only-include = ["src/injection"]

[tool.hatch.build.targets.wheel]
packages = ["src/injection"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Expand Down
12 changes: 7 additions & 5 deletions src/injection/provide.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from typing import Union
from typing import Generic, TypeVar, Union

from injection.providers.base import BaseProvider

T = TypeVar("T")

class ClassGetItemMeta(type):
def __getitem__(cls, item: Union[str, BaseProvider]) -> "Provide":

class ClassGetItemMeta(Generic[T], type):
def __getitem__(cls, item: Union[str, BaseProvider[T]]) -> T:
return cls(item)


class Provide(metaclass=ClassGetItemMeta):
def __init__(self, provider: Union[str, BaseProvider]) -> None:
def __init__(self, provider: Union[str, BaseProvider[T]]) -> None:
self.provider = provider

def __call__(self) -> "Provide":
def __call__(self) -> T:
return self
51 changes: 51 additions & 0 deletions src/injection/providers/base_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import inspect
from typing import Any, Callable, Dict, Tuple, Type, TypeVar, Union

from injection.providers.base import BaseProvider
from injection.resolving import get_clean_args, get_clean_kwargs

T = TypeVar("T")
FactoryType = Union[Type[T], Callable[..., T]]


class BaseFactoryProvider(BaseProvider[T]):
def __init__(self, factory: FactoryType, *args, **kwargs) -> None:
super().__init__()
self._factory = factory
self._args = args
self._kwargs = kwargs

def _get_final_args_and_kwargs(
self,
*args,
**kwargs,
) -> Tuple[Tuple[Any, ...], Dict[str, Any]]:
clean_args = get_clean_args(self._args)
clean_kwargs = get_clean_kwargs(self._kwargs)

# Common solution for bug when litestar try to add kwargs with name 'args' and 'kwargs'
if len(args) > 0 or len(kwargs) > 0:
type_cls_init_signature = inspect.signature(self._factory)
parameters = type_cls_init_signature.parameters

args = [arg for arg in args if arg in parameters]
kwargs = {arg: value for arg, value in kwargs.items() if arg in parameters}

final_args = []
final_args.extend(clean_args)
final_args.extend(args)

final_kwargs: Dict[str, Any] = {}
final_kwargs.update(clean_kwargs)
final_kwargs.update(kwargs)
return tuple(final_args), final_kwargs

def _resolve(self, *args, **kwargs) -> T:
"""https://python-dependency-injector.ets-labs.org/providers/factory.html

Positional arguments are appended after Factory positional dependencies.
Keyword arguments have the priority over the Factory keyword dependencies with the same name.
"""
final_args, final_kwargs = self._get_final_args_and_kwargs(*args, **kwargs)
instance = self._factory(*final_args, **final_kwargs)
return instance
31 changes: 6 additions & 25 deletions src/injection/providers/callable.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,13 @@
from typing import Any, Callable, TypeVar
from typing import Callable as CallableType
from typing import TypeVar

from injection.providers.base import BaseProvider
from injection.resolving import get_clean_args, get_clean_kwargs
from injection.providers.base_factory import BaseFactoryProvider

T = TypeVar("T")


class Callable(BaseProvider[T]):
class Callable(BaseFactoryProvider[T]):
"""Callable provider."""

def __init__(self, callable_object: Callable, *a, **kw):
super().__init__()
self.callable_object = callable_object
self.args = a
self.kwargs = kw
self._instance = None

def _resolve(self, *args, **kwargs) -> Any:
clean_args = get_clean_args(self.args)
clean_kwargs = get_clean_kwargs(self.kwargs)

final_args = []
final_args.extend(clean_args)
final_args.extend(args)

final_kwargs = {}
final_kwargs.update(clean_kwargs)
final_kwargs.update(kwargs)

result = self.callable_object(*final_args, **final_kwargs)
return result
def __init__(self, callable_object: CallableType[..., T], *a, **kw):
super().__init__(callable_object, *a, **kw)
26 changes: 3 additions & 23 deletions src/injection/providers/coroutine.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
from typing import Callable, TypeVar

from injection.providers.base import BaseProvider
from injection.resolving import get_clean_args, get_clean_kwargs
from injection.providers.base_factory import BaseFactoryProvider

T = TypeVar("T")


class Coroutine(BaseProvider[T]):
class Coroutine(BaseFactoryProvider[T]):
def __init__(self, coroutine: Callable, *a, **kw):
super().__init__()
self.coroutine = coroutine
self.args = a
self.kwargs = kw
self._instance = None

async def _resolve(self, *args, **kwargs):
clean_args = get_clean_args(self.args)
clean_kwargs = get_clean_kwargs(self.kwargs)

final_args = []
final_args.extend(clean_args)
final_args.extend(args)

final_kwargs = {}
final_kwargs.update(clean_kwargs)
final_kwargs.update(kwargs)

result = await self.coroutine(*final_args, **final_kwargs)
return result
super().__init__(coroutine, *a, **kw)
3 changes: 1 addition & 2 deletions src/injection/providers/object.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, List, TypeVar
from typing import TypeVar

from injection.providers.base import BaseProvider
from injection.resolving import resolve_value
Expand All @@ -10,7 +10,6 @@ class Object(BaseProvider[T]):
def __init__(self, obj: T) -> None:
super().__init__()
self._obj = obj
self._mocks: List[Any] = []

def _resolve(self) -> T:
value = resolve_value(self._obj)
Expand Down
30 changes: 8 additions & 22 deletions src/injection/providers/partial_callable.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,17 @@
from typing import Callable as CallableType
from typing import TypeVar

from injection.providers.base import BaseProvider
from injection.resolving import get_clean_args, get_clean_kwargs
from injection.providers.base_factory import BaseFactoryProvider

T = TypeVar("T")
F = CallableType[..., T]


class PartialCallable(BaseProvider[T]):
def __init__(self, callable_object: CallableType, *a, **kw):
super().__init__()
self.callable_object = callable_object
self.args = a
self.kwargs = kw
self._instance = None
class PartialCallable(BaseFactoryProvider[F]):
def __init__(self, callable_object: F, *a, **kw):
super().__init__(callable_object, *a, **kw)

def _resolve(self, *args, **kwargs) -> partial:
clean_args = get_clean_args(self.args)
clean_kwargs = get_clean_kwargs(self.kwargs)

final_args = []
final_args.extend(clean_args)
final_args.extend(args)

final_kwargs = {}
final_kwargs.update(clean_kwargs)
final_kwargs.update(kwargs)

partial_callable = partial(self.callable_object, *final_args, **final_kwargs)
def _resolve(self, *args, **kwargs) -> F:
final_args, final_kwargs = self._get_final_args_and_kwargs(*args, **kwargs)
partial_callable = partial(self._factory, *final_args, **final_kwargs)
return partial_callable
6 changes: 4 additions & 2 deletions src/injection/providers/singleton.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from typing import Type, TypeVar

from injection.providers.transient import Transient
from injection.providers.base_factory import BaseFactoryProvider

T = TypeVar("T")


class Singleton(Transient[T]):
class Singleton(BaseFactoryProvider[T]):
"""Global singleton object created only once"""

def __init__(self, type_cls: Type[T], *a, **kw):
super().__init__(type_cls, *a, **kw)
self._instance = None
self.type_cls = type_cls

def _resolve(self, *args, **kwargs) -> T:
"""https://python-dependency-injector.ets-labs.org/providers/factory.html
Expand Down
39 changes: 3 additions & 36 deletions src/injection/providers/transient.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,13 @@
import inspect
from typing import Type, TypeVar

from injection.providers.base import BaseProvider
from injection.resolving import get_clean_args, get_clean_kwargs
from injection.providers.base_factory import BaseFactoryProvider

T = TypeVar("T")


class Transient(BaseProvider[T]):
class Transient(BaseFactoryProvider[T]):
"""Object that needs to be created every time"""

def __init__(self, type_cls: Type[T], *a, **kw):
super().__init__()
super().__init__(type_cls, *a, **kw)
self.type_cls = type_cls
self.args = a
self.kwargs = kw
self._instance = None

def _resolve(self, *args, **kwargs) -> T:
"""https://python-dependency-injector.ets-labs.org/providers/factory.html

Positional arguments are appended after Factory positional dependencies.
Keyword arguments have the priority over the Factory keyword dependencies with the same name.
"""
clean_args = get_clean_args(self.args)
clean_kwargs = get_clean_kwargs(self.kwargs)

# Common solution for bug when litestar try to add kwargs with name 'args' and 'kwargs'
if len(args) > 0 or len(kwargs) > 0:
type_cls_init_signature = inspect.signature(self.type_cls)
parameters = type_cls_init_signature.parameters

args = [arg for arg in args if arg in parameters]
kwargs = {arg: value for arg, value in kwargs.items() if arg in parameters}

final_args = []
final_args.extend(clean_args)
final_args.extend(args)

final_kwargs = {}
final_kwargs.update(clean_kwargs)
final_kwargs.update(kwargs)

instance = self.type_cls(*final_args, **final_kwargs)
return instance
Loading