Skip to content

Commit 8179952

Browse files
mkurnikovilevkivskyi
authored andcommitted
Make error messages from multiple inheritance compatibility check more accurate (#5926)
Fixes #2871. Initially, in discussion with @ilevkivskyi in Gitter, he suggested to just remove error, if there are two nested classes in a multiple inheritance with the same name ``` class Mixin1: class Meta: pass class Mixin2: class Meta: pass class A(Mixin1, Mixin2): pass ``` However, later we decided to make it safe and emit a better error message, including for cases with nested class and non-class for obvious cases. Note that for class objects we ignore the `__init__` method signature of nested class and only check subclassing relationship between them.
1 parent 67f18be commit 8179952

File tree

3 files changed

+301
-12
lines changed

3 files changed

+301
-12
lines changed

mypy/checker.py

+23-11
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
)
5050
from mypy.constraints import SUPERTYPE_OF
5151
from mypy.maptype import map_instance_to_supertype
52-
from mypy.typevars import fill_typevars, has_no_typevars
52+
from mypy.typevars import fill_typevars, has_no_typevars, fill_typevars_with_any
5353
from mypy.semanal import set_callable_name, refers_to_fullname
5454
from mypy.mro import calculate_mro
5555
from mypy.erasetype import erase_typevars
@@ -1605,6 +1605,16 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None:
16051605
if name in base2.names and base2 not in base.mro:
16061606
self.check_compatibility(name, base, base2, typ)
16071607

1608+
def determine_type_of_class_member(self, sym: SymbolTableNode) -> Optional[Type]:
1609+
if sym.type is not None:
1610+
return sym.type
1611+
if isinstance(sym.node, FuncBase):
1612+
return self.function_type(sym.node)
1613+
if isinstance(sym.node, TypeInfo):
1614+
# nested class
1615+
return type_object_type(sym.node, self.named_type)
1616+
return None
1617+
16081618
def check_compatibility(self, name: str, base1: TypeInfo,
16091619
base2: TypeInfo, ctx: Context) -> None:
16101620
"""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,
16181628
return
16191629
first = base1[name]
16201630
second = base2[name]
1621-
first_type = first.type
1622-
if first_type is None and isinstance(first.node, FuncBase):
1623-
first_type = self.function_type(first.node)
1624-
second_type = second.type
1625-
if second_type is None and isinstance(second.node, FuncBase):
1626-
second_type = self.function_type(second.node)
1631+
first_type = self.determine_type_of_class_member(first)
1632+
second_type = self.determine_type_of_class_member(second)
1633+
16271634
# TODO: What if some classes are generic?
16281635
if (isinstance(first_type, FunctionLike) and
16291636
isinstance(second_type, FunctionLike)):
1630-
# Method override
1631-
first_sig = bind_self(first_type)
1632-
second_sig = bind_self(second_type)
1633-
ok = is_subtype(first_sig, second_sig, ignore_pos_arg_names=True)
1637+
if first_type.is_type_obj() and second_type.is_type_obj():
1638+
# For class objects only check the subtype relationship of the classes,
1639+
# since we allow incompatible overrides of '__init__'/'__new__'
1640+
ok = is_subtype(left=fill_typevars_with_any(first_type.type_object()),
1641+
right=fill_typevars_with_any(second_type.type_object()))
1642+
else:
1643+
first_sig = bind_self(first_type)
1644+
second_sig = bind_self(second_type)
1645+
ok = is_subtype(first_sig, second_sig, ignore_pos_arg_names=True)
16341646
elif first_type and second_type:
16351647
ok = is_equivalent(first_type, second_type)
16361648
else:

mypy/typevars.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from mypy.nodes import TypeInfo
44

55
from mypy.erasetype import erase_typevars
6-
from mypy.types import Instance, TypeVarType, TupleType, Type
6+
from mypy.types import Instance, TypeVarType, TupleType, Type, TypeOfAny, AnyType
77

88

99
def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]:
@@ -20,6 +20,14 @@ def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]:
2020
return typ.tuple_type.copy_modified(fallback=inst)
2121

2222

23+
def fill_typevars_with_any(typ: TypeInfo) -> Union[Instance, TupleType]:
24+
""" Apply a correct number of Any's as type arguments to a type."""
25+
inst = Instance(typ, [AnyType(TypeOfAny.special_form)] * len(typ.defn.type_vars))
26+
if typ.tuple_type is None:
27+
return inst
28+
return typ.tuple_type.copy_modified(fallback=inst)
29+
30+
2331
def has_no_typevars(typ: Type) -> bool:
2432
# We test if a type contains type variables by erasing all type variables
2533
# and comparing the result to the original type. We use comparison by equality that

test-data/unit/check-multiple-inheritance.test

+269
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,272 @@ def dec(f: Callable[..., T]) -> Callable[..., T]:
240240
[out]
241241
main:3: error: Cannot determine type of 'f' in base class 'B'
242242
main:3: error: Cannot determine type of 'f' in base class 'C'
243+
244+
[case testMultipleInheritance_NestedClassesWithSameName]
245+
class Mixin1:
246+
class Meta:
247+
pass
248+
class Mixin2:
249+
class Meta:
250+
pass
251+
class A(Mixin1, Mixin2):
252+
pass
253+
[out]
254+
main:7: error: Definition of "Meta" in base class "Mixin1" is incompatible with definition in base class "Mixin2"
255+
256+
[case testMultipleInheritance_NestedClassesWithSameNameCustomMetaclass]
257+
class Metaclass(type):
258+
pass
259+
class Mixin1:
260+
class Meta(metaclass=Metaclass):
261+
pass
262+
class Mixin2:
263+
class Meta(metaclass=Metaclass):
264+
pass
265+
class A(Mixin1, Mixin2):
266+
pass
267+
[out]
268+
main:9: error: Definition of "Meta" in base class "Mixin1" is incompatible with definition in base class "Mixin2"
269+
270+
[case testMultipleInheritance_NestedClassesWithSameNameOverloadedNew]
271+
from mixins import Mixin1, Mixin2
272+
class A(Mixin1, Mixin2):
273+
pass
274+
[file mixins.py]
275+
class Mixin1:
276+
class Meta:
277+
pass
278+
class Mixin2:
279+
class Meta:
280+
pass
281+
[file mixins.pyi]
282+
from typing import overload, Any, Mapping, Dict
283+
class Mixin1:
284+
class Meta:
285+
@overload
286+
def __new__(cls, *args, **kwargs: None) -> Mixin1.Meta:
287+
pass
288+
@overload
289+
def __new__(cls, *args, **kwargs: Dict[str, Any]) -> Mixin1.Meta:
290+
pass
291+
class Mixin2:
292+
class Meta:
293+
pass
294+
[builtins fixtures/dict.pyi]
295+
[out]
296+
main:2: error: Definition of "Meta" in base class "Mixin1" is incompatible with definition in base class "Mixin2"
297+
298+
[case testMultipleInheritance_NestedClassAndAttrHaveSameName]
299+
class Mixin1:
300+
class Nested1:
301+
pass
302+
class Mixin2:
303+
Nested1: str
304+
class A(Mixin1, Mixin2):
305+
pass
306+
[out]
307+
main:6: error: Definition of "Nested1" in base class "Mixin1" is incompatible with definition in base class "Mixin2"
308+
309+
[case testMultipleInheritance_NestedClassAndFunctionHaveSameName]
310+
class Mixin1:
311+
class Nested1:
312+
pass
313+
class Mixin2:
314+
def Nested1(self) -> str:
315+
pass
316+
class A(Mixin1, Mixin2):
317+
pass
318+
[out]
319+
main:7: error: Definition of "Nested1" in base class "Mixin1" is incompatible with definition in base class "Mixin2"
320+
321+
[case testMultipleInheritance_NestedClassAndRefToOtherClass]
322+
class Outer:
323+
pass
324+
class Mixin1:
325+
class Nested1:
326+
pass
327+
class Mixin2:
328+
Nested1 = Outer
329+
class A(Mixin2, Mixin1):
330+
pass
331+
[out]
332+
main:8: error: Definition of "Nested1" in base class "Mixin2" is incompatible with definition in base class "Mixin1"
333+
334+
[case testMultipleInheritance_ReferenceToSubclassesFromSameMRO]
335+
class A:
336+
def __init__(self, arg: str) -> None:
337+
pass
338+
class B(A):
339+
pass
340+
class Base1:
341+
NestedVar = A
342+
class Base2:
343+
NestedVar = B
344+
class Combo(Base2, Base1): ...
345+
[out]
346+
347+
[case testMultipleInheritance_ReferenceToSubclassesFromSameMROCustomMetaclass]
348+
class Metaclass(type):
349+
pass
350+
class A(metaclass=Metaclass):
351+
pass
352+
class B(A):
353+
pass
354+
class Base1:
355+
NestedVar = A
356+
class Base2:
357+
NestedVar = B
358+
class Combo(Base2, Base1): ...
359+
[out]
360+
361+
[case testMultipleInheritance_ReferenceToSubclassesFromSameMROOverloadedNew]
362+
from mixins import A, B
363+
class Base1:
364+
NestedVar = A
365+
class Base2:
366+
NestedVar = B
367+
class Combo(Base2, Base1): ...
368+
[file mixins.py]
369+
class A:
370+
pass
371+
class B(A):
372+
pass
373+
[file mixins.pyi]
374+
from typing import overload, Dict, Any
375+
class A:
376+
@overload
377+
def __new__(cls, *args, **kwargs: None) -> A:
378+
pass
379+
@overload
380+
def __new__(cls, *args, **kwargs: Dict[str, Any]) -> A:
381+
pass
382+
class B:
383+
pass
384+
[builtins fixtures/dict.pyi]
385+
[out]
386+
main:6: error: Definition of "NestedVar" in base class "Base2" is incompatible with definition in base class "Base1"
387+
388+
[case testMultipleInheritance_ReferenceToGenericClasses]
389+
from typing import TypeVar, Generic
390+
T = TypeVar('T')
391+
class Generic1(Generic[T]):
392+
pass
393+
class Generic2(Generic[T]):
394+
pass
395+
class Base1:
396+
Nested = Generic1
397+
class Base2:
398+
Nested = Generic2
399+
class A(Base1, Base2):
400+
pass
401+
[out]
402+
main:11: error: Definition of "Nested" in base class "Base1" is incompatible with definition in base class "Base2"
403+
404+
[case testMultipleInheritance_GenericSubclasses_SuperclassFirst]
405+
from typing import TypeVar, Generic
406+
T = TypeVar('T')
407+
class ParentGeneric(Generic[T]):
408+
pass
409+
class ChildGeneric(ParentGeneric[T]):
410+
pass
411+
class Base1:
412+
Nested = ParentGeneric
413+
class Base2:
414+
Nested = ChildGeneric
415+
class A(Base1, Base2):
416+
pass
417+
[out]
418+
main:11: error: Definition of "Nested" in base class "Base1" is incompatible with definition in base class "Base2"
419+
420+
[case testMultipleInheritance_GenericSubclasses_SubclassFirst]
421+
from typing import TypeVar, Generic
422+
T = TypeVar('T')
423+
class ParentGeneric(Generic[T]):
424+
pass
425+
class ChildGeneric(ParentGeneric[T]):
426+
pass
427+
class Base1:
428+
Nested = ParentGeneric
429+
class Base2:
430+
Nested = ChildGeneric
431+
class A(Base2, Base1):
432+
pass
433+
[out]
434+
435+
[case testMultipleInheritance_RefersToNamedTuples]
436+
from typing import NamedTuple
437+
class NamedTuple1:
438+
attr1: int
439+
class NamedTuple2:
440+
attr2: int
441+
class Base1:
442+
Nested = NamedTuple1
443+
class Base2:
444+
Nested = NamedTuple2
445+
class A(Base1, Base2):
446+
pass
447+
[out]
448+
main:10: error: Definition of "Nested" in base class "Base1" is incompatible with definition in base class "Base2"
449+
450+
[case testMultipleInheritance_NestedVariableRefersToSuperlassUnderSubclass]
451+
class A:
452+
def __init__(self, arg: str) -> None:
453+
pass
454+
class B(A):
455+
pass
456+
class Base1:
457+
NestedVar = B
458+
class Base2:
459+
NestedVar = A
460+
class Combo(Base2, Base1): ...
461+
[out]
462+
main:10: error: Definition of "NestedVar" in base class "Base2" is incompatible with definition in base class "Base1"
463+
464+
[case testNestedVariableRefersToSubclassOfAnotherNestedClass]
465+
class Mixin1:
466+
class Meta:
467+
pass
468+
class Outer(Mixin1.Meta):
469+
pass
470+
class Mixin2:
471+
NestedVar = Outer
472+
class Combo(Mixin2, Mixin1): ...
473+
[out]
474+
475+
[case testNestedVariableRefersToCompletelyDifferentClasses]
476+
class A:
477+
pass
478+
class B:
479+
pass
480+
class Base1:
481+
NestedVar = A
482+
class Base2:
483+
NestedVar = B
484+
class Combo(Base2, Base1): ...
485+
[out]
486+
main:9: error: Definition of "NestedVar" in base class "Base2" is incompatible with definition in base class "Base1"
487+
488+
[case testDoNotFailIfBothNestedClassesInheritFromAny]
489+
from typing import Any
490+
class Mixin1:
491+
class Meta(Any):
492+
pass
493+
class Mixin2:
494+
class Meta(Any):
495+
pass
496+
class A(Mixin1, Mixin2):
497+
pass
498+
[out]
499+
500+
[case testDoNotFailIfOneOfNestedClassesIsOuterInheritedFromAny]
501+
from typing import Any
502+
class Outer(Any):
503+
pass
504+
class Mixin1:
505+
Meta = Outer
506+
class Mixin2:
507+
class Meta(Any):
508+
pass
509+
class A(Mixin1, Mixin2):
510+
pass
511+
[out]

0 commit comments

Comments
 (0)