Skip to content

Make NamedTuple provide __new__ instead of __init__ #5643

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 3 commits into from
Sep 19, 2018
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
4 changes: 2 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1563,8 +1563,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 @@ -748,3 +748,15 @@ def f(o: Base) -> None:
reveal_type(o.value) # E: Revealed type is 'builtins.float'
[builtins fixtures/isinstance.pyi]
[out]

[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]'