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

Graph validation #85

Merged
merged 15 commits into from
Mar 3, 2024
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,22 @@ 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
* 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
container = make_async_container(MyProvider(), context={App: app})
from dishka import from_context, Provider, provide, Scope

class MyProvider(Provider):
scope = Scope.REQUEST

app = from_context(provides=App, scope=Scope.APP)
request = from_context(provides=RequestClass)

@provide
async 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
```
Expand Down
3 changes: 2 additions & 1 deletion docs/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ There are 3 special functions:

* ``@provide`` is used to declare a factory providing a dependency. It can be used with some class or as a method decorator. :ref:`Read more<provide>`
* ``alias`` is used to allow retrieving of the same object by different type hints. :ref:`Read more<alias>`
* ``decorate`` is used to modify or wrap an object which is already configured in another ``Provider``. :ref:`Read more<decorate>`
* ``from_context`` is used to mark a dependency as context data, which will be set manually when entering a scope. :ref:`Read more<from-context>`
* ``@decorate`` is used to modify or wrap an object which is already configured in another ``Provider``. :ref:`Read more<decorate>`

Component
====================
Expand Down
4 changes: 3 additions & 1 deletion docs/container/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@ To prevent such a condition you need to protect any session whose children can b
Context data
====================

Often, your scopes are assigned with some external events: HTTP-requests, message from queue, callbacks from framework. You can use those objects when creating dependencies. The difference from normal factories is that they are not created inside some ``Provider``, but passed to the scope:
Often, your scopes are assigned with some external events: HTTP-requests, message from queue, callbacks from framework. You can use those objects when creating dependencies. The difference from normal factories is that they are not created inside some ``Provider``, but passed to the scope. You need explicitly tell dishka which dependencies are expected to be received from context using ``from_context``

.. code-block:: python

from framework import Request

class MyProvider:
request = from_context(provides=Request, scope=Scope.REQUEST)

@provide(scope=Scope.REQUEST)
def a(self, request: Request) -> A:
return A(data=request.contents)
Expand Down
25 changes: 25 additions & 0 deletions docs/provider/from_context.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.. _from-context:

@from_context
****************

You can put some data manually when entering scope and rely on it in your provider factories. To make it work you need to mark a dependency as retrieved from context using ``from_context`` and the use it as usual. Later, set ``context=`` argument when you enter corresponding scope.


.. code-block:: python

from dishka import from_context, Provider, provide, Scope

class MyProvider(Provider):
scope = Scope.REQUEST

app = from_context(provides=App, scope=Scope.APP)
request = from_context(provides=RequestClass)

@provide
async 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
1 change: 1 addition & 0 deletions docs/provider/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,5 @@ Though it is a normal object, not all attributes are analyzed by ``Container``,

provide
alias
from_context
decorate
38 changes: 38 additions & 0 deletions examples/context_val.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Annotated

from dishka import Provider, provide, make_container, Scope, FromComponent, \
Container


class A:
def __init__(self, a: int) -> None:
pass


class Provider1(Provider):
scope = Scope.APP

@provide
def a1(self, a: float) -> int: ...

@provide
def a2(self, a: bool) -> float: ...

@provide
def a3(self,
a: Annotated[complex, FromComponent("provider2")]) -> bool: ...

@provide
def a4(self, a: A) -> complex: ...


class Provider2(Provider):
scope = Scope.APP
component = "provider2"

@provide
def a1(self, a: Container) -> complex: ...


c = make_container(Provider1(), Provider2())
c.get(type)
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ruff==0.2.*
ruff==0.3.*
tox==4.12.*
3 changes: 2 additions & 1 deletion src/dishka/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"DEFAULT_COMPONENT", "Component",
"make_container", "Container", "FromComponent",
"Provider",
"alias", "decorate", "provide", "DependencyKey",
"alias", "decorate", "from_context", "provide", "DependencyKey",
"BaseScope", "Scope",
]

Expand All @@ -12,6 +12,7 @@
from .dependency_source import (
alias,
decorate,
from_context,
provide,
)
from .entities.component import DEFAULT_COMPONENT, Component
Expand Down
20 changes: 17 additions & 3 deletions src/dishka/async_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
from .dependency_source import Factory, FactoryType
from .exceptions import (
ExitError,
NoContextValueError,
NoFactoryError,
UnsupportedFactoryError,
)
from .provider import Provider
from .registry import Registry, make_registries
from .registry import Registry, RegistryBuilder

T = TypeVar("T")

Expand Down Expand Up @@ -92,7 +93,7 @@ async def _get_from_self(
for dependency in factory.dependencies
]
except NoFactoryError as e:
e.add_path(key)
e.add_path(factory)
raise

if factory.type is FactoryType.GENERATOR:
Expand All @@ -109,6 +110,13 @@ async def _get_from_self(
solved = await factory.source(*sub_dependencies)
elif factory.type is FactoryType.VALUE:
solved = factory.source
elif factory.type is FactoryType.ALIAS:
solved = sub_dependencies[0]
elif factory.type is FactoryType.CONTEXT:
raise NoContextValueError(
f"Value for type {factory.provides.type_hint} is not found "
f"in container context with scope={factory.scope}",
)
else:
raise UnsupportedFactoryError(
f"Unsupported factory type {factory.type}.",
Expand Down Expand Up @@ -175,8 +183,14 @@ def make_async_container(
scopes: type[BaseScope] = Scope,
context: dict | None = None,
lock_factory: Callable[[], Lock] | None = Lock,
skip_validation: bool = False,
) -> AsyncContainer:
registries = make_registries(*providers, scopes=scopes)
registries = RegistryBuilder(
scopes=scopes,
container_type=AsyncContainer,
providers=providers,
skip_validation=skip_validation,
).build()
return AsyncContainer(
*registries,
context=context,
Expand Down
20 changes: 17 additions & 3 deletions src/dishka/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
from .dependency_source import Factory, FactoryType
from .exceptions import (
ExitError,
NoContextValueError,
NoFactoryError,
UnsupportedFactoryError,
)
from .provider import Provider
from .registry import Registry, make_registries
from .registry import Registry, RegistryBuilder

T = TypeVar("T")

Expand Down Expand Up @@ -91,7 +92,7 @@ def _get_from_self(
for dependency in factory.dependencies
]
except NoFactoryError as e:
e.add_path(key)
e.add_path(factory)
raise

if factory.type is FactoryType.GENERATOR:
Expand All @@ -112,6 +113,13 @@ def _get_from_self(
)
elif factory.type is FactoryType.VALUE:
solved = factory.source
elif factory.type is FactoryType.ALIAS:
solved = sub_dependencies[0]
elif factory.type is FactoryType.CONTEXT:
raise NoContextValueError(
f"Value for type {factory.provides.type_hint} is not found "
f"in container context with scope={factory.scope}",
)
else:
raise UnsupportedFactoryError(
f"Unsupported factory type {factory.type}. ",
Expand Down Expand Up @@ -176,6 +184,12 @@ def make_container(
scopes: type[BaseScope] = Scope,
context: dict | None = None,
lock_factory: Callable[[], Lock] | None = None,
skip_validation: bool = False,
) -> Container:
registries = make_registries(*providers, scopes=scopes)
registries = RegistryBuilder(
scopes=scopes,
container_type=Container,
providers=providers,
skip_validation=skip_validation,
).build()
return Container(*registries, context=context, lock_factory=lock_factory)
4 changes: 3 additions & 1 deletion src/dishka/dependency_source/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
__all__ = [
"DependencySource",
"alias", "Alias",
"context_var", "from_context",
"decorate", "Decorator",
"provide", "Factory", "FactoryType",
]

from .alias import Alias, alias
from .context_var import ContextVariable, from_context
from .decorator import Decorator, decorate
from .factory import Factory, FactoryType, provide

DependencySource = Alias | Factory | Decorator
DependencySource = Alias | Factory | Decorator | ContextVariable
2 changes: 1 addition & 1 deletion src/dishka/dependency_source/alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def as_factory(
provides=self.provides.with_component(component),
is_to_bind=False,
dependencies=[self.source.with_component(component)],
type_=FactoryType.FACTORY,
type_=FactoryType.ALIAS,
cache=self.cache,
)

Expand Down
64 changes: 64 additions & 0 deletions src/dishka/dependency_source/context_var.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from typing import (
Any,
)

from dishka.entities.component import DEFAULT_COMPONENT, Component
from dishka.entities.key import DependencyKey
from dishka.entities.scope import BaseScope
from .alias import alias
from .factory import Factory, FactoryType


def _context_stub() -> Any:
raise NotImplementedError


class ContextVariable:
__slots__ = ("provides", "scope")

def __init__(
self, *,
provides: DependencyKey,
scope: BaseScope,
) -> None:
self.provides = provides
self.scope = scope

def as_factory(
self, component: Component,
) -> Factory:
if component == DEFAULT_COMPONENT:
return Factory(
scope=self.scope,
source=_context_stub,
provides=self.provides,
is_to_bind=False,
dependencies=[],
type_=FactoryType.CONTEXT,
cache=False,
)
else:
aliased = alias(
source=self.provides.type_hint,
component=DEFAULT_COMPONENT,
)
return aliased.as_factory(scope=self.scope, component=component)

def __get__(self, instance, owner):
scope = self.scope or instance.scope
return ContextVariable(
scope=scope,
provides=self.provides,
)


def from_context(
*, provides: Any, scope: BaseScope | None = None,
) -> ContextVariable:
return ContextVariable(
provides=DependencyKey(
type_hint=provides,
component=DEFAULT_COMPONENT,
),
scope=scope,
)
8 changes: 7 additions & 1 deletion src/dishka/dependency_source/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class FactoryType(Enum):
FACTORY = "factory"
ASYNC_FACTORY = "async_factory"
VALUE = "value"
ALIAS = "alias"
CONTEXT = "context"


def _is_bound_method(obj):
Expand Down Expand Up @@ -138,7 +140,7 @@ def _guess_factory_type(source):

def _async_generator_result(possible_dependency: Any):
origin = get_origin(possible_dependency)
if origin in (AsyncIterable, AsyncIterator, AsyncGenerator):
if origin is AsyncIterable:
return get_args(possible_dependency)[0]
elif origin is AsyncIterator:
return get_args(possible_dependency)[0]
Expand Down Expand Up @@ -387,3 +389,7 @@ def scoped(func):
)

return scoped


def _context_stub():
raise NotImplementedError
3 changes: 3 additions & 0 deletions src/dishka/entities/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ def with_component(self, component: Component) -> "DependencyKey":
component=component,
)

def __str__(self):
return f"({self.type_hint}, component={self.component!r})"


def hint_to_dependency_key(hint: Any) -> DependencyKey:
if get_origin(hint) is not Annotated:
Expand Down
7 changes: 7 additions & 0 deletions src/dishka/entities/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ class Scope(BaseScope):
REQUEST = "REQUEST"
ACTION = "ACTION"
STEP = "STEP"


class InvalidScopes(BaseScope):
UNKNOWN_SCOPE = "<unknown scope>"

def __str__(self):
return self.value
Loading
Loading