From 95fdb41c87eafa880124bc0edeb0947b47ce8dab Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 25 Jul 2021 21:18:46 +0300 Subject: [PATCH 1/5] Adds attribute type inference from super-types for partial types, refs #10870 --- mypy/checker.py | 14 +++++++++++++ test-data/unit/check-classes.test | 33 +++++++++++++++++++++++++++++++ test-data/unit/fixtures/list.pyi | 3 ++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index ae93b8558add..3c331a26063a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4870,6 +4870,12 @@ def enter_partial_types(self, *, is_function: bool = False, and not permissive): var.type = NoneType() else: + if is_class: + # Special case: possibly super-type defines the type for us? + parent_type = self.get_defined_in_base_class(var) + if parent_type is not None: + var.type = parent_type + self.partial_reported.add(var) if var not in self.partial_reported and not permissive: self.msg.need_annotation_for_var(var, context, self.options.python_version) self.partial_reported.add(var) @@ -4926,6 +4932,14 @@ def is_defined_in_base_class(self, var: Var) -> bool: return True return False + def get_defined_in_base_class(self, var: Var) -> Optional[Type]: + if var.info: + for base in var.info.mro[1:]: + found = base.get(var.name) + if found is not None: + return found.type + return None + def find_partial_types(self, var: Var) -> Optional[Dict[Var, Context]]: """Look for an active partial type scope containing variable. diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index ee58d98d0675..5336ccb00f9d 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -972,6 +972,39 @@ x = C.x [out] main:2: error: Need type annotation for "x" (hint: "x: List[] = ...") +[case testAccessingClassAttributeWithTypeInferenceWithSuperType] +from typing import List +class P: + x: List[int] +class C(P): + x = [] +reveal_type(C.x) # N: Revealed type is "builtins.list[builtins.int]" +[builtins fixtures/list.pyi] + +[case testAccessingClassAttributeWithTypeInferenceWithMixinType] +from typing import List +class P: + pass +class M: + x: List[int] +class C(P, M): + x = [] +reveal_type(C.x) # N: Revealed type is "builtins.list[builtins.int]" +[builtins fixtures/list.pyi] + +[case testClassAttributeWithTypeInferenceInvalidType] +from typing import List +class P: + x: List[int] +class C(P): + x = ['a'] # E: List item 0 has incompatible type "str"; expected "int" +[builtins fixtures/list.pyi] + +[case testClassSlotsAttributeWithTypeInference] +class P: + __slots__ = [] +[builtins fixtures/list.pyi] + [case testAccessingGenericClassAttribute] from typing import Generic, TypeVar T = TypeVar('T') diff --git a/test-data/unit/fixtures/list.pyi b/test-data/unit/fixtures/list.pyi index c4baf89ffc13..13996c570daf 100644 --- a/test-data/unit/fixtures/list.pyi +++ b/test-data/unit/fixtures/list.pyi @@ -1,10 +1,11 @@ # Builtins stub used in list-related test cases. -from typing import TypeVar, Generic, Iterable, Iterator, Sequence, overload +from typing import TypeVar, Generic, Iterable, Iterator, Union, Sequence, overload T = TypeVar('T') class object: + __slots__: Union['str', Iterable['str']] def __init__(self) -> None: pass class type: pass From 03dedfecc0ca082b0f042409e269583f7615e131 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 25 Jul 2021 21:55:49 +0300 Subject: [PATCH 2/5] `__slots__` in `list.pyi` caused several failures --- test-data/unit/check-classes.test | 5 ----- test-data/unit/fixtures/list.pyi | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 5336ccb00f9d..c26385c7d7c2 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1000,11 +1000,6 @@ class C(P): x = ['a'] # E: List item 0 has incompatible type "str"; expected "int" [builtins fixtures/list.pyi] -[case testClassSlotsAttributeWithTypeInference] -class P: - __slots__ = [] -[builtins fixtures/list.pyi] - [case testAccessingGenericClassAttribute] from typing import Generic, TypeVar T = TypeVar('T') diff --git a/test-data/unit/fixtures/list.pyi b/test-data/unit/fixtures/list.pyi index 13996c570daf..c4baf89ffc13 100644 --- a/test-data/unit/fixtures/list.pyi +++ b/test-data/unit/fixtures/list.pyi @@ -1,11 +1,10 @@ # Builtins stub used in list-related test cases. -from typing import TypeVar, Generic, Iterable, Iterator, Union, Sequence, overload +from typing import TypeVar, Generic, Iterable, Iterator, Sequence, overload T = TypeVar('T') class object: - __slots__: Union['str', Iterable['str']] def __init__(self) -> None: pass class type: pass From c3ca807c88719e499fc8bb464187f5a1ec8bd331 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 26 Jul 2021 12:59:59 +0300 Subject: [PATCH 3/5] New approach --- mypy/checker.py | 41 ++++++++++++++++++++++++++----- test-data/unit/check-classes.test | 18 ++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 3c331a26063a..eb4e86c8c3c5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2091,6 +2091,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type infer_lvalue_type) else: self.try_infer_partial_generic_type_from_assignment(lvalue, rvalue, '=') + self.try_infer_partial_generic_type_from_super(lvalue, rvalue) lvalue_type, index_lvalue, inferred = self.check_lvalue(lvalue) # If we're assigning to __getattr__ or similar methods, check that the signature is # valid. @@ -2241,6 +2242,40 @@ def try_infer_partial_generic_type_from_assignment(self, var.type = fill_typevars_with_any(typ.type) del partial_types[var] + def try_infer_partial_generic_type_from_super(self, lvalue: Lvalue, + rvalue: Expression) -> None: + """Try to infer a precise type for partial generic type from super types. + + Example where this happens: + + class P: + x: List[int] + + class C(P): + x = [] # Infer List[int] as type of 'x' + + """ + var = None + if (isinstance(lvalue, NameExpr) + and isinstance(lvalue.node, Var) + and lvalue.node.type is None): + var = lvalue.node + self.infer_partial_type(var, lvalue, + self.expr_checker.accept(rvalue)) + + if var is not None: + partial_types = self.find_partial_types(var) + if partial_types is None: + return + + parent_type = self.get_defined_in_base_class(var) + if parent_type is not None and is_valid_inferred_type(parent_type): + self.set_inferred_type(var, lvalue, parent_type) + if isinstance(lvalue, RefExpr): + # We need this to escape another round of inference: + lvalue.is_inferred_def = False + del partial_types[var] + def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[Type], rvalue: Expression) -> bool: lvalue_node = lvalue.node @@ -4870,12 +4905,6 @@ def enter_partial_types(self, *, is_function: bool = False, and not permissive): var.type = NoneType() else: - if is_class: - # Special case: possibly super-type defines the type for us? - parent_type = self.get_defined_in_base_class(var) - if parent_type is not None: - var.type = parent_type - self.partial_reported.add(var) if var not in self.partial_reported and not permissive: self.msg.need_annotation_for_var(var, context, self.options.python_version) self.partial_reported.add(var) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index c26385c7d7c2..d60054d0cb68 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -992,6 +992,24 @@ class C(P, M): reveal_type(C.x) # N: Revealed type is "builtins.list[builtins.int]" [builtins fixtures/list.pyi] +[case testAccessingClassAttributeWithTypeInferenceWithMixinTypeConflict] +from typing import List +class P: + x: List[int] +class M: + x: List[str] +class C(P, M): + x = [] # E: Incompatible types in assignment (expression has type "List[int]", base class "M" defined the type as "List[str]") +reveal_type(C.x) # N: Revealed type is "builtins.list[builtins.int]" +[builtins fixtures/list.pyi] + +[case testClassAttributeWithTypeInferenceNoParentType] +class P: + ... +class C(P): + x = [] # E: Need type annotation for "x" (hint: "x: List[] = ...") +[builtins fixtures/list.pyi] + [case testClassAttributeWithTypeInferenceInvalidType] from typing import List class P: From 9331a9f94d4a60f26ff2376bf848b4452b1824ba Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 26 Jul 2021 13:15:02 +0300 Subject: [PATCH 4/5] Proper types --- mypy/checker.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index eb4e86c8c3c5..ae323dff63f1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2261,7 +2261,7 @@ class C(P): and lvalue.node.type is None): var = lvalue.node self.infer_partial_type(var, lvalue, - self.expr_checker.accept(rvalue)) + get_proper_type(self.expr_checker.accept(rvalue))) if var is not None: partial_types = self.find_partial_types(var) @@ -2269,12 +2269,14 @@ class C(P): return parent_type = self.get_defined_in_base_class(var) - if parent_type is not None and is_valid_inferred_type(parent_type): - self.set_inferred_type(var, lvalue, parent_type) - if isinstance(lvalue, RefExpr): - # We need this to escape another round of inference: - lvalue.is_inferred_def = False - del partial_types[var] + if parent_type is not None: + parent_type = get_proper_type(parent_type) + if is_valid_inferred_type(parent_type): + self.set_inferred_type(var, lvalue, parent_type) + if isinstance(lvalue, RefExpr): + # We need this to escape another round of inference: + lvalue.is_inferred_def = False + del partial_types[var] def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[Type], rvalue: Expression) -> bool: From 2af538865ccb6d1e3cc8e76604f2a278b433df9a Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 26 Jul 2021 15:04:22 +0300 Subject: [PATCH 5/5] Reverts c3ca807c88719e499fc8bb464187f5a1ec8bd331 --- mypy/checker.py | 43 +++++-------------------------- test-data/unit/check-classes.test | 21 ++++++++------- 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index ae323dff63f1..3c331a26063a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2091,7 +2091,6 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type infer_lvalue_type) else: self.try_infer_partial_generic_type_from_assignment(lvalue, rvalue, '=') - self.try_infer_partial_generic_type_from_super(lvalue, rvalue) lvalue_type, index_lvalue, inferred = self.check_lvalue(lvalue) # If we're assigning to __getattr__ or similar methods, check that the signature is # valid. @@ -2242,42 +2241,6 @@ def try_infer_partial_generic_type_from_assignment(self, var.type = fill_typevars_with_any(typ.type) del partial_types[var] - def try_infer_partial_generic_type_from_super(self, lvalue: Lvalue, - rvalue: Expression) -> None: - """Try to infer a precise type for partial generic type from super types. - - Example where this happens: - - class P: - x: List[int] - - class C(P): - x = [] # Infer List[int] as type of 'x' - - """ - var = None - if (isinstance(lvalue, NameExpr) - and isinstance(lvalue.node, Var) - and lvalue.node.type is None): - var = lvalue.node - self.infer_partial_type(var, lvalue, - get_proper_type(self.expr_checker.accept(rvalue))) - - if var is not None: - partial_types = self.find_partial_types(var) - if partial_types is None: - return - - parent_type = self.get_defined_in_base_class(var) - if parent_type is not None: - parent_type = get_proper_type(parent_type) - if is_valid_inferred_type(parent_type): - self.set_inferred_type(var, lvalue, parent_type) - if isinstance(lvalue, RefExpr): - # We need this to escape another round of inference: - lvalue.is_inferred_def = False - del partial_types[var] - def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[Type], rvalue: Expression) -> bool: lvalue_node = lvalue.node @@ -4907,6 +4870,12 @@ def enter_partial_types(self, *, is_function: bool = False, and not permissive): var.type = NoneType() else: + if is_class: + # Special case: possibly super-type defines the type for us? + parent_type = self.get_defined_in_base_class(var) + if parent_type is not None: + var.type = parent_type + self.partial_reported.add(var) if var not in self.partial_reported and not permissive: self.msg.need_annotation_for_var(var, context, self.options.python_version) self.partial_reported.add(var) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index d60054d0cb68..6e4ab5fc36ff 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -992,14 +992,23 @@ class C(P, M): reveal_type(C.x) # N: Revealed type is "builtins.list[builtins.int]" [builtins fixtures/list.pyi] -[case testAccessingClassAttributeWithTypeInferenceWithMixinTypeConflict] +[case testClassAttributeWithTypeInferenceInvalidType] +from typing import List +class P: + x: List[int] +class C(P): + x = ['a'] # E: List item 0 has incompatible type "str"; expected "int" +[builtins fixtures/list.pyi] + +case testAccessingClassAttributeWithTypeInferenceWithMixinTypeConflict] from typing import List class P: x: List[int] class M: x: List[str] class C(P, M): - x = [] # E: Incompatible types in assignment (expression has type "List[int]", base class "M" defined the type as "List[str]") + x = [] # This hides subclassing issue, but that's how it is for now + reveal_type(C.x) # N: Revealed type is "builtins.list[builtins.int]" [builtins fixtures/list.pyi] @@ -1010,14 +1019,6 @@ class C(P): x = [] # E: Need type annotation for "x" (hint: "x: List[] = ...") [builtins fixtures/list.pyi] -[case testClassAttributeWithTypeInferenceInvalidType] -from typing import List -class P: - x: List[int] -class C(P): - x = ['a'] # E: List item 0 has incompatible type "str"; expected "int" -[builtins fixtures/list.pyi] - [case testAccessingGenericClassAttribute] from typing import Generic, TypeVar T = TypeVar('T')