From 1b41df3246d798be2ef95310303b0ebdb95f93f3 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Mon, 3 Dec 2018 17:19:50 +0300 Subject: [PATCH 1/6] better error message for nested classes multiple inheritance --- mypy/checker.py | 32 ++- .../unit/check-multiple-inheritance.test | 238 ++++++++++++++++++ 2 files changed, 260 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f2af5d7a7a7e..d430dd876d25 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1605,6 +1605,16 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None: if name in base2.names and base2 not in base.mro: self.check_compatibility(name, base, base2, typ) + def determine_type_of_class_member(self, sym: SymbolTableNode) -> Optional[Type]: + if sym.type is not None: + return sym.type + if isinstance(sym.node, FuncBase): + return self.function_type(sym.node) + if isinstance(sym.node, TypeInfo): + # nested class + return type_object_type(sym.node, self.named_type) + return None + def check_compatibility(self, name: str, base1: TypeInfo, base2: TypeInfo, ctx: Context) -> None: """Check if attribute name in base1 is compatible with base2 in multiple inheritance. @@ -1618,19 +1628,21 @@ def check_compatibility(self, name: str, base1: TypeInfo, return first = base1[name] second = base2[name] - first_type = first.type - if first_type is None and isinstance(first.node, FuncBase): - first_type = self.function_type(first.node) - second_type = second.type - if second_type is None and isinstance(second.node, FuncBase): - second_type = self.function_type(second.node) + first_type = self.determine_type_of_class_member(first) + second_type = self.determine_type_of_class_member(second) + # TODO: What if some classes are generic? if (isinstance(first_type, FunctionLike) and isinstance(second_type, FunctionLike)): - # Method override - first_sig = bind_self(first_type) - second_sig = bind_self(second_type) - ok = is_subtype(first_sig, second_sig, ignore_pos_arg_names=True) + if first_type.is_type_obj() and second_type.is_type_obj(): + # For class objects only check the subtype relationship of the classes, + # since we allow incompatible overrides of '__init__'/'__new__' + ok = is_subtype(left=fill_typevars(first_type.type_object()), + right=fill_typevars(second_type.type_object())) + else: + first_sig = bind_self(first_type) + second_sig = bind_self(second_type) + ok = is_subtype(first_sig, second_sig, ignore_pos_arg_names=True) elif first_type and second_type: ok = is_equivalent(first_type, second_type) else: diff --git a/test-data/unit/check-multiple-inheritance.test b/test-data/unit/check-multiple-inheritance.test index 678ccad2ab5c..b38e15be2304 100644 --- a/test-data/unit/check-multiple-inheritance.test +++ b/test-data/unit/check-multiple-inheritance.test @@ -240,3 +240,241 @@ def dec(f: Callable[..., T]) -> Callable[..., T]: [out] main:3: error: Cannot determine type of 'f' in base class 'B' main:3: error: Cannot determine type of 'f' in base class 'C' + +[case testMultipleInheritance_NestedClassesWithSameName] +class Mixin1: + class Meta: + pass +class Mixin2: + class Meta: + pass +class A(Mixin1, Mixin2): + pass +[out] +main:7: error: Definition of "Meta" in base class "Mixin1" is incompatible with definition in base class "Mixin2" + +[case testMultipleInheritance_NestedClassesWithSameNameCustomMetaclass] +class Metaclass(type): + pass +class Mixin1: + class Meta(metaclass=Metaclass): + pass +class Mixin2: + class Meta(metaclass=Metaclass): + pass +class A(Mixin1, Mixin2): + pass +[out] +main:9: error: Definition of "Meta" in base class "Mixin1" is incompatible with definition in base class "Mixin2" + +[case testMultipleInheritance_NestedClassesWithSameNameOverloadedNew] +from mixins import Mixin1, Mixin2 +class A(Mixin1, Mixin2): + pass +[file mixins.py] +class Mixin1: + class Meta: + pass +class Mixin2: + class Meta: + pass +[file mixins.pyi] +from typing import overload, Any, Mapping, Dict +class Mixin1: + class Meta: + @overload + def __new__(cls, *args, **kwargs: None) -> Mixin1.Meta: + pass + @overload + def __new__(cls, *args, **kwargs: Dict[str, Any]) -> Mixin1.Meta: + pass +class Mixin2: + class Meta: + pass +[builtins fixtures/dict.pyi] +[out] +main:2: error: Definition of "Meta" in base class "Mixin1" is incompatible with definition in base class "Mixin2" + +[case testMultipleInheritance_NestedClassAndAttrHaveSameName] +class Mixin1: + class Nested1: + pass +class Mixin2: + Nested1: str +class A(Mixin1, Mixin2): + pass +[out] +main:6: error: Definition of "Nested1" in base class "Mixin1" is incompatible with definition in base class "Mixin2" + +[case testMultipleInheritance_NestedClassAndFunctionHaveSameName] +class Mixin1: + class Nested1: + pass +class Mixin2: + def Nested1(self) -> str: + pass +class A(Mixin1, Mixin2): + pass +[out] +main:7: error: Definition of "Nested1" in base class "Mixin1" is incompatible with definition in base class "Mixin2" + +[case testMultipleInheritance_NestedClassAndRefToOtherClass] +class Outer: + pass +class Mixin1: + class Nested1: + pass +class Mixin2: + Nested1 = Outer +class A(Mixin2, Mixin1): + pass +[out] +main:8: error: Definition of "Nested1" in base class "Mixin2" is incompatible with definition in base class "Mixin1" + +[case testMultipleInheritance_ReferenceToSubclassesFromSameMRO] +class A: + def __init__(self, arg: str) -> None: + pass +class B(A): + pass +class Base1: + NestedVar = A +class Base2: + NestedVar = B +class Combo(Base2, Base1): ... +[out] + +[case testMultipleInheritance_ReferenceToSubclassesFromSameMROCustomMetaclass] +class Metaclass(type): + pass +class A(metaclass=Metaclass): + pass +class B(A): + pass +class Base1: + NestedVar = A +class Base2: + NestedVar = B +class Combo(Base2, Base1): ... +[out] + +[case testMultipleInheritance_ReferenceToSubclassesFromSameMROOverloadedNew] +from mixins import A, B +class Base1: + NestedVar = A +class Base2: + NestedVar = B +class Combo(Base2, Base1): ... +[file mixins.py] +class A: + pass +class B(A): + pass +[file mixins.pyi] +from typing import overload, Dict, Any +class A: + @overload + def __new__(cls, *args, **kwargs: None) -> A: + pass + @overload + def __new__(cls, *args, **kwargs: Dict[str, Any]) -> A: + pass +class B: + pass +[builtins fixtures/dict.pyi] +[out] +main:6: error: Definition of "NestedVar" in base class "Base2" is incompatible with definition in base class "Base1" + +[case testMultipleInheritance_ReferenceToGenericClasses] +from typing import TypeVar, Generic +T = TypeVar('T') +class Generic1(Generic[T]): + pass +class Generic2(Generic[T]): + pass +class Base1: + Nested = Generic1 +class Base2: + Nested = Generic2 +class A(Base1, Base2): + pass +[out] +main:11: error: Definition of "Nested" in base class "Base1" is incompatible with definition in base class "Base2" + +[case testMultipleInheritance_RefersToNamedTuples] +from typing import NamedTuple +class NamedTuple1: + attr1: int +class NamedTuple2: + attr2: int +class Base1: + Nested = NamedTuple1 +class Base2: + Nested = NamedTuple2 +class A(Base1, Base2): + pass +[out] +main:10: error: Definition of "Nested" in base class "Base1" is incompatible with definition in base class "Base2" + +[case testMultipleInheritance_NestedVariableRefersToSuperlassUnderSubclass] +class A: + def __init__(self, arg: str) -> None: + pass +class B(A): + pass +class Base1: + NestedVar = B +class Base2: + NestedVar = A +class Combo(Base2, Base1): ... +[out] +main:10: error: Definition of "NestedVar" in base class "Base2" is incompatible with definition in base class "Base1" + +[case testNestedVariableRefersToSubclassOfAnotherNestedClass] +class Mixin1: + class Meta: + pass +class Outer(Mixin1.Meta): + pass +class Mixin2: + NestedVar = Outer +class Combo(Mixin2, Mixin1): ... +[out] + +[case testNestedVariableRefersToCompletelyDifferentClasses] +class A: + pass +class B: + pass +class Base1: + NestedVar = A +class Base2: + NestedVar = B +class Combo(Base2, Base1): ... +[out] +main:9: error: Definition of "NestedVar" in base class "Base2" is incompatible with definition in base class "Base1" + +[case testDoNotFailIfBothNestedClassesInheritFromAny] +from typing import Any +class Mixin1: + class Meta(Any): + pass +class Mixin2: + class Meta(Any): + pass +class A(Mixin1, Mixin2): + pass +[out] + +[case testDoNotFailIfOneOfNestedClassesIsOuterInheritedFromAny] +from typing import Any +class Outer(Any): + pass +class Mixin1: + Meta = Outer +class Mixin2: + class Meta(Any): + pass +class A(Mixin1, Mixin2): + pass +[out] From d8f4b269eca9b9e1eb4b8138e0292c8f14089ddc Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Mon, 24 Dec 2018 15:11:20 +0300 Subject: [PATCH 2/6] address latest comments --- mypy/checker.py | 6 ++-- mypy/typevars.py | 9 +++++- .../unit/check-multiple-inheritance.test | 31 +++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e0b71bb9640d..2f39c3a22caa 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -49,7 +49,7 @@ ) from mypy.constraints import SUPERTYPE_OF from mypy.maptype import map_instance_to_supertype -from mypy.typevars import fill_typevars, has_no_typevars +from mypy.typevars import fill_typevars, has_no_typevars, fill_typevars_with_any from mypy.semanal import set_callable_name, refers_to_fullname, calculate_mro from mypy.erasetype import erase_typevars from mypy.expandtype import expand_type, expand_type_by_instance @@ -1637,8 +1637,8 @@ def check_compatibility(self, name: str, base1: TypeInfo, if first_type.is_type_obj() and second_type.is_type_obj(): # For class objects only check the subtype relationship of the classes, # since we allow incompatible overrides of '__init__'/'__new__' - ok = is_subtype(left=fill_typevars(first_type.type_object()), - right=fill_typevars(second_type.type_object())) + ok = is_subtype(left=fill_typevars_with_any(first_type.type_object()), + right=fill_typevars_with_any(second_type.type_object())) else: first_sig = bind_self(first_type) second_sig = bind_self(second_type) diff --git a/mypy/typevars.py b/mypy/typevars.py index 4e53a95b6877..00df57a86b2d 100644 --- a/mypy/typevars.py +++ b/mypy/typevars.py @@ -3,7 +3,7 @@ from mypy.nodes import TypeInfo from mypy.erasetype import erase_typevars -from mypy.types import Instance, TypeVarType, TupleType, Type +from mypy.types import Instance, TypeVarType, TupleType, Type, TypeOfAny, AnyType def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]: @@ -20,6 +20,13 @@ def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]: return typ.tuple_type.copy_modified(fallback=inst) +def fill_typevars_with_any(typ: TypeInfo) -> Union[Instance, TupleType]: + inst = Instance(typ, [AnyType(TypeOfAny.special_form)] * len(typ.defn.type_vars)) + if typ.tuple_type is None: + return inst + return typ.tuple_type.copy_modified(fallback=inst) + + def has_no_typevars(typ: Type) -> bool: # We test if a type contains type variables by erasing all type variables # and comparing the result to the original type. We use comparison by equality that diff --git a/test-data/unit/check-multiple-inheritance.test b/test-data/unit/check-multiple-inheritance.test index b38e15be2304..e6e46ea81377 100644 --- a/test-data/unit/check-multiple-inheritance.test +++ b/test-data/unit/check-multiple-inheritance.test @@ -401,6 +401,37 @@ class A(Base1, Base2): [out] main:11: error: Definition of "Nested" in base class "Base1" is incompatible with definition in base class "Base2" +[case testMultipleInheritance_GenericSubclasses_SuperclassFirst] +from typing import TypeVar, Generic +T = TypeVar('T') +class ParentGeneric(Generic[T]): + pass +class ChildGeneric(ParentGeneric, Generic[T]): + pass +class Base1: + Nested = ParentGeneric +class Base2: + Nested = ChildGeneric +class A(Base1, Base2): + pass +[out] +main:11: error: Definition of "Nested" in base class "Base1" is incompatible with definition in base class "Base2" + +[case testMultipleInheritance_GenericSubclasses_SubclassFirst] +from typing import TypeVar, Generic +T = TypeVar('T') +class ParentGeneric(Generic[T]): + pass +class ChildGeneric(ParentGeneric, Generic[T]): + pass +class Base1: + Nested = ParentGeneric +class Base2: + Nested = ChildGeneric +class A(Base2, Base1): + pass +[out] + [case testMultipleInheritance_RefersToNamedTuples] from typing import NamedTuple class NamedTuple1: From b38a5cf87ff9c5413358be256483f9324a0184a7 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Wed, 9 Jan 2019 03:08:18 +0300 Subject: [PATCH 3/6] fix removed import --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index d9d7ebf992a5..7a9fb7e40075 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -49,7 +49,7 @@ ) from mypy.constraints import SUPERTYPE_OF from mypy.maptype import map_instance_to_supertype -from mypy.typevars import fill_typevars, has_no_typevars +from mypy.typevars import fill_typevars, has_no_typevars, fill_typevars_with_any from mypy.semanal import set_callable_name, refers_to_fullname from mypy.mro import calculate_mro from mypy.erasetype import erase_typevars From 860665cd13ca48eea527a29857d17dd354558ca5 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Wed, 9 Jan 2019 03:17:38 +0300 Subject: [PATCH 4/6] add docstring to new helper function --- mypy/typevars.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/typevars.py b/mypy/typevars.py index 00df57a86b2d..862be4587794 100644 --- a/mypy/typevars.py +++ b/mypy/typevars.py @@ -21,6 +21,8 @@ def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]: def fill_typevars_with_any(typ: TypeInfo) -> Union[Instance, TupleType]: + """ Add correct number of Any's as typevars to a type. + """ inst = Instance(typ, [AnyType(TypeOfAny.special_form)] * len(typ.defn.type_vars)) if typ.tuple_type is None: return inst From 37af755a654ddae99e113075ff0dbb55b6e1716c Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Wed, 9 Jan 2019 03:34:59 +0300 Subject: [PATCH 5/6] address comments --- test-data/unit/check-multiple-inheritance.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-multiple-inheritance.test b/test-data/unit/check-multiple-inheritance.test index e6e46ea81377..1bdc70c19fa1 100644 --- a/test-data/unit/check-multiple-inheritance.test +++ b/test-data/unit/check-multiple-inheritance.test @@ -406,7 +406,7 @@ from typing import TypeVar, Generic T = TypeVar('T') class ParentGeneric(Generic[T]): pass -class ChildGeneric(ParentGeneric, Generic[T]): +class ChildGeneric(ParentGeneric[T]): pass class Base1: Nested = ParentGeneric @@ -422,7 +422,7 @@ from typing import TypeVar, Generic T = TypeVar('T') class ParentGeneric(Generic[T]): pass -class ChildGeneric(ParentGeneric, Generic[T]): +class ChildGeneric(ParentGeneric[T]): pass class Base1: Nested = ParentGeneric From 0a1128749b34ee1ca00b4f723544dd7b7a53e098 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 9 Jan 2019 15:02:37 +0000 Subject: [PATCH 6/6] Fine-tune the docstring --- mypy/typevars.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/typevars.py b/mypy/typevars.py index 862be4587794..229d49ac99b8 100644 --- a/mypy/typevars.py +++ b/mypy/typevars.py @@ -21,8 +21,7 @@ def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]: def fill_typevars_with_any(typ: TypeInfo) -> Union[Instance, TupleType]: - """ Add correct number of Any's as typevars to a type. - """ + """ Apply a correct number of Any's as type arguments to a type.""" inst = Instance(typ, [AnyType(TypeOfAny.special_form)] * len(typ.defn.type_vars)) if typ.tuple_type is None: return inst