kodemore / kink

Dependency injection container made for Python
MIT License
393 stars 25 forks source link

Error with @inject typing and mypy #36

Open zulrang opened 1 year ago

zulrang commented 1 year ago

This code works with mypy:

class MyClass:
    pass

di[MyClass] = MyClass()

async def my_async_func(my_class: MyClass) -> str:
    return MyClass.__class__.__name__

async def calling_func() -> str:
    return await my_async_func(di[MyClass])

While this code results in an error:

class MyClass:
    pass

di[MyClass] = MyClass()

@inject
async def my_async_func(my_class: MyClass) -> str:
    return MyClass.__class__.__name__

async def calling_func() -> str:
    return await my_async_func()

mypy error: error: Incompatible types in "await" (actual type "Union[Any, Callable[..., Any]]", expected type "Awaitable[Any]")

Aside from ignoring the calling line, is there any way to make mypy happy with the type returned from the @inject decorator?

dkraczkowski commented 1 year ago

@zulrang Thanks for raising this issue, I think @inject decorator return types might need a fix. Will update you later on today on this.

seanlossef-kce commented 1 year ago

Any update on this?

andrewbutlerboudakiankce commented 1 year ago

Any update on this?

zulrang commented 1 year ago

I think this will be necessary: https://peps.python.org/pep-0612/

skewty commented 1 year ago

This is also problematic in PyCharm 2023.,2.

@zulrang were you able to figure out typing using PEP612 that seems to work in PyCharm / VSCode?

Perhaps something more simple (and backwards compatible) is possible using typing.overload?

andrewbutlerboudakiankce commented 1 year ago
@overload
def inject(
        _service: None = None,
        alias: Any | None = None,
        bind: Dict[str, Any] | None = None,
        container: Container = di,
        use_factory: bool = False) -> Callable[[ServiceDefinition], ServiceResult]: ...

@overload
def inject(
        _service: ServiceDefinition,
        alias: Any | None = None,
        bind: Dict[str, Any] | None = None,
        container: Container = di,
        use_factory: bool = False) -> ServiceResult: ...

^Try this

skewty commented 1 year ago

Those overloads are not working for me in PyCharm.. Are they working for you in VSCode?

# Python version 3.11.4
from typing import Any, Callable, Type, TypeVar, cast, overload

from kink import Container as KinkContainer, di as kink_di, inject as kink_inject
from kink.inject import ServiceDefinition, ServiceResult

T = TypeVar("T")

class Container(KinkContainer):
    @overload
    def __getitem__(self, key: str) -> Any:
        ...

    @overload
    def __getitem__(self, key: Type[T]) -> T:
        ...

    def __getitem__(self, key):
        return super().__getitem__(key)

di = cast(Container, kink_di)

@overload
def inject(
    _service: None = None,
    alias: Any | None = None,
    bind: dict[str, Any] | None = None,
    container: Container = di,
    use_factory: bool = False,
) -> Callable[[ServiceDefinition], ServiceResult]:
    ...

@overload
def inject(
    _service: ServiceDefinition,
    alias: Any | None = None,
    bind: dict[str, Any] | None = None,
    container: Container = di,
    use_factory: bool = False,
) -> ServiceResult:
    ...

def inject(
    _service: ServiceDefinition = None,
    alias: Any = None,
    bind: dict[str, Any] = None,
    container: Container = di,
    use_factory: bool = False,
):
    return kink_inject(_service, alias, bind, container, use_factory)

@inject
class TestA:
    def as_dict(self) -> dict:
        return self.__dict__

@inject()
class TestB:
    def as_dict(self) -> dict:
        return self.__dict__

di[bytes].|    # auto-complete  -- but inject decorator not used
di[TestA].|    # no auto-complete
di[TestB].|    # no auto-complete
seanlossef-kce commented 1 year ago

Those overloads are working for me in vscode

seanlossef-kce commented 1 year ago

To clarify, those overloads fix the issue where mypy cannot resolve the return type of an injected function because the injected parameters are not being explicitly passed in.

It seems like the issue you're talking about might be different. I also do not get intellisense on the di container itself when doing di[TestA].

andrewbutlerboudakiankce commented 1 year ago

Adding inject overloads as well as overloads in your snippet to __getitem__, as well as an additional overload to __setitem__ as follows appears to work in VS Code for autocomplete:

    @overload
    def __setitem__(self, key: Type[T], value: T) -> None:
        ...

    @overload
    def __setitem__(self, key: str, value: Any) -> None:
        ...
skewty commented 11 months ago

Played around in PyCharm a bit and this is what actually got type hinting working:

T = TypeVar("T")

@overload
def inject(
    _service: None = None,
    alias: Any | None = None,
    bind: dict[str, Any] | None = None,
    container: Container = di,
    use_factory: bool = False,
) -> Callable[[T], T]:
    ...

# @overload
# def inject(
#     _service = None,
#     *,
#     alias: Any | None = None,
#     bind: dict[str, Any] | None = None,
#     container: Container = di,
#     use_factory: bool = False,
# ) -> Callable[[T], T]:
#     ...

def inject(*args, **kwargs):
    return kink_inject(*args, **kwargs)

If you un-comment the middle overload it breaks di[TestA] type hinting.

dkraczkowski commented 11 months ago

@seanlossef-kce The Issue with intellisense from the container as of 0.7.0 should be fixed now (also added auto-wiring for optional types).

With inject solution I would like to play a bit with this. Unless anyone is willing to prepare a PR with tests, which would help me a lot as time is the essence ;)

Thanks for your understanding and support!

skewty commented 6 months ago

Any progress on the type hints for @inject? This issue is now over a year old.

For me, the typing hinting issues and mypy issues make kink unusable in projects and I am guessing many others feel the same. Nobody likes linter overrides all over their code.

dkraczkowski commented 4 months ago

@skewty I'd like to shed some light on the management and contribution process for this, and potentially other open-source projects. It's important to note that there's a limited number of contributors, particularly those offering improvements on the technical side. I'm not compensated anyway for the work I am doing here. My contributions are done in my personal time, outside of professional obligations and amidst other demands of life, which have been lately considerable.

This means my capacity to address problems, roll out new features, or respond quickly might be limited. However, I'm always open to contributions from the community. Since this is an open-source project, you're more than welcome to contribute by submitting your PR.