Skip to content
Closed
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
27 changes: 17 additions & 10 deletions docs/advanced/when.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ To set conditional activation you create special ``Marker`` objects and use them

.. code-block:: python

from dishka import Provider, provide, Scope
from dishka import Marker, Provider, Scope, provide

class MyProvider(Provider)
class MyProvider(Provider):
@provide(scope=Scope.APP)
def base_impl(self) -> Cache:
return NormalCacheImpl()
Expand All @@ -49,9 +49,9 @@ It can be the same or another provider while you pass when creating a container.

.. code-block:: python

from dishka import activate, Provider
from dishka import Marker, Provider, activate

class MyProvider(Provider)
class MyProvider(Provider):
@activate(Marker("debug"))
def is_debug(self) -> bool:
return False
Expand All @@ -60,7 +60,7 @@ This function can use other objects as well. For example, we can pass config usi

.. code-block:: python

class MyProvider(Provider)
class MyProvider(Provider):
config = from_context(Config, scope=Scope.APP)

@activate(Marker("debug"))
Expand All @@ -78,7 +78,7 @@ More general pattern is to create own marker type and register a single activato
pass


class MyProvider(Provider)
class MyProvider(Provider):
config = from_context(Config, scope=Scope.APP)

@activate(EnvMarker)
Expand Down Expand Up @@ -154,14 +154,21 @@ In case you want to activate some features when specific objects are available y
* it is activated
* if it actually presents in context while being registered as ``from_context``

``Has(T)`` implicitly registers ``T`` for graph validation. Use
``from_context(T, ...)`` when ``Has(T)`` should become true only after a real
context value is passed.

The implicit registration only helps validation. ``Has(T)`` still stays false
until some real provider or real context value is available.


For example:

.. code-block:: python

from dishka import Provider, provide, Scope
from dishka import Has, Provider, Scope, from_context, make_container, provide

class MyProvider(Provider)
class MyProvider(Provider):
config = from_context(RedisConfig, scope=Scope.APP)

@provide(scope=Scope.APP)
Expand All @@ -182,6 +189,6 @@ For example:

In this case,

* ``memcached_impl`` is not used because no factory for ``MemcachedConfig`` is provided
* ``memcached_impl`` is not used because no real factory for ``MemcachedConfig`` is provided
* ``redis_impl`` is not used while it is registered as ``from_context`` but no real value is provided.
* ``base_impl`` is used as a default one, because none of later is active
* ``base_impl`` is used as a default one, because none of later is active
134 changes: 127 additions & 7 deletions src/dishka/graph_builder/validator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import itertools
from collections.abc import Sequence
from enum import Enum

from dishka.dependency_source import Factory
from dishka.entities.component import Component
from dishka.entities.key import DependencyKey
from dishka.entities.marker import (
AndMarker,
BaseMarker,
BoolMarker,
Has,
Marker,
NotMarker,
OrMarker,
)
from dishka.exceptions import (
CycleDependenciesError,
GraphMissingFactoryError,
Expand All @@ -12,12 +23,115 @@
from dishka.registry import Registry


class MarkerValue(Enum):
TRUE = True
FALSE = False
UNKNOWN = None


class GraphValidator:
def __init__(self, registries: Sequence[Registry]) -> None:
self.registries = registries
self.path: dict[DependencyKey, Factory] = {}
self.valid_keys: dict[DependencyKey, bool] = {}

def _get_factory(
self,
key: DependencyKey,
registry_index: int,
) -> Factory | None:
for index in range(registry_index + 1):
factory = self.registries[index].get_factory(key)
if factory is not None:
return factory
return None

def _has_reachable_factory(
self,
key: DependencyKey,
registry_index: int,
) -> bool:
return self._get_factory(key, registry_index) is not None

def _marker_component(self, component: Component | None) -> Component:
if component is None:
raise TypeError
return component

def _invert_marker_value(self, value: MarkerValue) -> MarkerValue:
if value is MarkerValue.TRUE:
return MarkerValue.FALSE
if value is MarkerValue.FALSE:
return MarkerValue.TRUE
return MarkerValue.UNKNOWN

def _and_marker_values(
self,
left: MarkerValue,
right: MarkerValue,
) -> MarkerValue:
if MarkerValue.FALSE in (left, right):
return MarkerValue.FALSE
if MarkerValue.TRUE == left == right:
return MarkerValue.TRUE
return MarkerValue.UNKNOWN

def _or_marker_values(
self,
left: MarkerValue,
right: MarkerValue,
) -> MarkerValue:
if MarkerValue.TRUE in (left, right):
return MarkerValue.TRUE
if MarkerValue.FALSE == left == right:
return MarkerValue.FALSE
return MarkerValue.UNKNOWN

def _eval_marker(
self,
marker: BaseMarker | None,
component: Component | None,
registry_index: int,
) -> MarkerValue:
result = MarkerValue.UNKNOWN
match marker:
case None | BoolMarker(True):
result = MarkerValue.TRUE
case BoolMarker(False):
result = MarkerValue.FALSE
case AndMarker():
result = self._and_marker_values(
self._eval_marker(marker.left, component, registry_index),
self._eval_marker(marker.right, component, registry_index),
)
case OrMarker():
result = self._or_marker_values(
self._eval_marker(marker.left, component, registry_index),
self._eval_marker(marker.right, component, registry_index),
)
case NotMarker():
result = self._invert_marker_value(
self._eval_marker(
marker.marker,
component,
registry_index,
),
)
case Has():
key = DependencyKey(
marker.value,
self._marker_component(component),
)
if self._has_reachable_factory(key, registry_index):
result = MarkerValue.UNKNOWN
else:
result = MarkerValue.FALSE
case Marker():
result = MarkerValue.UNKNOWN
case _:
result = MarkerValue.UNKNOWN
return result

def _validate_key(
self,
key: DependencyKey,
Expand Down Expand Up @@ -54,7 +168,7 @@
suggest_concrete_factories=suggest_concrete_factories,
)

def _validate_factory(

Check failure on line 171 in src/dishka/graph_builder/validator.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=reagento_dishka&issues=AZ0lCH4AMRarW6UvCD4P&open=AZ0lCH4AMRarW6UvCD4P&pullRequest=694
self, factory: Factory, registry_index: int,
) -> None:
self.path[factory.provides] = factory
Expand All @@ -65,13 +179,19 @@
raise CycleDependenciesError([factory])

try:
for dep in itertools.chain(
factory.dependencies,
factory.kw_dependencies.values(),
):
# ignore TypeVar and const parameters
if not dep.is_type_var() and not dep.is_const():
self._validate_key(dep, registry_index)
when_active = self._eval_marker(
factory.when_active,
factory.when_component,
registry_index,
)
if when_active is not MarkerValue.FALSE:
for dep in itertools.chain(
factory.dependencies,
factory.kw_dependencies.values(),
):
# ignore TypeVar and const parameters
if not dep.is_type_var() and not dep.is_const():
self._validate_key(dep, registry_index)
except NoFactoryError as e:
e.add_path(factory)
raise
Expand Down
Loading