Support different type for class variable and instance variable

Oh, that’s exactly what we need.

Here’s my current version (going with protocol). And, it works perfectly for mypy (actually no, it does not throw error when assigning, but revealed types is wrong), with pyright throwing an error on a line i care about. But that’s probably problem on the side of pyright, thanks a lot!

from __future__ import annotations

from typing import Any, Generic, Iterable, Protocol, TypeVar, overload
from typing_extensions import reveal_type

ClassT = TypeVar("ClassT")
InstanceT = TypeVar("InstanceT")


class ClassOrInstanceAttribute(Protocol, Generic[ClassT, InstanceT]):
    @overload
    def __get__(self, obj: None, owner: type) -> ClassT: ...
    @overload
    def __get__(self, obj: object, owner: type) -> InstanceT: ...
    def __set__(self, obj: object, value: InstanceT) -> None: ...


class TranslationField: ...


class FieldsAggregationMetaClass(type):
    fields: ClassOrInstanceAttribute[Iterable[str], dict[str, set[TranslationField]]]

    def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type: ...


class TranslationOptions(metaclass=FieldsAggregationMetaClass):
    def __init__(self) -> None:
        ...
        # self.fields: dict[str, set[TranslationField]] = {f: set() for f in self.fields}


class BookTranslationOptions(TranslationOptions):
    fields = ["name"]  # This is a problematic line


reveal_type(BookTranslationOptions.fields)
reveal_type(BookTranslationOptions().fields)

Mypy:

types-test.py:10: error: Invariant type variable "ClassT" used in protocol where covariant one is expected  [misc]
    class ClassOrInstanceAttribute(Protocol, Generic[ClassT, InstanceT]):
    ^
types-test.py: note: In member "__new__" of class "FieldsAggregationMetaClass":
types-test.py:24: error: Missing return statement  [empty-body]
        def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type: ...
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
types-test.py: note: At top level:
types-test.py:37: note: Revealed type is "typing.Iterable[builtins.str]"
types-test.py:38: note: Revealed type is "builtins.list[builtins.str]"

Pyright:

types-test.py:10:7 - warning: Type variable "ClassT" used in generic protocol "ClassOrInstanceAttribute" should be covariant (reportInvalidTypeVarUse)
types-test.py:34:14 - error: Expression of type "list[str]" is incompatible with declared type "ClassOrInstanceAttribute[Iterable[str], dict[str, set[TranslationField]]]"
    "list[str]" is incompatible with protocol "ClassOrInstanceAttribute[Iterable[str], dict[str, set[TranslationField]]]"
      "__get__" is not present
      "__set__" is not present (reportAssignmentType)
types-test.py:37:13 - information: Type of "BookTranslationOptions.fields" is "Iterable[str]"
types-test.py:38:13 - information: Type of "BookTranslationOptions().fields" is "dict[str, set[TranslationField]]"
1 error, 1 warning, 2 informations