diff --git a/example/basic.py b/example/basic.py index 8e562d6..77928f0 100644 --- a/example/basic.py +++ b/example/basic.py @@ -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) @@ -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 ! diff --git a/example/resolver.py b/example/resolver.py index f0ee230..6993025 100644 --- a/example/resolver.py +++ b/example/resolver.py @@ -1,6 +1,6 @@ from typing import Protocol -from uncoupled.container import Container, Depends +from uncoupled.container import Container, Depends, inject class Interface(Protocol): @@ -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( diff --git a/pyproject.toml b/pyproject.toml index e81fc54..0c80408 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/uncoupled/container.py b/src/uncoupled/container.py index acb1d75..a54aa57 100644 --- a/src/uncoupled/container.py +++ b/src/uncoupled/container.py @@ -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 ( @@ -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) diff --git a/tests/test_depends.py b/tests/test_depends.py index dd0f7cd..62a5e06 100644 --- a/tests/test_depends.py +++ b/tests/test_depends.py @@ -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): @@ -35,6 +35,7 @@ def init_container() -> Generator: def test_injected() -> None: + @inject def foo(interface: Interface = Depends(Interface)) -> int: return interface.foo() @@ -42,6 +43,7 @@ def foo(interface: Interface = Depends(Interface)) -> int: def test_injected_with_resolver() -> None: + @inject def foo( interface: Interface = Depends( Interface, @@ -56,6 +58,7 @@ def foo( def test_injected_with_lifetime() -> None: + @inject def foo( interface: Interface = Depends( Interface, diff --git a/tests/test_depends_scoped.py b/tests/test_depends_scoped.py index 8ae7e5a..dbe9492 100644 --- a/tests/test_depends_scoped.py +++ b/tests/test_depends_scoped.py @@ -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): @@ -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 diff --git a/uv.lock b/uv.lock index b094d04..e3a6835 100644 --- a/uv.lock +++ b/uv.lock @@ -79,7 +79,7 @@ wheels = [ [[package]] name = "uncoupled" -version = "0.1.1" +version = "0.1.2" source = { editable = "." } [package.dev-dependencies]