Repository: maldoinc/wireup

Issue:
Warn when using Injected[T] in Wireup services
The use of `Injected[T]`/ `Annotated[T, Inject()]` is required when Wireup injects in foreign targets but not necessary in its own services. If a Wireup service has any annotations of that form it should warn and let the user know it's unnecessary. This can be done in the registry when adding dependencies by checking for instances of `EmptyContainerInjectionRequest` which is what `Inject()` produces.

Discussion:
@yutaroyamanaka: Hi! I was amazed by your project. Let me work on this issue.

@maldoinc: Hi. Thanks for for offering. Feel free to submit a PR for this.

Solution patch:
diff --git a/wireup/ioc/service_registry.py b/wireup/ioc/service_registry.py
index 3389b17..1d87b2f 100644
--- a/wireup/ioc/service_registry.py
+++ b/wireup/ioc/service_registry.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import inspect
 import typing
+import warnings
 from collections import defaultdict
 from dataclasses import dataclass
 from enum import Enum, auto
@@ -15,8 +16,9 @@ from wireup.errors import (
     UnknownQualifiedServiceRequestedError,
     WireupError,
 )
-from wireup.ioc.types import AnnotatedParameter, AnyCallable
+from wireup.ioc.types import AnnotatedParameter, AnyCallable, EmptyContainerInjectionRequest
 from wireup.ioc.util import ensure_is_type, get_globals, param_get_annotation
+from wireup.ioc.validation import stringify_type
 
 if TYPE_CHECKING:
     from wireup.ioc.types import (
@@ -135,6 +137,14 @@ class ServiceRegistry:
                 msg = f"Wireup dependencies must have types. Please add a type to the '{name}' parameter in {target}."
                 raise WireupError(msg)
 
+            if isinstance(annotated_param.annotation, EmptyContainerInjectionRequest):
+                warnings.warn(
+                    f"Redundant Injected[T] or Annotated[T, Inject()] in parameter '{name}' of "
+                    f"{stringify_type(target)}. See: "
+                    "https://maldoinc.github.io/wireup/latest/annotations/",
+                    stacklevel=2,
+                )
+
             self.dependencies[target][name] = annotated_param
 
     def is_impl_with_qualifier_known(self, klass: type, qualifier_value: Qualifier | None) -> bool:
diff --git a/wireup/ioc/validation.py b/wireup/ioc/validation.py
index f87c6db..cc34761 100644
--- a/wireup/ioc/validation.py
+++ b/wireup/ioc/validation.py
@@ -20,6 +20,10 @@ def hide_annotated_names(func: AnyCallable) -> None:
         return
 
     names_to_hide = get_inject_annotated_parameters(func)
+
+    if not names_to_hide:
+        return
+
     orig_sig = inspect.signature(func)
     filtered_params = {name: param for name, param in orig_sig.parameters.items() if param.name not in names_to_hide}
     new_sig = inspect.Signature(parameters=list(filtered_params.values()), return_annotation=orig_sig.return_annotation)


Test patch:
diff --git a/test/unit/services/with_annotations/services.py b/test/unit/services/with_annotations/services.py
index e645b03..217ea4d 100644
--- a/test/unit/services/with_annotations/services.py
+++ b/test/unit/services/with_annotations/services.py
@@ -1,7 +1,7 @@
 import abc
 
 from typing_extensions import Annotated
-from wireup import Inject, abstract, service
+from wireup import Inject, Injected, abstract, service
 
 from test.unit.services.no_annotations.random.random_service import RandomService
 from test.unit.services.no_annotations.random.truly_random_service import TrulyRandomService
@@ -45,6 +45,12 @@ class OtherFooImpl(Foo):
         return "other foo"
 
 
+@service
+class FooImplWithInjected:
+    def __init__(self, foo: Injected[Foo]) -> None:
+        self.foo = foo
+
+
 @service(lifetime="scoped")
 class ScopedService: ...
 
diff --git a/test/unit/test_service_registry.py b/test/unit/test_service_registry.py
index 2e16d12..a3fdfd1 100644
--- a/test/unit/test_service_registry.py
+++ b/test/unit/test_service_registry.py
@@ -9,6 +9,7 @@ from wireup.errors import (
 from wireup.ioc.service_registry import ServiceRegistry
 
 from test.unit.services.no_annotations.random.random_service import RandomService
+from test.unit.services.with_annotations.services import FooImplWithInjected
 
 
 @pytest.fixture
@@ -70,6 +71,11 @@ def test_is_type_with_qualifier_known(registry: ServiceRegistry) -> None:
     assert registry.is_type_with_qualifier_known(MyService, "default")
 
 
+def test_register_with_redundant_annotation(registry: ServiceRegistry) -> None:
+    with pytest.warns(UserWarning, match=r"Redundant Injected\[T\] or Annotated\[T, Inject\(\)\] in parameter"):
+        registry.register(FooImplWithInjected, lifetime="singleton")
+
+
 def test_is_interface_known(registry: ServiceRegistry) -> None:
     assert not registry.is_interface_known(MyInterface)