Finistere / antidote

Dependency injection for Python
MIT License
90 stars 9 forks source link

Interface and implements raises an error when using a union with a parametrized generic #56

Closed rschyboll closed 2 years ago

rschyboll commented 2 years ago

Hi, first i just wanted to say that i really like antidote, a really refreshing take on DI in Python :D

I encountered today an error, when defining an interface, and its implementation, with a method that has a parameter that consists of a union of a subscripted generic (like list[str]), it throws the following error:

Traceback (most recent call last):
  File "/workspace/test.py", line 19, in <module>
    class TestClass(ITestClass):
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/lib/interface/interface.py", line 366, in by_default
    register_default_implementation(
  File "src/antidote/_internal/wrapper.pyx", line 136, in antidote._internal.wrapper.SyncInjectedWrapper.__call__
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/lib/interface/_internal.py", line 200, in register_default_implementation
    injectable(implementation, type_hints_locals=type_hints_locals)
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/lib/injectable/injectable.py", line 172, in injectable
    return klass and reg(klass) or reg
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/lib/injectable/injectable.py", line 163, in reg
    register_injectable(
  File "src/antidote/_internal/wrapper.pyx", line 136, in antidote._internal.wrapper.SyncInjectedWrapper.__call__
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/lib/injectable/_internal.py", line 32, in register_injectable
    wiring.wire(klass=klass, type_hints_locals=type_hints_locals)
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/core/wiring.py", line 244, in wire
    wire_class(klass=cls, wiring=self, type_hints_locals=type_hints_locals)
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/core/_wiring.py", line 43, in wire_class
    injected_method = inject(
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/core/injection.py", line 449, in __call__
    return __arg and decorate(__arg) or decorate
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/core/injection.py", line 440, in decorate
    return raw_inject(
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/core/_injection.py", line 93, in raw_inject
    blueprint = _build_injection_blueprint(
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/core/_injection.py", line 132, in _build_injection_blueprint
    annotated = _build_from_annotations(arguments)
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/core/_injection.py", line 163, in _build_from_annotations
    dependency = extract_annotated_arg_dependency(arg)
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/core/_annotations.py", line 55, in extract_annotated_arg_dependency
    type_hint, origin, args = _extract_type_hint(argument)
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/core/_annotations.py", line 176, in _extract_type_hint
    if argument.is_optional:
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/_internal/argspec.py", line 27, in is_optional
    return is_optional(self.type_hint)
  File "/home/vscode/.local/lib/python3.10/site-packages/antidote/_internal/utils/__init__.py", line 112, in is_optional
    and (isinstance(None, args[1]) or isinstance(None, args[0]))
  File "/usr/local/lib/python3.10/typing.py", line 994, in __instancecheck__
    return self.__subclasscheck__(type(obj))
  File "/usr/local/lib/python3.10/typing.py", line 997, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

I created a simple reproducible example that shows that error:

from typing import List

from antidote import implements, inject, interface

@interface
class ITestClass:
    def static(self, actions: str | List[str]) -> bool | None:
        pass

@implements(ITestClass).by_default
class TestClass(ITestClass):
    def static(self, actions: str | List[str]) -> bool | None:
        pass

def test(aclHelper: ITestClass = inject.me()):
    print(ITestClass)

test()

It seems that the library is calling a isinstance check on the list[str] type in the is_optional method, so there might be required a check, if that type is a generic type, maybe with a get_args call?

Finistere commented 2 years ago

Hello! Thanks for the bug report and happy you like it. :)

It's indeed the is_optional check that fails with List[str]. I've replaced it with a check is type(None) instead, the goal is only to detect Optional[T] arguments for inject.me(). Pushing a fix currently. I will close the issue once published!

Finistere commented 2 years ago

Fixed with 1.4.2

Finistere commented 2 years ago

By the way, by_default is not necessary in your example. The goal of @implements(...).by_default is to have a default implementation that is used when no implementation can be provided. Either because there is simply none at all or because none matches the constraints on the predicates (more advanced use of @interface).

So @implements(...) is enough most of the time unless you want somebody else to be able to override easily a default implementation that you defined.

The documentation needs some rework on those topics. ^^

Finistere commented 2 years ago

And if you have any comments on the library, I'd love to hear them!

rschyboll commented 2 years ago

Thanks!

Just tested it, the fix seems to be working. And thanks for the tip, I misunderstood how by_default actually works, need to change that in my project.

I do have one question, but I'm going to open a new issue for it ^^