diff --git a/docs/source/kinds_of_types.rst b/docs/source/kinds_of_types.rst index 1efc2b30c328..f691bc899339 100644 --- a/docs/source/kinds_of_types.rst +++ b/docs/source/kinds_of_types.rst @@ -566,6 +566,29 @@ Python 3.6 introduced an alternative, class-based syntax for named tuples with t p = Point(x=1, y='x') # Argument has incompatible type "str"; expected "int" +.. note:: + + You can use raw ``NamedTuple`` pseudo-class to annotate type + where any ``NamedTuple`` is expected. + + For example, it can be useful for deserialization: + + .. code-block:: python + + def deserialize_named_tuple(arg: NamedTuple) -> Dict[str, Any]: + return arg._asdict() + + Point = namedtuple('Point', ['x', 'y']) + Person = NamedTuple('Person', [('name', str), ('age', int)]) + + deserialize_named_tuple(Point(x=1, y=2)) # ok + deserialize_named_tuple(Person(name='Nikita', age=18)) # ok + + deserialize_named_tuple((1, 2)) # Argument 1 to "deserialize_named_tuple" has incompatible type "Tuple[int, int]"; expected "NamedTuple" + + Note, that behavior is highly experimental, non-standard, + and can be not supported by other type checkers. + .. _type-of-class: The type of class objects diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 63cebc8aa483..e10a7bba4e5f 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -263,8 +263,13 @@ def visit_instance(self, left: Instance) -> bool: rname = right.type.fullname # Always try a nominal check if possible, # there might be errors that a user wants to silence *once*. - if ((left.type.has_base(rname) or rname == 'builtins.object') and - not self.ignore_declared_variance): + # NamedTuples are a special case, because `NamedTuple` is not listed + # in `TypeInfo.mro`, so when `(a: NamedTuple) -> None` is used, + # we need to check for `is_named_tuple` property + if ((left.type.has_base(rname) or rname == 'builtins.object' + or (rname == 'typing.NamedTuple' + and any(l.is_named_tuple for l in left.type.mro))) + and not self.ignore_declared_variance): # Map left type to corresponding right instances. t = map_instance_to_supertype(left, right.type) nominal = all(self.check_type_parameter(lefta, righta, tvar.variance) diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index d47b069ea45e..fdb0f841cff6 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -973,3 +973,90 @@ B = namedtuple('X', ['a']) # E: First argument to namedtuple() should be C = NamedTuple('X', [('a', 'Y')]) # E: First argument to namedtuple() should be "C", not "X" class Y: ... [builtins fixtures/tuple.pyi] + +[case testNamedTupleTypeIsASuperTypeOfOtherNamedTuples] +from typing import Tuple, NamedTuple + +class Bar(NamedTuple): + name: str = "Bar" + +class Baz(NamedTuple): + a: str + b: str + +class Biz(Baz): ... +class Other: ... +class Both1(Bar, Other): ... +class Both2(Other, Bar): ... +class Both3(Biz, Other): ... + +def print_namedtuple(obj: NamedTuple) -> None: + reveal_type(obj.name) # N: Revealed type is "builtins.str" + +b1: Bar +b2: Baz +b3: Biz +b4: Both1 +b5: Both2 +b6: Both3 +print_namedtuple(b1) # ok +print_namedtuple(b2) # ok +print_namedtuple(b3) # ok +print_namedtuple(b4) # ok +print_namedtuple(b5) # ok +print_namedtuple(b6) # ok + +print_namedtuple(1) # E: Argument 1 to "print_namedtuple" has incompatible type "int"; expected "NamedTuple" +print_namedtuple(('bar',)) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[str]"; expected "NamedTuple" +print_namedtuple((1, 2)) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[int, int]"; expected "NamedTuple" +print_namedtuple((b1,)) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[Bar]"; expected "NamedTuple" +t: Tuple[str, ...] +print_namedtuple(t) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[str, ...]"; expected "NamedTuple" + +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-namedtuple.pyi] + +[case testNamedTupleTypeIsASuperTypeOfOtherNamedTuplesReturns] +from typing import Tuple, NamedTuple + +class Bar(NamedTuple): + n: int + +class Baz(NamedTuple): + a: str + b: str + +class Biz(Bar): ... +class Other: ... +class Both1(Bar, Other): ... +class Both2(Other, Bar): ... +class Both3(Biz, Other): ... + +def good1() -> NamedTuple: + b: Bar + return b +def good2() -> NamedTuple: + b: Baz + return b +def good3() -> NamedTuple: + b: Biz + return b +def good4() -> NamedTuple: + b: Both1 + return b +def good5() -> NamedTuple: + b: Both2 + return b +def good6() -> NamedTuple: + b: Both3 + return b + +def bad1() -> NamedTuple: + return 1 # E: Incompatible return value type (got "int", expected "NamedTuple") +def bad2() -> NamedTuple: + return () # E: Incompatible return value type (got "Tuple[]", expected "NamedTuple") +def bad3() -> NamedTuple: + return (1, 2) # E: Incompatible return value type (got "Tuple[int, int]", expected "NamedTuple") + +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-namedtuple.pyi] diff --git a/test-data/unit/fixtures/typing-namedtuple.pyi b/test-data/unit/fixtures/typing-namedtuple.pyi new file mode 100644 index 000000000000..13b6c4cf9441 --- /dev/null +++ b/test-data/unit/fixtures/typing-namedtuple.pyi @@ -0,0 +1,17 @@ +TypeVar = 0 +Generic = 0 +Any = 0 +overload = 0 +Type = 0 + +T_co = TypeVar('T_co', covariant=True) +KT = TypeVar('KT') + +class Iterable(Generic[T_co]): pass +class Iterator(Iterable[T_co]): pass +class Sequence(Iterable[T_co]): pass +class Mapping(Iterable[KT], Generic[KT, T_co]): pass + +class Tuple(Sequence): pass +class NamedTuple(Tuple): + name: str diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 8a7b39756867..c15c898db46e 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1390,6 +1390,32 @@ x = X(a=1, b='s') [out] _testNamedTupleNew.py:12: note: Revealed type is "Tuple[builtins.int, fallback=_testNamedTupleNew.Child]" +[case testNamedTupleTypeInheritanceSpecialCase] +from typing import NamedTuple, Tuple +from collections import namedtuple + +A = NamedTuple('A', [('param', int)]) +B = namedtuple('B', ['param']) + +def accepts_named_tuple(arg: NamedTuple): + reveal_type(arg._asdict()) + reveal_type(arg._fields) + reveal_type(arg._field_defaults) + +a = A(1) +b = B(1) + +accepts_named_tuple(a) +accepts_named_tuple(b) +accepts_named_tuple(1) +accepts_named_tuple((1, 2)) +[out] +_testNamedTupleTypeInheritanceSpecialCase.py:8: note: Revealed type is "collections.OrderedDict[builtins.str, Any]" +_testNamedTupleTypeInheritanceSpecialCase.py:9: note: Revealed type is "builtins.tuple[builtins.str]" +_testNamedTupleTypeInheritanceSpecialCase.py:10: note: Revealed type is "builtins.dict[builtins.str, Any]" +_testNamedTupleTypeInheritanceSpecialCase.py:17: error: Argument 1 to "accepts_named_tuple" has incompatible type "int"; expected "NamedTuple" +_testNamedTupleTypeInheritanceSpecialCase.py:18: error: Argument 1 to "accepts_named_tuple" has incompatible type "Tuple[int, int]"; expected "NamedTuple" + [case testNewAnalyzerBasicTypeshed_newsemanal] from typing import Dict, List, Tuple