Skip to content
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
3 changes: 2 additions & 1 deletion example/basic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Protocol

from uncoupled.container import Container, Depends
from uncoupled.container import Container, Depends, inject


# Define the protocol (i.e the interface)
Expand All @@ -15,6 +15,7 @@ def compute(self) -> int:


# Inject with `Depends` the required protocol
@inject
def my_function(svc: IService = Depends(IService)) -> None:
print(svc.compute()) # Should return 42 !

Expand Down
3 changes: 2 additions & 1 deletion example/resolver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Protocol

from uncoupled.container import Container, Depends
from uncoupled.container import Container, Depends, inject


class Interface(Protocol):
Expand All @@ -23,6 +23,7 @@ def foo(self, x: int) -> int:
return self.bar * x


@inject
def run(
foo_first_impl: Interface = Depends(Interface),
foo_second_impl: Interface = Depends(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uncoupled"
version = "0.1.1"
version = "0.1.2"
description = "Modern Dependency Injection Container for Python 3.12+"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
69 changes: 55 additions & 14 deletions src/uncoupled/container.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from collections.abc import Callable, Hashable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Self, cast
from inspect import signature
import inspect
from typing import TYPE_CHECKING, Any, Self, cast, get_type_hints
from uncoupled.lifetime import Lifetime

from uncoupled.exception import (
Expand Down Expand Up @@ -119,25 +121,64 @@ def get_concrete_instance[I](
raise UnregisteredInterfaceError(interface)


def make_proxy_method(name: str):
def proxy_method(self, *args: Any, **kwargs: Any) -> Any:
interface = object.__getattribute__(self, "_interface")
resolver = object.__getattribute__(self, "_resolver")
concrete = Container._get_instance().get_concrete_instance(interface, resolver)
return getattr(concrete, name)(*args, **kwargs)
def make_not_wrapped_method(name: str):
def _make_not_wrapped_method(self, *args: Any, **kwargs: Any) -> Any:
if name in "__getattribute__" and ("_interface" in args or "_resolver" in args):
return object.__getattribute__(self, *args, **kwargs)
raise RuntimeError(
"You are trying to call a Depends object, did you forget to use @inject ?"
)

return proxy_method
return _make_not_wrapped_method


class LazyProxy[I]:
def __init__(self, interface: type[I], resolver: Resolver | None = None) -> None:
class _DependsMarker[I]:
def __init__(self, interface: type[I], resolver: Resolver[I] | None = None) -> None:
self._interface = interface
self._resolver = resolver

__call__ = make_proxy_method("__call__")
__getattribute__ = make_proxy_method("__getattribute__")
__repr__ = make_proxy_method("__repr__")

for name, _ in inspect.getmembers(_DependsMarker):
if name in {
"__class__",
"__new__",
"__call__",
"__init__",
"__dict__",
"__class_getitem__",
"__parameters__",
"__setattr__",
}:
continue
setattr(_DependsMarker, name, make_not_wrapped_method(name))


def Depends[I](interface: type[I], resolver: Resolver[I] | None = None) -> I:
return cast(I, LazyProxy[I](interface, resolver))
return cast(I, _DependsMarker[I](interface, resolver))


def Resolve[I](
interface: type[I], resolver: Resolver[I] | None = None
) -> Callable[[], I]:
@inject
def _resolve(i: I = Depends(interface, resolver)) -> I:
return i

return _resolve


def inject[F: Callable](func: F) -> F:
def _uncoupled_inject_func(*args: Any, **kwargs: Any) -> Any:
bound_args = signature(func).bind_partial(*args, **kwargs)
bound_args.apply_defaults()

for arg_name, arg_value in bound_args.arguments.items():
if isinstance(arg_value, _DependsMarker):
concrete_instance = Container._get_instance().get_concrete_instance(
arg_value._interface, arg_value._resolver
)
bound_args.arguments[arg_name] = concrete_instance

return func(*bound_args.args, **bound_args.kwargs)

return cast(F, _uncoupled_inject_func)
5 changes: 4 additions & 1 deletion tests/test_depends.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Protocol
import pytest

from uncoupled.container import Container, Depends
from uncoupled.container import Container, Depends, inject


class Interface(Protocol):
Expand Down Expand Up @@ -35,13 +35,15 @@ def init_container() -> Generator:


def test_injected() -> None:
@inject
def foo(interface: Interface = Depends(Interface)) -> int:
return interface.foo()

assert foo() == 42


def test_injected_with_resolver() -> None:
@inject
def foo(
interface: Interface = Depends(
Interface,
Expand All @@ -56,6 +58,7 @@ def foo(


def test_injected_with_lifetime() -> None:
@inject
def foo(
interface: Interface = Depends(
Interface,
Expand Down
3 changes: 2 additions & 1 deletion tests/test_depends_scoped.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from uuid import uuid4
import pytest

from uncoupled.container import Container, Depends
from uncoupled.container import Container, Depends, inject


class Interface(Protocol):
Expand All @@ -27,6 +27,7 @@ def init_container_scoped() -> Generator:


def test_instance_recreated() -> None:
@inject
def get_instance(inst: Interface = Depends(Interface)) -> Interface:
return inst

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading