Skip to content

Commit

Permalink
Make NamedTuple provide __new__ instead of __init__
Browse files Browse the repository at this point in the history
Closes #1279.
  • Loading branch information
msullivan committed Sep 19, 2018
1 parent b06d60a commit 5529359
Show file tree
Hide file tree
Showing 9 changed files with 50 additions and 13 deletions.
4 changes: 2 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1560,8 +1560,8 @@ def check_compatibility(self, name: str, base1: TypeInfo,
a direct subclass relationship (i.e., the compatibility requirement only derives from
multiple inheritance).
"""
if name == '__init__':
# __init__ can be incompatible -- it's a special case.
if name in ('__init__', '__new__', '__init_subclass__'):
# __init__ and friends can be incompatible -- it's a special case.
return
first = base1[name]
second = base2[name]
Expand Down
2 changes: 1 addition & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]:
nt_names = named_tuple_info.names
named_tuple_info.names = SymbolTable()
# This is needed for the cls argument to classmethods to get bound correctly.
named_tuple_info.names['__init__'] = nt_names['__init__']
named_tuple_info.names['__new__'] = nt_names['__new__']

self.enter_class(named_tuple_info)

Expand Down
8 changes: 5 additions & 3 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,9 @@ def add_method(funcname: str,
args: List[Argument],
name: Optional[str] = None,
is_classmethod: bool = False,
is_new: bool = False,
) -> None:
if is_classmethod:
if is_classmethod or is_new:
first = [Argument(Var('cls'), TypeType.make_normalized(selftype), None, ARG_POS)]
else:
first = [Argument(Var('self'), selftype, None, ARG_POS)]
Expand Down Expand Up @@ -384,8 +385,9 @@ def make_init_arg(var: Var) -> Argument:
kind = ARG_POS if default is None else ARG_OPT
return Argument(var, var.type, default, kind)

add_method('__init__', ret=NoneTyp(), name=info.name(),
args=[make_init_arg(var) for var in vars])
add_method('__new__', ret=selftype, name=info.name(),
args=[make_init_arg(var) for var in vars],
is_new=True)
add_method('_asdict', args=[], ret=ordereddictype)
special_form_any = AnyType(TypeOfAny.special_form)
add_method('_make', ret=selftype, is_classmethod=True,
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-class-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ class XMethBad(NamedTuple):
class MagicalFields(NamedTuple):
x: int
def __slots__(self) -> None: pass # E: Cannot overwrite NamedTuple attribute "__slots__"
def __new__(cls) -> None: pass # E: Cannot overwrite NamedTuple attribute "__new__"
def __new__(cls) -> None: pass # E: Name '__new__' already defined (possibly by an import)
def _source(self) -> int: pass # E: Cannot overwrite NamedTuple attribute "_source"
__annotations__ = {'x': float} # E: NamedTuple field name cannot start with an underscore: __annotations__ \
# E: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" \
Expand Down
12 changes: 12 additions & 0 deletions test-data/unit/check-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -732,3 +732,15 @@ class CallableTuple(Thing):

o = CallableTuple('hello ', 12)
o()

[case testNamedTupleNew]
from typing import NamedTuple

Base = NamedTuple('Base', [('param', int)])

class Child(Base):
def __new__(cls, param: int = 1) -> 'Child':
return Base.__new__(cls, param)

Base(param=10)
Child(param=10)
4 changes: 2 additions & 2 deletions test-data/unit/cmdline.test
Original file line number Diff line number Diff line change
Expand Up @@ -1182,7 +1182,7 @@ warn_unused_configs = True
[[mypy-emarg.hatch]
-- Currently we don't treat an unstructured pattern like a.*.b as unused
-- if it matches another section (like a.x.b). This would be reasonable
-- to change.
-- to change. '
[[mypy-a.*.b]
[[mypy-a.*.c]
[[mypy-a.x.b]
Expand Down Expand Up @@ -1257,6 +1257,6 @@ import d
[case testCacheMap]
-- This just checks that a valid --cache-map triple is accepted.
-- (Errors are too verbose to check.)
# cmd: mypy a.py --cache-map a.py a.meta.json a.data.json
# cmd: mypy a.py --cache-map a.py a.meta.json a.data.json
[file a.py]
[out]
2 changes: 1 addition & 1 deletion test-data/unit/diff.test
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ M = NamedTuple('M', [('x', int), ('y', str)])
[out]
__main__.A
__main__.N
__main__.N.__init__
__main__.N.__new__
__main__.N._asdict
__main__.N._make
__main__.N._replace
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/merge.test
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ TypeInfo<2>(
Names(
__annotations__<4> (builtins.object<1>)
__doc__<5> (builtins.str<6>)
__init__<7>
__new__<7>
_asdict<8>
_field_defaults<9> (builtins.object<1>)
_field_types<10> (builtins.object<1>)
Expand All @@ -690,7 +690,7 @@ TypeInfo<2>(
Names(
__annotations__<4> (builtins.object<1>)
__doc__<5> (builtins.str<6>)
__init__<7>
__new__<7>
_asdict<8>
_field_defaults<9> (builtins.object<1>)
_field_types<10> (builtins.object<1>)
Expand Down
25 changes: 24 additions & 1 deletion test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ print(list(reversed(A())))
[[3, 2, 1]
[['c', 'b', 'a']
[['f', 'o', 'o']
-- ]]]]]

[case testIntAndFloatConversion]
from typing import SupportsInt, SupportsFloat
Expand Down Expand Up @@ -90,6 +91,7 @@ import typing
print(list.__add__([1, 2], [3, 4]))
[out]
[[1, 2, 3, 4]
-- ]

[case testInheritedClassAttribute]
import typing
Expand Down Expand Up @@ -1047,7 +1049,7 @@ _testTypedDictGet.py:9: error: TypedDict "D" has no key 'z'
_testTypedDictGet.py:10: error: All overload variants of "get" of "Mapping" require at least one argument
_testTypedDictGet.py:10: note: Possible overload variants:
_testTypedDictGet.py:10: note: def get(self, k: str) -> object
_testTypedDictGet.py:10: note: def [_T] get(self, k: str, default: object) -> object
_testTypedDictGet.py:10: note: def [_T] get(self, k: str, default: object) -> object
_testTypedDictGet.py:12: error: Revealed type is 'builtins.object*'

[case testTypedDictMappingMethods]
Expand Down Expand Up @@ -1317,3 +1319,24 @@ def g(ms: 'T[M]') -> None:
reduce(f, ms)
T = Iterable
[out]

[case testNamedTupleNew]
# This is an eval test because there was a snag found only with full stubs
from typing import NamedTuple

Base = NamedTuple('Base', [('param', int)])

class Child(Base):
def __new__(cls, param: int = 1) -> 'Child':
return Base.__new__(cls, param)

Base(param=10)
Child(param=10)
reveal_type(Child())

from collections import namedtuple
X = namedtuple('X', ['a', 'b'])
x = X(a=1, b='s')

[out]
_testNamedTupleNew.py:12: error: Revealed type is 'Tuple[builtins.int, fallback=_testNamedTupleNew.Child]'

0 comments on commit 5529359

Please sign in to comment.