Skip to content

Commit

Permalink
Apply generic class fix also to non-callable types (#8030)
Browse files Browse the repository at this point in the history
This is a follow up for #8021, that applies the fix also to non-callable types. Currently non-callable types are still wrong:
```python
R = TypeVar('R')
T = TypeVar('T')
# Can be any decorator that makes type non-callable.
def classproperty(f: Callable[..., R]) -> R: ...

class C(Generic[T]):
    @classproperty
    def test(self) -> T: ...

x: C[int]
y: Type[C[int]]
reveal_type(x.test)  # Revealed type is 'int', OK
reveal_type(y.test)  # Revealed type is 'T' ???
```

So, #7724 strikes again. It turns out there is not only duplicated logic for attribute kinds (decorators vs normal methods), but also for callable vs non-callable types. In latter case we still need to expand the type (like in other places, e.g., `analyze_var`).
  • Loading branch information
ilevkivskyi authored Nov 29, 2019
1 parent f6e250d commit b5f4df9
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 10 deletions.
27 changes: 17 additions & 10 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,8 +764,8 @@ def analyze_class_attribute_access(itype: Instance,
t = get_proper_type(t)
if isinstance(t, FunctionLike) and is_classmethod:
t = check_self_arg(t, mx.self_type, False, mx.context, name, mx.msg)
result = add_class_tvars(t, itype, isuper, is_classmethod,
mx.builtin_type, mx.self_type, original_vars=original_vars)
result = add_class_tvars(t, isuper, is_classmethod,
mx.self_type, original_vars=original_vars)
if not mx.is_lvalue:
result = analyze_descriptor_access(mx.original_type, result, mx.builtin_type,
mx.msg, mx.context, chk=mx.chk)
Expand Down Expand Up @@ -808,9 +808,8 @@ def analyze_class_attribute_access(itype: Instance,
return typ


def add_class_tvars(t: ProperType, itype: Instance, isuper: Optional[Instance],
def add_class_tvars(t: ProperType, isuper: Optional[Instance],
is_classmethod: bool,
builtin_type: Callable[[str], Instance],
original_type: Type,
original_vars: Optional[List[TypeVarDef]] = None) -> Type:
"""Instantiate type variables during analyze_class_attribute_access,
Expand All @@ -821,12 +820,18 @@ class A(Generic[T]):
def foo(cls: Type[Q]) -> Tuple[T, Q]: ...
class B(A[str]): pass
B.foo()
original_type is the value of the type B in the expression B.foo() or the corresponding
component in case if a union (this is used to bind the self-types); original_vars are type
variables of the class callable on which the method was accessed.
Args:
t: Declared type of the method (or property)
isuper: Current instance mapped to the superclass where method was defined, this
is usually done by map_instance_to_supertype()
is_classmethod: True if this method is decorated with @classmethod
original_type: The value of the type B in the expression B.foo() or the corresponding
component in case of a union (this is used to bind the self-types)
original_vars: Type variables of the class callable on which the method was accessed
Returns:
Expanded method type with added type variables (when needed).
"""
# TODO: verify consistency between Q and T

Expand All @@ -851,10 +856,12 @@ class B(A[str]): pass
t = cast(CallableType, expand_type_by_instance(t, isuper))
return t.copy_modified(variables=tvars + t.variables)
elif isinstance(t, Overloaded):
return Overloaded([cast(CallableType, add_class_tvars(item, itype, isuper, is_classmethod,
builtin_type, original_type,
return Overloaded([cast(CallableType, add_class_tvars(item, isuper,
is_classmethod, original_type,
original_vars=original_vars))
for item in t.items()])
if isuper is not None:
t = cast(ProperType, expand_type_by_instance(t, isuper))
return t


Expand Down
1 change: 1 addition & 0 deletions mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def expand_type(typ: Type, env: Mapping[TypeVarId, Type]) -> Type:
def expand_type_by_instance(typ: Type, instance: Instance) -> Type:
"""Substitute type variables in type using values from an Instance.
Type variables are considered to be bound by the class declaration."""
# TODO: use an overloaded signature? (ProperType stays proper after expansion.)
if instance.args == []:
return typ
else:
Expand Down
54 changes: 54 additions & 0 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -2337,3 +2337,57 @@ class Test():
reveal_type(MakeTwoAppliedSubAbstract()(2)) # N: Revealed type is '__main__.TwoTypes[builtins.str, builtins.int*]'
reveal_type(MakeTwoGenericSubAbstract[str]()('foo')) # N: Revealed type is '__main__.TwoTypes[builtins.str, builtins.str*]'
reveal_type(MakeTwoGenericSubAbstract[str]()(2)) # N: Revealed type is '__main__.TwoTypes[builtins.str, builtins.int*]'

[case testGenericClassPropertyBound]
from typing import Generic, TypeVar, Callable, Type, List, Dict

T = TypeVar('T')
U = TypeVar('U')

def classproperty(f: Callable[..., U]) -> U: ...

class C(Generic[T]):
@classproperty
def test(self) -> T: ...

class D(C[str]): ...
class E1(C[T], Generic[T, U]): ...
class E2(C[U], Generic[T, U]): ...
class G(C[List[T]]): ...

x: C[int]
y: Type[C[int]]
reveal_type(x.test) # N: Revealed type is 'builtins.int*'
reveal_type(y.test) # N: Revealed type is 'builtins.int*'

xd: D
yd: Type[D]
reveal_type(xd.test) # N: Revealed type is 'builtins.str*'
reveal_type(yd.test) # N: Revealed type is 'builtins.str*'

ye1: Type[E1[int, str]]
ye2: Type[E2[int, str]]
reveal_type(ye1.test) # N: Revealed type is 'builtins.int*'
reveal_type(ye2.test) # N: Revealed type is 'builtins.str*'

xg: G[int]
yg: Type[G[int]]
reveal_type(xg.test) # N: Revealed type is 'builtins.list*[builtins.int*]'
reveal_type(yg.test) # N: Revealed type is 'builtins.list*[builtins.int*]'

class Sup:
attr: int
S = TypeVar('S', bound=Sup)

def func(tp: Type[C[S]]) -> S:
reveal_type(tp.test.attr) # N: Revealed type is 'builtins.int'

reg: Dict[S, G[S]]
reveal_type(reg[tp.test]) # N: Revealed type is '__main__.G*[S`-1]'
reveal_type(reg[tp.test].test) # N: Revealed type is 'builtins.list*[S`-1]'

if bool():
return tp.test
else:
return reg[tp.test].test[0]
[builtins fixtures/dict.pyi]

0 comments on commit b5f4df9

Please sign in to comment.