2

I am working on making stubs for an external ORM library, I have encountered an issue that I am not sure how to overcome though. So the example bellow technically passes the mypy check, but only after expecting the library user to tediously repeat themselves during class declaration.

# Library stubs:
from typing import Generic, TypeVar, Type, Any, Optional
from collections.abc import Collection, Sequence
from abc import ABC


T = TypeVar('T', bound='BaseItem')
K = TypeVar('K')

class ItemSet(Generic[K]):
    def get_or_none(self, **kwargs: Any) -> Optional[K]: ...
    def first(self) -> K: ...
    def all(self) -> Collection[K]: ...
    def order_by(self, *args: Any) -> Sequence[K]: ...

class BaseItem(ABC, Generic[T]):
    @classmethod
    def set(cls: Type[T]) -> ItemSet[T]: ...

# User's model:
from library import BaseItem


class FooItem(BaseItem['FooItem']):
    name: str

class BarItem(BaseItem['BarItem']):
    size: float

class BazItem(BaseItem['BazItem']):
    id_: int

reveal_type(FooItem.set())
reveal_type(FooItem.set().all())

This generates this output:

main.py:32: note: Revealed type is "__main__.ItemSet[__main__.FooItem*]"
main.py:33: note: Revealed type is "typing.Collection[__main__.FooItem*]"

Which is exactly what you would expect, however this only works because the user had to pass the class name as a type on every class definition. The omission of type leads to it having the Any type

class FooItem(BaseItem):
    name: str
main.py:32: note: Revealed type is "__main__.ItemSet[Any]"
main.py:33: note: Revealed type is "typing.Collection[Any]"

So my question is how to make so this type inference is invisible to the user?

Mathias Sven
  • 349
  • 3
  • 7

1 Answers1

2

It's because you made it a generic class, it shouldn't be generic class, it is a generic function, essentially. Just use the following:

from typing import Generic, TypeVar, Type, Any, Optional
from collections.abc import Collection, Sequence
from abc import ABC


T = TypeVar('T', bound='BaseItem')
K = TypeVar('K')

class ItemSet(Generic[K]):
    def get_or_none(self, **kwargs: Any) -> Optional[K]: ...
    def first(self) -> K: ...
    def all(self) -> Collection[K]: ...
    def order_by(self, *args: Any) -> Sequence[K]: ...

class BaseItem(ABC):
    @classmethod
    def set(cls: Type[T]) -> ItemSet[T]: ...


class FooItem(BaseItem):
    name: str

class BarItem(BaseItem):
    size: float

class BazItem(BaseItem):
    id_: int

reveal_type(FooItem.set())
reveal_type(FooItem.set().all())

Here's what MyPy thinks (note, I put everything in one module named test.py for brevity):

(py39) Juans-MacBook-Pro:~ juan$ mypy test.py
test.py:29: note: Revealed type is "test.ItemSet[test.FooItem*]"
test.py:30: note: Revealed type is "typing.Collection[test.FooItem*]"

Note, this specific situation is addressed here in the PEP-484 spec

Note, there is a PEP to remove the TypeVar boilerplate:

https://www.python.org/dev/peps/pep-0673/

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • Ahh, I understand, what lead me to keep the Generic was because another part of my Type Hinting relied on it being generic, I suppose this means the problem is definitely on the other part – Mathias Sven Dec 14 '21 at 00:36
  • @MathiasSven you probably just need another type variable, I would guess – juanpa.arrivillaga Dec 14 '21 at 00:38
  • Well, in the code I provided, where I declare `@classmethod`, that is actually a `@classproperty` with [this](https://github.com/neo4j-contrib/neomodel/blob/b1bc5d50b0f64f64cfcf99be7c4622f178a7325a/neomodel/util.py#L338) implementation. This is the [hastebin](https://paste.pythondiscord.com/rereremeco.py) of my implementation of stubs for it but it only worked if I used `Generic` on the `BaseItem`. Note that in the stubs I would still keep it as a @classmethod above the @classproperty since mypy only passes the Type[T] if it is a classmethod – Mathias Sven Dec 14 '21 at 00:48
  • It might be worth another separate stack Question though. – Mathias Sven Dec 14 '21 at 00:51
  • 1
    @MathiasSven yeah, make a [mcve] and post a new question. As an aside, that's a really weird implementation, like, why is the class defined inside the decorator function? It should just be the class! It also doesn't addresss some corner cases... – juanpa.arrivillaga Dec 14 '21 at 00:53
  • 1
    @MathiasSven and note, in the most recent versions of Python, you can just *chain the descriptors!* see: https://stackoverflow.com/questions/128573/using-property-on-classmethods/64738850#64738850 – juanpa.arrivillaga Dec 14 '21 at 00:56
  • Bests me, I was also surprised when I saw the class within the decorator, maybe when I PR my stubs I could also suggest chaining the descriptors to eliminate the need of that function completely, thank you for that tip btw! – Mathias Sven Dec 14 '21 at 01:09