Skip to content

Make error messages from multiple inheritance compatibility check more accurate #5926

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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_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)
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:
Expand Down
10 changes: 9 additions & 1 deletion mypy/typevars.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -20,6 +20,14 @@ 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]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add a docstring here?

""" 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
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
Expand Down
269 changes: 269 additions & 0 deletions test-data/unit/check-multiple-inheritance.test
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,272 @@ 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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is irrelevant for this PR (IIUC you are just testing that the overloaded constructor actually works), but this signature looks suspicious. If you have def func(**kwds: int): ..., then this call is valid func(x=1, y=2). From your signature it looks like every keyword arguments to the constructor are either all None, or all of them are dictionaries: Mixin1(x=None, y=None), or Mixin1(x={'a': 42, 'b': None}).

No need to fix this, just wanted to avoid confusions.

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_GenericSubclasses_SuperclassFirst]
from typing import TypeVar, Generic
T = TypeVar('T')
class ParentGeneric(Generic[T]):
pass
class ChildGeneric(ParentGeneric[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[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:
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]