Skip to content

Commit a8befe7

Browse files
authored
Fix class method type variables (#7862)
Fixes #7846 The fix is mostly straightforward, instead of ad-hoc guessing of type variables from the return type, we pass the them all the way through intermediate helper functions. The `ctypes` plugin change is because it used the bare representation of types (the one used by `reveal_type()`) for user errors. So after my change arrangement of stars changed, instead of adjusting it, I switched to the proper type formatting.
1 parent 5b73e2a commit a8befe7

File tree

7 files changed

+231
-58
lines changed

7 files changed

+231
-58
lines changed

mypy/checkexpr.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,12 @@ def analyze_type_type_callee(self, item: ProperType, context: Context) -> Type:
911911
res = type_object_type(item.type, self.named_type)
912912
if isinstance(res, CallableType):
913913
res = res.copy_modified(from_type_type=True)
914-
return expand_type_by_instance(res, item)
914+
expanded = get_proper_type(expand_type_by_instance(res, item))
915+
if isinstance(expanded, CallableType):
916+
# Callee of the form Type[...] should never be generic, only
917+
# proper class objects can be.
918+
expanded = expanded.copy_modified(variables=[])
919+
return expanded
915920
if isinstance(item, UnionType):
916921
return UnionType([self.analyze_type_type_callee(get_proper_type(tp), context)
917922
for tp in item.relevant_items()], item.line)

mypy/checkmember.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Type checking of attribute access"""
22

3-
from typing import cast, Callable, Optional, Union
3+
from typing import cast, Callable, Optional, Union, List
44
from typing_extensions import TYPE_CHECKING
55

66
from mypy.types import (
@@ -240,7 +240,9 @@ def analyze_type_callable_member_access(name: str,
240240
# This check makes sure that when we encounter an operator, we skip looking up
241241
# the corresponding method in the current instance to avoid this edge case.
242242
# See https://github.com/python/mypy/pull/1787 for more info.
243-
result = analyze_class_attribute_access(ret_type, name, mx)
243+
# TODO: do not rely on same type variables being present in all constructor overloads.
244+
result = analyze_class_attribute_access(ret_type, name, mx,
245+
original_vars=typ.items()[0].variables)
244246
if result:
245247
return result
246248
# Look up from the 'type' type.
@@ -649,8 +651,15 @@ def f(self: S) -> T: ...
649651
def analyze_class_attribute_access(itype: Instance,
650652
name: str,
651653
mx: MemberContext,
652-
override_info: Optional[TypeInfo] = None) -> Optional[Type]:
653-
"""original_type is the type of E in the expression E.var"""
654+
override_info: Optional[TypeInfo] = None,
655+
original_vars: Optional[List[TypeVarDef]] = None
656+
) -> Optional[Type]:
657+
"""Analyze access to an attribute on a class object.
658+
659+
itype is the return type of the class object callable, original_type is the type
660+
of E in the expression E.var, original_vars are type variables of the class callable
661+
(for generic classes).
662+
"""
654663
info = itype.type
655664
if override_info:
656665
info = override_info
@@ -718,7 +727,7 @@ def analyze_class_attribute_access(itype: Instance,
718727
# C[int].x # Also an error, since C[int] is same as C at runtime
719728
if isinstance(t, TypeVarType) or has_type_vars(t):
720729
# Exception: access on Type[...], including first argument of class methods is OK.
721-
if not isinstance(get_proper_type(mx.original_type), TypeType):
730+
if not isinstance(get_proper_type(mx.original_type), TypeType) or node.implicit:
722731
if node.node.is_classvar:
723732
message = message_registry.GENERIC_CLASS_VAR_ACCESS
724733
else:
@@ -737,7 +746,7 @@ def analyze_class_attribute_access(itype: Instance,
737746
if isinstance(t, FunctionLike) and is_classmethod:
738747
t = check_self_arg(t, mx.self_type, False, mx.context, name, mx.msg)
739748
result = add_class_tvars(t, itype, isuper, is_classmethod,
740-
mx.builtin_type, mx.self_type)
749+
mx.builtin_type, mx.self_type, original_vars=original_vars)
741750
if not mx.is_lvalue:
742751
result = analyze_descriptor_access(mx.original_type, result, mx.builtin_type,
743752
mx.msg, mx.context, chk=mx.chk)
@@ -783,7 +792,8 @@ def analyze_class_attribute_access(itype: Instance,
783792
def add_class_tvars(t: ProperType, itype: Instance, isuper: Optional[Instance],
784793
is_classmethod: bool,
785794
builtin_type: Callable[[str], Instance],
786-
original_type: Type) -> Type:
795+
original_type: Type,
796+
original_vars: Optional[List[TypeVarDef]] = None) -> Type:
787797
"""Instantiate type variables during analyze_class_attribute_access,
788798
e.g T and Q in the following:
789799
@@ -796,10 +806,10 @@ class B(A[str]): pass
796806
B.foo()
797807
798808
original_type is the value of the type B in the expression B.foo() or the corresponding
799-
component in case if a union (this is used to bind the self-types).
809+
component in case if a union (this is used to bind the self-types); original_vars are type
810+
variables of the class callable on which the method was accessed.
800811
"""
801812
# TODO: verify consistency between Q and T
802-
info = itype.type # type: TypeInfo
803813
if is_classmethod:
804814
assert isuper is not None
805815
t = get_proper_type(expand_type_by_instance(t, isuper))
@@ -815,22 +825,15 @@ class B(A[str]): pass
815825
# This behaviour is useful for defining alternative constructors for generic classes.
816826
# To achieve such behaviour, we add the class type variables that are still free
817827
# (i.e. appear in the return type of the class object on which the method was accessed).
818-
free_ids = {t.id for t in itype.args if isinstance(t, TypeVarType)}
819-
820828
if isinstance(t, CallableType):
821-
# NOTE: in practice either all or none of the variables are free, since
822-
# visit_type_application() will detect any type argument count mismatch and apply
823-
# a correct number of Anys.
824-
tvars = [TypeVarDef(n, n, i + 1, [], builtin_type('builtins.object'), tv.variance)
825-
for (i, n), tv in zip(enumerate(info.type_vars), info.defn.type_vars)
826-
# use 'is' to avoid id clashes with unrelated variables
827-
if any(tv.id is id for id in free_ids)]
829+
tvars = original_vars if original_vars is not None else []
828830
if is_classmethod:
829831
t = bind_self(t, original_type, is_classmethod=True)
830832
return t.copy_modified(variables=tvars + t.variables)
831833
elif isinstance(t, Overloaded):
832834
return Overloaded([cast(CallableType, add_class_tvars(item, itype, isuper, is_classmethod,
833-
builtin_type, original_type))
835+
builtin_type, original_type,
836+
original_vars=original_vars))
834837
for item in t.items()])
835838
return t
836839

mypy/plugins/ctypes.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import mypy.plugin
77
from mypy import nodes
88
from mypy.maptype import map_instance_to_supertype
9+
from mypy.messages import format_type
910
from mypy.subtypes import is_subtype
1011
from mypy.types import (
1112
AnyType, CallableType, Instance, NoneType, Type, TypeOfAny, UnionType,
@@ -125,18 +126,17 @@ def array_constructor_callback(ctx: 'mypy.plugin.FunctionContext') -> Type:
125126
for arg_num, (arg_kind, arg_type) in enumerate(zip(ctx.arg_kinds[0], ctx.arg_types[0]), 1):
126127
if arg_kind == nodes.ARG_POS and not is_subtype(arg_type, allowed):
127128
ctx.api.msg.fail(
128-
'Array constructor argument {} of type "{}"'
129-
' is not convertible to the array element type "{}"'
130-
.format(arg_num, arg_type, et),
131-
ctx.context)
129+
'Array constructor argument {} of type {}'
130+
' is not convertible to the array element type {}'
131+
.format(arg_num, format_type(arg_type), format_type(et)), ctx.context)
132132
elif arg_kind == nodes.ARG_STAR:
133133
ty = ctx.api.named_generic_type("typing.Iterable", [allowed])
134134
if not is_subtype(arg_type, ty):
135+
it = ctx.api.named_generic_type("typing.Iterable", [et])
135136
ctx.api.msg.fail(
136-
'Array constructor argument {} of type "{}"'
137-
' is not convertible to the array element type "Iterable[{}]"'
138-
.format(arg_num, arg_type, et),
139-
ctx.context)
137+
'Array constructor argument {} of type {}'
138+
' is not convertible to the array element type {}'
139+
.format(arg_num, format_type(arg_type), format_type(it)), ctx.context)
140140

141141
return ctx.default_return_type
142142

@@ -204,10 +204,9 @@ def array_value_callback(ctx: 'mypy.plugin.AttributeContext') -> Type:
204204
types.append(_get_text_type(ctx.api))
205205
else:
206206
ctx.api.msg.fail(
207-
'ctypes.Array attribute "value" is only available'
208-
' with element type c_char or c_wchar, not "{}"'
209-
.format(et),
210-
ctx.context)
207+
'Array attribute "value" is only available'
208+
' with element type "c_char" or "c_wchar", not {}'
209+
.format(format_type(et)), ctx.context)
211210
return make_simplified_union(types)
212211
return ctx.default_attr_type
213212

@@ -223,9 +222,8 @@ def array_raw_callback(ctx: 'mypy.plugin.AttributeContext') -> Type:
223222
types.append(_get_bytes_type(ctx.api))
224223
else:
225224
ctx.api.msg.fail(
226-
'ctypes.Array attribute "raw" is only available'
227-
' with element type c_char, not "{}"'
228-
.format(et),
229-
ctx.context)
225+
'Array attribute "raw" is only available'
226+
' with element type "c_char", not {}'
227+
.format(format_type(et)), ctx.context)
230228
return make_simplified_union(types)
231229
return ctx.default_attr_type

mypy/plugins/dataclasses.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
269269
elif not isinstance(stmt.rvalue, TempNode):
270270
has_default = True
271271

272+
if not has_default:
273+
# Make all non-default attributes implicit because they are de-facto set
274+
# on self in the generated __init__(), not in the class body.
275+
sym.implicit = True
276+
272277
known_attrs.add(lhs.name)
273278
attrs.append(DataclassAttribute(
274279
name=lhs.name,

test-data/unit/check-ctypes.test

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ class MyCInt(ctypes.c_int):
66

77
intarr4 = ctypes.c_int * 4
88
a = intarr4(1, ctypes.c_int(2), MyCInt(3), 4)
9-
intarr4(1, 2, 3, "invalid") # E: Array constructor argument 4 of type "builtins.str" is not convertible to the array element type "ctypes.c_int"
10-
reveal_type(a) # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
9+
intarr4(1, 2, 3, "invalid") # E: Array constructor argument 4 of type "str" is not convertible to the array element type "c_int"
10+
reveal_type(a) # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
1111
reveal_type(a[0]) # N: Revealed type is 'builtins.int'
1212
reveal_type(a[1:3]) # N: Revealed type is 'builtins.list[builtins.int]'
1313
a[0] = 42
@@ -30,11 +30,11 @@ class MyCInt(ctypes.c_int):
3030

3131
myintarr4 = MyCInt * 4
3232
mya = myintarr4(1, 2, MyCInt(3), 4)
33-
myintarr4(1, ctypes.c_int(2), MyCInt(3), "invalid") # E: Array constructor argument 2 of type "ctypes.c_int" is not convertible to the array element type "__main__.MyCInt" \
34-
# E: Array constructor argument 4 of type "builtins.str" is not convertible to the array element type "__main__.MyCInt"
35-
reveal_type(mya) # N: Revealed type is 'ctypes.Array[__main__.MyCInt]'
36-
reveal_type(mya[0]) # N: Revealed type is '__main__.MyCInt'
37-
reveal_type(mya[1:3]) # N: Revealed type is 'builtins.list[__main__.MyCInt]'
33+
myintarr4(1, ctypes.c_int(2), MyCInt(3), "invalid") # E: Array constructor argument 2 of type "c_int" is not convertible to the array element type "MyCInt" \
34+
# E: Array constructor argument 4 of type "str" is not convertible to the array element type "MyCInt"
35+
reveal_type(mya) # N: Revealed type is 'ctypes.Array[__main__.MyCInt*]'
36+
reveal_type(mya[0]) # N: Revealed type is '__main__.MyCInt*'
37+
reveal_type(mya[1:3]) # N: Revealed type is 'builtins.list[__main__.MyCInt*]'
3838
mya[0] = 42
3939
mya[1] = ctypes.c_int(42) # E: No overload variant of "__setitem__" of "Array" matches argument types "int", "c_int" \
4040
# N: Possible overload variants: \
@@ -106,7 +106,7 @@ import ctypes
106106

107107
wca = (ctypes.c_wchar * 4)('a', 'b', 'c', '\x00')
108108
reveal_type(wca.value) # N: Revealed type is 'builtins.str'
109-
wca.raw # E: ctypes.Array attribute "raw" is only available with element type c_char, not "ctypes.c_wchar"
109+
wca.raw # E: Array attribute "raw" is only available with element type "c_char", not "c_wchar"
110110
[builtins fixtures/floatdict.pyi]
111111

112112
[case testCtypesWcharArrayAttrsPy2]
@@ -115,7 +115,7 @@ import ctypes
115115

116116
wca = (ctypes.c_wchar * 4)(u'a', u'b', u'c', u'\x00')
117117
reveal_type(wca.value) # N: Revealed type is 'builtins.unicode'
118-
wca.raw # E: ctypes.Array attribute "raw" is only available with element type c_char, not "ctypes.c_wchar"
118+
wca.raw # E: Array attribute "raw" is only available with element type "c_char", not "c_wchar"
119119
[builtins_py2 fixtures/floatdict_python2.pyi]
120120

121121
[case testCtypesCharUnionArrayAttrs]
@@ -124,7 +124,7 @@ from typing import Union
124124

125125
cua: ctypes.Array[Union[ctypes.c_char, ctypes.c_wchar]]
126126
reveal_type(cua.value) # N: Revealed type is 'Union[builtins.bytes, builtins.str]'
127-
cua.raw # E: ctypes.Array attribute "raw" is only available with element type c_char, not "Union[ctypes.c_char, ctypes.c_wchar]"
127+
cua.raw # E: Array attribute "raw" is only available with element type "c_char", not "Union[c_char, c_wchar]"
128128
[builtins fixtures/floatdict.pyi]
129129

130130
[case testCtypesAnyUnionArrayAttrs]
@@ -141,8 +141,8 @@ import ctypes
141141
from typing import Union
142142

143143
cua: ctypes.Array[Union[ctypes.c_char, ctypes.c_int]]
144-
cua.value # E: ctypes.Array attribute "value" is only available with element type c_char or c_wchar, not "Union[ctypes.c_char, ctypes.c_int]"
145-
cua.raw # E: ctypes.Array attribute "raw" is only available with element type c_char, not "Union[ctypes.c_char, ctypes.c_int]"
144+
cua.value # E: Array attribute "value" is only available with element type "c_char" or "c_wchar", not "Union[c_char, c_int]"
145+
cua.raw # E: Array attribute "raw" is only available with element type "c_char", not "Union[c_char, c_int]"
146146
[builtins fixtures/floatdict.pyi]
147147

148148
[case testCtypesAnyArrayAttrs]
@@ -157,8 +157,8 @@ reveal_type(aa.raw) # N: Revealed type is 'builtins.bytes'
157157
import ctypes
158158

159159
oa = (ctypes.c_int * 4)(1, 2, 3, 4)
160-
oa.value # E: ctypes.Array attribute "value" is only available with element type c_char or c_wchar, not "ctypes.c_int"
161-
oa.raw # E: ctypes.Array attribute "raw" is only available with element type c_char, not "ctypes.c_int"
160+
oa.value # E: Array attribute "value" is only available with element type "c_char" or "c_wchar", not "c_int"
161+
oa.raw # E: Array attribute "raw" is only available with element type "c_char", not "c_int"
162162
[builtins fixtures/floatdict.pyi]
163163

164164
[case testCtypesArrayConstructorStarargs]
@@ -168,13 +168,13 @@ intarr4 = ctypes.c_int * 4
168168
intarr6 = ctypes.c_int * 6
169169
int_values = [1, 2, 3, 4]
170170
c_int_values = [ctypes.c_int(1), ctypes.c_int(2), ctypes.c_int(3), ctypes.c_int(4)]
171-
reveal_type(intarr4(*int_values)) # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
172-
reveal_type(intarr4(*c_int_values)) # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
173-
reveal_type(intarr6(1, ctypes.c_int(2), *int_values)) # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
174-
reveal_type(intarr6(1, ctypes.c_int(2), *c_int_values)) # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
171+
reveal_type(intarr4(*int_values)) # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
172+
reveal_type(intarr4(*c_int_values)) # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
173+
reveal_type(intarr6(1, ctypes.c_int(2), *int_values)) # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
174+
reveal_type(intarr6(1, ctypes.c_int(2), *c_int_values)) # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
175175

176176
float_values = [1.0, 2.0, 3.0, 4.0]
177-
intarr4(*float_values) # E: Array constructor argument 1 of type "builtins.list[builtins.float*]" is not convertible to the array element type "Iterable[ctypes.c_int]"
177+
intarr4(*float_values) # E: Array constructor argument 1 of type "List[float]" is not convertible to the array element type "Iterable[c_int]"
178178
[builtins fixtures/floatdict.pyi]
179179

180180
[case testCtypesArrayConstructorKwargs]

test-data/unit/check-dataclasses.test

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,9 +484,12 @@ class A(Generic[T]):
484484
@classmethod
485485
def foo(cls) -> None:
486486
reveal_type(cls) # N: Revealed type is 'Type[__main__.A[T`1]]'
487-
reveal_type(cls(1)) # N: Revealed type is '__main__.A[builtins.int*]'
488-
reveal_type(cls('wooooo')) # N: Revealed type is '__main__.A[builtins.str*]'
487+
cls.x # E: Access to generic instance variables via class is ambiguous
489488

489+
@classmethod
490+
def other(cls, x: T) -> A[T]: ...
491+
492+
reveal_type(A(0).other) # N: Revealed type is 'def (x: builtins.int*) -> __main__.A[builtins.int*]'
490493
[builtins fixtures/classmethod.pyi]
491494

492495
[case testDataclassesForwardRefs]

0 commit comments

Comments
 (0)