Skip to content
Open
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
11 changes: 9 additions & 2 deletions fundi/debug.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import typing
import collections.abc

from fundi.scope import Scope
from fundi.inject import injection_impl
from fundi.types import CacheKey, CallableInfo


def tree(
scope: collections.abc.Mapping[str, typing.Any],
scope: collections.abc.Mapping[str, typing.Any] | Scope,
info: CallableInfo[typing.Any],
cache: (
collections.abc.MutableMapping[CacheKey, collections.abc.Mapping[str, typing.Any]] | None
Expand All @@ -20,6 +21,9 @@ def tree(
:param cache: tree generation cache
:return: Tree of dependencies
"""
if not isinstance(scope, Scope):
scope = Scope.from_legacy(scope)

if cache is None:
cache = {}

Expand All @@ -36,7 +40,7 @@ def tree(


def order(
scope: collections.abc.Mapping[str, typing.Any],
scope: collections.abc.Mapping[str, typing.Any] | Scope,
info: CallableInfo[typing.Any],
cache: (
collections.abc.MutableMapping[CacheKey, list[typing.Callable[..., typing.Any]]] | None
Expand All @@ -50,6 +54,9 @@ def order(
:param cache: solvation cache
:return: order of dependencies
"""
if not isinstance(scope, Scope):
scope = Scope.from_legacy(scope)

if cache is None:
cache = {}

Expand Down
11 changes: 11 additions & 0 deletions fundi/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,14 @@ def __init__(
super().__init__(f"Generator exited too early")
self.function: FunctionType = function
self.generator: AsyncGenerator[typing.Any] | Generator[typing.Any, None, None] = generator


class InvalidInitialValue(ValueError):
"""
Initial value passed to the ``Scope`` constructor is invalid
"""

def __init__(self, value: typing.Any):
super().__init__(
f"Initial value is invalid: got {value!r}, but ``TypeFactory`` or ``TypeInstance`` expected"
)
49 changes: 33 additions & 16 deletions fundi/inject.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@
import contextlib
import collections.abc

from fundi.scope import Scope
from fundi.resolve import resolve
from fundi.logging import get_logger
from fundi.types import CacheKey, CallableInfo
from fundi.util import call_sync, call_async, add_injection_trace
from fundi.util import call_sync, call_async, add_injection_trace, callable_str

injection_logger = get_logger("inject.injection")
collection_logger = get_logger("inject.collection")


def injection_impl(
scope: collections.abc.Mapping[str, typing.Any],
scope: Scope,
info: CallableInfo[typing.Any],
cache: collections.abc.MutableMapping[CacheKey, typing.Any],
override: collections.abc.Mapping[typing.Callable[..., typing.Any], typing.Any] | None,
) -> collections.abc.Generator[
tuple[collections.abc.Mapping[str, typing.Any], CallableInfo[typing.Any], bool],
tuple[collections.abc.Mapping[str, typing.Any] | Scope, CallableInfo[typing.Any], bool],
typing.Any,
None,
]:
Expand All @@ -41,7 +42,7 @@ def injection_impl(

if info.scopehook:
collection_logger.debug("Calling scope hook for %r", info.call)
scope = dict(scope)
scope = scope.copy()
info.scopehook(scope, info)

values: dict[str, typing.Any] = {}
Expand All @@ -57,7 +58,9 @@ def injection_impl(
), "Dependency expected, got None. This is a bug, please report at https://github.com/KuyuCode/fundi"

collection_logger.debug("Passing %r upstream to be injected", dependency.call)
value = yield {**scope, "__fundi_parameter__": result.parameter}, dependency, True

subscope = scope | Scope.from_legacy({"__fundi_parameter__": result.parameter})
value = yield subscope, dependency, True

if dependency.use_cache:
collection_logger.debug(
Expand All @@ -71,15 +74,19 @@ def injection_impl(
collection_logger.debug("Passing %r side effects upstream to be injected", info.call)
_values = values.copy()
_info = info.copy(True)
_scope = {**scope}
for side_effect in info.side_effects:
yield {
**scope,
_scope = scope.copy()

subscope = scope | Scope(
{
"__values__": _values,
"__dependant__": _info,
"__scope__": _scope,
"__fundi_parameter__": None,
}, side_effect, True
}
)

for side_effect in info.side_effects:
yield subscope, side_effect, True

collection_logger.debug(
"Passing %r with collected values %r to be called", info.call, values
Expand All @@ -93,7 +100,7 @@ def injection_impl(


def inject(
scope: collections.abc.Mapping[str, typing.Any],
scope: collections.abc.Mapping[str, typing.Any] | Scope,
info: CallableInfo[typing.Any],
stack: contextlib.ExitStack | None = None,
cache: collections.abc.MutableMapping[CacheKey, typing.Any] | None = None,
Expand All @@ -112,7 +119,14 @@ def inject(
:return: result of callable
"""
if info.async_:
raise RuntimeError("Cannot process async functions in synchronous injection")
raise RuntimeError(
"Cannot process async functions ({func}) in synchronous injection".format(
func=callable_str(info.call)
)
)

if not isinstance(scope, Scope):
scope = Scope.from_legacy(scope)

if stack is None:
injection_logger.debug("Exit stack not provided, creating own")
Expand Down Expand Up @@ -143,7 +157,7 @@ def inject(
inner_info.call,
)

return call_sync(stack, inner_info, inner_scope)
return call_sync(stack, inner_info, inner_scope) # type: ignore
except Exception as exc:
injection_logger.debug("Passing exception %r (%r) to downstream", exc, type(exc))
with contextlib.suppress(StopIteration):
Expand All @@ -153,7 +167,7 @@ def inject(


async def ainject(
scope: collections.abc.Mapping[str, typing.Any],
scope: collections.abc.Mapping[str, typing.Any] | Scope,
info: CallableInfo[typing.Any],
stack: contextlib.AsyncExitStack | None = None,
cache: collections.abc.MutableMapping[CacheKey, typing.Any] | None = None,
Expand All @@ -171,6 +185,9 @@ async def ainject(
:param override: override dependencies
:return: result of callable
"""
if not isinstance(scope, Scope):
scope = Scope.from_legacy(scope)

if stack is None:
injection_logger.debug("Exit stack not provided, creating own")
async with contextlib.AsyncExitStack() as stack:
Expand Down Expand Up @@ -201,9 +218,9 @@ async def ainject(
)

if info.async_:
return await call_async(stack, inner_info, inner_scope)
return await call_async(stack, inner_info, inner_scope) # type: ignore

return call_sync(stack, inner_info, inner_scope)
return call_sync(stack, inner_info, inner_scope) # type: ignore
except Exception as exc:
injection_logger.debug("Passing exception %r (%r) to downstream", exc, type(exc))
with contextlib.suppress(StopIteration):
Expand Down
23 changes: 12 additions & 11 deletions fundi/inject.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import typing
from typing import overload
from collections.abc import Generator, AsyncGenerator, Mapping, MutableMapping, Awaitable

from fundi.scope import Scope
from fundi.types import CacheKey, CallableInfo

from contextlib import (
Expand All @@ -16,82 +17,82 @@ R = typing.TypeVar("R")
ExitStack = AsyncExitStack | SyncExitStack

def injection_impl(
scope: Mapping[str, typing.Any],
scope: Scope,
info: CallableInfo[typing.Any],
cache: MutableMapping[CacheKey, typing.Any],
override: Mapping[typing.Callable[..., typing.Any], typing.Any] | None,
) -> Generator[
tuple[Mapping[str, typing.Any], CallableInfo[typing.Any], bool],
tuple[Mapping[str, typing.Any] | Scope, CallableInfo[typing.Any], bool],
typing.Any,
None,
]: ...
@overload
def inject(
scope: Mapping[str, typing.Any],
scope: Mapping[str, typing.Any] | Scope,
info: CallableInfo[Generator[R, None, None]],
stack: ExitStack | None = None,
cache: MutableMapping[CacheKey, typing.Any] | None = None,
override: Mapping[typing.Callable[..., typing.Any], typing.Any] | None = None,
) -> R: ...
@overload
def inject(
scope: Mapping[str, typing.Any],
scope: Mapping[str, typing.Any] | Scope,
info: CallableInfo[AbstractContextManager[R]],
stack: ExitStack | None = None,
cache: MutableMapping[CacheKey, typing.Any] | None = None,
override: Mapping[typing.Callable[..., typing.Any], typing.Any] | None = None,
) -> R: ...
@overload
def inject(
scope: Mapping[str, typing.Any],
scope: Mapping[str, typing.Any] | Scope,
info: CallableInfo[R],
stack: ExitStack | None = None,
cache: MutableMapping[CacheKey, typing.Any] | None = None,
override: Mapping[typing.Callable[..., typing.Any], typing.Any] | None = None,
) -> R: ...
@overload
async def ainject(
scope: Mapping[str, typing.Any],
scope: Mapping[str, typing.Any] | Scope,
info: CallableInfo[Generator[R, None, None]],
stack: AsyncExitStack | None = None,
cache: MutableMapping[CacheKey, typing.Any] | None = None,
override: Mapping[typing.Callable[..., typing.Any], typing.Any] | None = None,
) -> R: ...
@overload
async def ainject(
scope: Mapping[str, typing.Any],
scope: Mapping[str, typing.Any] | Scope,
info: CallableInfo[AsyncGenerator[R, None]],
stack: AsyncExitStack | None = None,
cache: MutableMapping[CacheKey, typing.Any] | None = None,
override: Mapping[typing.Callable[..., typing.Any], typing.Any] | None = None,
) -> R: ...
@overload
async def ainject(
scope: Mapping[str, typing.Any],
scope: Mapping[str, typing.Any] | Scope,
info: CallableInfo[Awaitable[R]],
stack: AsyncExitStack | None = None,
cache: MutableMapping[CacheKey, typing.Any] | None = None,
override: Mapping[typing.Callable[..., typing.Any], typing.Any] | None = None,
) -> R: ...
@overload
async def ainject(
scope: Mapping[str, typing.Any],
scope: Mapping[str, typing.Any] | Scope,
info: CallableInfo[AbstractAsyncContextManager[R]],
stack: AsyncExitStack | None = None,
cache: MutableMapping[CacheKey, typing.Any] | None = None,
override: Mapping[typing.Callable[..., typing.Any], typing.Any] | None = None,
) -> R: ...
@overload
async def ainject(
scope: Mapping[str, typing.Any],
scope: Mapping[str, typing.Any] | Scope,
info: CallableInfo[AbstractContextManager[R]],
stack: AsyncExitStack | None = None,
cache: MutableMapping[CacheKey, typing.Any] | None = None,
override: Mapping[typing.Callable[..., typing.Any], typing.Any] | None = None,
) -> R: ...
@overload
async def ainject(
scope: Mapping[str, typing.Any],
scope: Mapping[str, typing.Any] | Scope,
info: CallableInfo[R],
stack: AsyncExitStack | None = None,
cache: MutableMapping[CacheKey, typing.Any] | None = None,
Expand Down
40 changes: 28 additions & 12 deletions fundi/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import collections.abc

from fundi.logging import get_logger
from fundi.util import normalize_annotation
from fundi.util import normalize_annotation, callable_str
from fundi.scope import Scope, NO_VALUE, TypeInstance, TypeFactory
from fundi.types import CacheKey, CallableInfo, ParameterResult, Parameter

logger = get_logger("resolve")
Expand All @@ -17,7 +18,7 @@ def resolve_by_dependency(

assert dependency is not None

logger.debug("Resolving %r using dependency %r", param.name, dependency.call)
logger.debug("Resolving %r using dependency %s", param.name, callable_str(dependency.call))

value = override.get(dependency.call)
if value is not None:
Expand All @@ -40,25 +41,35 @@ def resolve_by_dependency(
return ParameterResult(param, None, dependency, resolved=False)


def resolve_by_type(
scope: collections.abc.Mapping[str, typing.Any], param: Parameter
) -> ParameterResult:
def resolve_by_type(scope: Scope, param: Parameter) -> ParameterResult:
logger.debug("Resolving %r using annotation %r", param.name, param.annotation)
type_options = normalize_annotation(param.annotation)

for value in scope.values():
if not isinstance(value, type_options):
for type_ in type_options:
value = scope.resolve_by_type(typing.cast(type[typing.Any], type_))

if value is NO_VALUE:
continue

logger.debug("Found value %r for %r: Annotation", value, param.name)
match value:
case TypeInstance(value):
logger.debug("Found type instance %r for %r", value, param.name)
return ParameterResult(param, value, None, resolved=True)
case TypeFactory(factory):
logger.debug(
"Found type factory %s for %r",
callable_str(factory.call),
param.name,
)
return ParameterResult(param, None, factory, False)

return ParameterResult(param, value, None, resolved=True)
logger.debug("Not found value for %r using annotation %r", param.name, param.annotation)

return ParameterResult(param, None, None, resolved=False)


def resolve(
scope: collections.abc.Mapping[str, typing.Any],
scope: Scope,
info: CallableInfo[typing.Any],
cache: collections.abc.Mapping[CacheKey, typing.Any],
override: collections.abc.Mapping[typing.Callable[..., typing.Any], typing.Any] | None = None,
Expand Down Expand Up @@ -102,12 +113,17 @@ def resolve(
if parameter.resolve_by_type:
result = resolve_by_type(scope, parameter)

if result.dependency is not None:
yield resolve_by_dependency(
parameter.copy(from_=result.dependency), cache, override
)
continue

if result.resolved:
yield result
continue

elif parameter.name in scope:
value = scope[parameter.name]
elif (value := scope.resolve_by_name(parameter.name)) is not NO_VALUE:
logger.debug("Found value %r for %r: Name", value, parameter.name)
yield ParameterResult(parameter, value, None, resolved=True)
continue
Expand Down
Loading