Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type hint broken for inject.attr when using ABCMeta #69

Open
TheEdgeOfRage opened this issue Sep 23, 2020 · 8 comments
Open

Type hint broken for inject.attr when using ABCMeta #69

TheEdgeOfRage opened this issue Sep 23, 2020 · 8 comments

Comments

@TheEdgeOfRage
Copy link

I'm not sure whether it's possible to do fix this at all, or whether it's an inherent Python thing. Also, just a heads up. This issue is only related to static type checking. It doesn't manifest itself in runtime.

I have an interface class (let's say BaseService) that has ABCMeta as its metaclass with @abstractmethod decorated empty methods. Several other classes (in my example Service) inherit from it and implement the methods.
Then I have a factory function that instantiates one of the Implementations and returns it with the BaseService type hint. I use inject bind the factory to the BaseService type.

To resolve the dependency in another class I used inject.attr(BaseService), but the returned value is type hinted as Union[object, Any], instead of BaseService. This leads mypy to flag any code that uses the resolved dependency, saying that the object doesn't have any of the attributes that BaseService has.

Here is a simple reproducible example:

Click to expand
from abc import ABCMeta, abstractmethod

import inject


# Dummy service interface (abstract base) class
class BaseService(metaclass=ABCMeta):
    @abstractmethod
    def f(self) -> None:
        pass


# Dummy service implementation
class Service(BaseService):
    def f(self) -> None:
        print('f')


# Dummy service factory (for multiple different implementations)
def service_factory() -> BaseService:
    return Service()


# Simple Test class to trigger the issue
class Test:
    service = inject.attr(BaseService)

    def test(self) -> None:
        self.service.f()  # mypy error: Item "object" of "Union[object, Any]" has no attribute "f"


# Binding the factory as a constructor (not sure if this is necessary)
def bindings(binder: inject.Binder) -> None:
    binder.bind_to_constructor(BaseService, service_factory)


inject.configure(bindings)
Test().test()

As I said, this code works just fine when I run it through the CPython (3.8) interpreter. The problem is only related to mypy static checking. Running mypy on the code yields the following error:

test.py:29: error: Item "object" of "Union[object, Any]" has no attribute "f"

If i instead remove metaclass=ABCMeta and the @abstractmethod decorator from BaseService and use it as a normal base class, there is no issue. My guess is that the ABCMeta metaclass breaks the type hint for some reason, but I don't know enough about how it works to even make an educated guess.

If this issue is not in fact rooted in this library, let me know and I'll open the issue elsewhere.

Thanks :)

@ivankorobkov
Copy link
Owner

Hi!

Thank you for a detailed explanation. I'll try to research the issue today or tomorrow and write back.

@TheEdgeOfRage
Copy link
Author

I have found a workaround for this, but it's not pretty.

from typing import cast


class Test:
    service = cast(BaseService, inject.attr(BaseService))

    def test(self) -> None:
        self.service.f()  # No error occurs now

By using the cast function from the typing module, mypy correctly identifies the type. It works, but it requires code duplication and isn't really as neat as just having inject.attr(Type).

@JosXa
Copy link

JosXa commented Sep 26, 2020

I suppose another workaround could be:

T = TypeVar("T", bound=Type)

inject.attr = cast(Callable[[Type[T]], T], inject.attr)

so you only have to do it once.
Haven't tested that though, I'm on a phone and only just stumbled over this library. Seriously considering to switch from Haps...

@JosXa
Copy link

JosXa commented Sep 27, 2020

With these type declarations:

Injectable = Union[object, Any]
T = TypeVar('T', bound=Injectable)

In combination with abstract base classes it seems that we're running into a very common problem: python/mypy#5374 (comment)

@ivankorobkov
Copy link
Owner

@TheEdgeOfRage I haven't found any way how to solve this in inject. Also I'm not a mypy expert. If anyone knows how to make this work, please, make a PR.

@Rahix
Copy link

Rahix commented Oct 8, 2020

I'm hitting this problem in an unrelated project and found a workaround which might help here as well. In python/mypy#4717, someone found out that you can use Callable[..., T] as a "replacement" for Type[T] which allows abstract types. So something like this could work:

T = TypeVar("T", bound=Union[object, Any])
def attr(t: Callable[..., T]) -> T:
    ...

It does smell like a horrible hack though ...

@AlTosterino
Copy link

Hey!

Any update on this? I also stumbled upon this issue today ;)
I see that python/mypy#5374 is still Open :/

@ivankorobkov
Copy link
Owner

@AlTosterino Hi! I still know no way to fix this :-( Maybe, someone will find a way and make a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants