From d394c6e198ce24b6b2e517da8e02f795d6bb5d16 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 13 Jun 2018 20:31:18 -0700 Subject: [PATCH 01/11] add tests --- test-data/unit/check-namedtuple.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index 94bba5045016..944a5ef61d70 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -79,6 +79,18 @@ x = X(1, 'x') a, b = x a, b, c = x # E: Need more than 2 values to unpack (3 expected) +[case testNamedTupleDefaults] +# flags: --python-version 3.7 +from collections import namedtuple + +X = namedtuple('X', ['x', 'y'], defaults=(1,)) + +X() # E: Too few arguments for "X" +X(0) # ok +X(0, 1) # ok +X(0, 1, 2) # E: Too many arguments for "X" + +[builtins fixtures/list.pyi] [case testNamedTupleWithItemTypes] from typing import NamedTuple From c73583da722cd344b66a4a9386b5a6af6e0dc4e1 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 13 Jun 2018 21:22:23 -0700 Subject: [PATCH 02/11] support additional args to namedtuple --- mypy/checkexpr.py | 6 +++++ mypy/nodes.py | 4 ++- mypy/semanal_namedtuple.py | 25 ++++++++++-------- test-data/unit/check-namedtuple.test | 34 +++++++++++++++++++------ test-data/unit/check-newtype.test | 3 +++ test-data/unit/check-python2.test | 8 ------ test-data/unit/fixtures/args.pyi | 1 + test-data/unit/fixtures/ops.pyi | 2 ++ test-data/unit/lib-stub/collections.pyi | 12 +++++++-- test-data/unit/python2eval.test | 8 ++++-- test-data/unit/semanal-namedtuple.test | 4 --- 11 files changed, 72 insertions(+), 35 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 09aca5fedf09..ac337aa16da6 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -200,8 +200,14 @@ def analyze_var_ref(self, var: Var, context: Context) -> Type: def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: """Type check a call expression.""" if e.analyzed: + if isinstance(e.analyzed, NamedTupleExpr) and not e.analyzed.is_typed: + # Type check the arguments, but ignore the results. + self.visit_call_expr_inner(e) # It's really a special form that only looks like a call. return self.accept(e.analyzed, self.type_context[-1]) + return self.visit_call_expr_inner(e, allow_none_return=allow_none_return) + + def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> Type: if isinstance(e.callee, NameExpr) and isinstance(e.callee.node, TypeInfo) and \ e.callee.node.typeddict_type is not None: # Use named fallback for better error messages. diff --git a/mypy/nodes.py b/mypy/nodes.py index 2ec8859a8a46..ff25bf9c4eef 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1906,10 +1906,12 @@ class NamedTupleExpr(Expression): # The class representation of this named tuple (its tuple_type attribute contains # the tuple item types) info = None # type: TypeInfo + is_typed = False # whether this class was created with typing.NamedTuple - def __init__(self, info: 'TypeInfo') -> None: + def __init__(self, info: 'TypeInfo', is_typed: bool = False) -> None: super().__init__() self.info = info + self.is_typed = is_typed def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_namedtuple_expr(self) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 321baf44c09f..907fddf94023 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -14,7 +14,7 @@ AssignmentStmt, PassStmt, Decorator, FuncBase, ClassDef, Expression, RefExpr, TypeInfo, NamedTupleExpr, CallExpr, Context, TupleExpr, ListExpr, SymbolTableNode, FuncDef, Block, TempNode, - ARG_POS, ARG_NAMED_OPT, ARG_OPT, MDEF, GDEF + ARG_POS, ARG_NAMED, ARG_NAMED_OPT, ARG_OPT, MDEF, GDEF ) from mypy.options import Options from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError @@ -48,7 +48,7 @@ def analyze_namedtuple_classdef(self, defn: ClassDef) -> Optional[TypeInfo]: node.node = info defn.info.replaced = info defn.info = info - defn.analyzed = NamedTupleExpr(info) + defn.analyzed = NamedTupleExpr(info, is_typed=True) defn.analyzed.line = defn.line defn.analyzed.column = defn.column return info @@ -142,9 +142,13 @@ def check_namedtuple(self, if not isinstance(callee, RefExpr): return None fullname = callee.fullname - if fullname not in ('collections.namedtuple', 'typing.NamedTuple'): + if fullname == 'collections.namedtuple': + is_typed = False + elif fullname == 'typing.NamedTuple': + is_typed = True + else: return None - items, types, ok = self.parse_namedtuple_args(call, fullname) + items, types, ok = self.parse_namedtuple_args(call, fullname, is_typed) if not ok: # Error. Construct dummy return value. return self.build_namedtuple_typeinfo('namedtuple', [], [], {}) @@ -157,20 +161,21 @@ def check_namedtuple(self, # (Or in the nearest class if there is one.) stnode = SymbolTableNode(GDEF, info) self.api.add_symbol_table_node(name, stnode) - call.analyzed = NamedTupleExpr(info) + call.analyzed = NamedTupleExpr(info, is_typed=is_typed) call.analyzed.set_line(call.line, call.column) return info - def parse_namedtuple_args(self, call: CallExpr, - fullname: str) -> Tuple[List[str], List[Type], bool]: + def parse_namedtuple_args(self, call: CallExpr, fullname: str, + is_typed: bool) -> Tuple[List[str], List[Type], bool]: # TODO: Share code with check_argument_count in checkexpr.py? args = call.args if len(args) < 2: return self.fail_namedtuple_arg("Too few arguments for namedtuple()", call) if len(args) > 2: - # FIX incorrect. There are two additional parameters - return self.fail_namedtuple_arg("Too many arguments for namedtuple()", call) - if call.arg_kinds != [ARG_POS, ARG_POS]: + # Typed namedtuple doesn't support additional arguments. + if is_typed: + return self.fail_namedtuple_arg("Too many arguments for namedtuple()", call) + if call.arg_kinds[:2] != [ARG_POS, ARG_POS]: return self.fail_namedtuple_arg("Unexpected arguments to namedtuple()", call) if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)): return self.fail_namedtuple_arg( diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index 944a5ef61d70..10f88058dd15 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -1,7 +1,7 @@ [case testNamedTupleUsedAsTuple] from collections import namedtuple -X = namedtuple('X', ['x', 'y']) +X = namedtuple('X', 'x y') x = None # type: X a, b = x b = x[0] @@ -28,7 +28,7 @@ X = namedtuple('X', 'x, _y, _z') # E: namedtuple() field names cannot start wit [case testNamedTupleAccessingAttributes] from collections import namedtuple -X = namedtuple('X', ['x', 'y']) +X = namedtuple('X', 'x y') x = None # type: X x.x x.y @@ -38,7 +38,7 @@ x.z # E: "X" has no attribute "z" [case testNamedTupleAttributesAreReadOnly] from collections import namedtuple -X = namedtuple('X', ['x', 'y']) +X = namedtuple('X', 'x y') x = None # type: X x.x = 5 # E: Property "x" defined in "X" is read-only x.y = 5 # E: Property "y" defined in "X" is read-only @@ -54,32 +54,46 @@ a.y = 5 # E: Property "y" defined in "X" is read-only [case testNamedTupleCreateWithPositionalArguments] from collections import namedtuple -X = namedtuple('X', ['x', 'y']) +X = namedtuple('X', 'x y') x = X(1, 'x') x.x x.z # E: "X" has no attribute "z" x = X(1) # E: Too few arguments for "X" x = X(1, 2, 3) # E: Too many arguments for "X" +[builtins fixtures/list.pyi] + [case testCreateNamedTupleWithKeywordArguments] from collections import namedtuple -X = namedtuple('X', ['x', 'y']) +X = namedtuple('X', 'x y') x = X(x=1, y='x') x = X(1, y='x') x = X(x=1, z=1) # E: Unexpected keyword argument "z" for "X" x = X(y=1) # E: Missing positional argument "x" in call to "X" - [case testNamedTupleCreateAndUseAsTuple] from collections import namedtuple -X = namedtuple('X', ['x', 'y']) +X = namedtuple('X', 'x y') x = X(1, 'x') a, b = x a, b, c = x # E: Need more than 2 values to unpack (3 expected) -[case testNamedTupleDefaults] +[case testNamedTupleAdditionalArgs] +from collections import namedtuple + +A = namedtuple('A', 'a b') +B = namedtuple('B', 'a b', rename=1) +C = namedtuple('C', 'a b', rename='not a bool') # E: Argument "rename" to "namedtuple" has incompatible type "str"; expected "int" +# This works correctly but also produces "mypy/test-data/unit/lib-stub/collections.pyi:3: note: "namedtuple" defined here" +# Don't know how to express that in a test. +# D = namedtuple('D', 'a b', unrecognized_arg=False) # E: Unexpected keyword argument "unrecognized_arg" for "namedtuple" +E = namedtuple('E', 'a b', 0) # E: Too many positional arguments for "namedtuple" + +[builtins fixtures/bool.pyi] + +[case testNamedTupleDefaults-skip] # flags: --python-version 3.7 from collections import namedtuple @@ -235,6 +249,7 @@ import collections MyNamedTuple = collections.namedtuple('MyNamedTuple', ['spam', 'eggs']) MyNamedTuple.x # E: "Type[MyNamedTuple]" has no attribute "x" +[builtins fixtures/list.pyi] [case testNamedTupleEmptyItems] from typing import NamedTuple @@ -275,6 +290,8 @@ x._replace(x=3, y=5) x._replace(z=5) # E: Unexpected keyword argument "z" for "_replace" of "X" x._replace(5) # E: Too many positional arguments for "_replace" of "X" +[builtins fixtures/list.pyi] + [case testNamedTupleReplaceAsClass] from collections import namedtuple @@ -283,6 +300,7 @@ x = None # type: X X._replace(x, x=1, y=2) X._replace(x=1, y=2) # E: Missing positional argument "self" in call to "_replace" of "X" +[builtins fixtures/list.pyi] [case testNamedTupleReplaceTyped] from typing import NamedTuple diff --git a/test-data/unit/check-newtype.test b/test-data/unit/check-newtype.test index 82e99ae5b7b3..25485f627d97 100644 --- a/test-data/unit/check-newtype.test +++ b/test-data/unit/check-newtype.test @@ -123,6 +123,9 @@ Point3 = NewType('Point3', Vector3) p3 = Point3(Vector3(1, 3)) reveal_type(p3.x) # E: Revealed type is 'builtins.int' reveal_type(p3.y) # E: Revealed type is 'builtins.int' + +[builtins fixtures/list.pyi] + [out] [case testNewTypeWithCasts] diff --git a/test-data/unit/check-python2.test b/test-data/unit/check-python2.test index 4011ef57f4e7..af6eef02acb4 100644 --- a/test-data/unit/check-python2.test +++ b/test-data/unit/check-python2.test @@ -13,14 +13,6 @@ s = b'foo' from typing import TypeVar T = TypeVar(u'T') -[case testNamedTupleUnicode] -from typing import NamedTuple -from collections import namedtuple -N = NamedTuple(u'N', [(u'x', int)]) -n = namedtuple(u'n', u'x y') - -[builtins fixtures/dict.pyi] - [case testPrintStatement] print ''() # E: "str" not callable print 1, 1() # E: "int" not callable diff --git a/test-data/unit/fixtures/args.pyi b/test-data/unit/fixtures/args.pyi index b3d05b5a7f39..0a38ceeece2e 100644 --- a/test-data/unit/fixtures/args.pyi +++ b/test-data/unit/fixtures/args.pyi @@ -27,3 +27,4 @@ class int: class str: pass class bool: pass class function: pass +class ellipsis: pass diff --git a/test-data/unit/fixtures/ops.pyi b/test-data/unit/fixtures/ops.pyi index ae48a1f2019a..8f0eb8b2fcce 100644 --- a/test-data/unit/fixtures/ops.pyi +++ b/test-data/unit/fixtures/ops.pyi @@ -55,3 +55,5 @@ class float: pass class BaseException: pass def __print(a1=None, a2=None, a3=None, a4=None): pass + +class ellipsis: pass diff --git a/test-data/unit/lib-stub/collections.pyi b/test-data/unit/lib-stub/collections.pyi index 00b7cea64beb..1f2c4ad26e75 100644 --- a/test-data/unit/lib-stub/collections.pyi +++ b/test-data/unit/lib-stub/collections.pyi @@ -1,3 +1,11 @@ -import typing +from typing import Any, Iterable, Union, Optional -namedtuple = object() +def namedtuple( + typename: str, + field_names: Union[str, Iterable[str]], + *, + # really int but many tests don't have bool available + rename: int = ..., + module: Optional[str] = ..., + defaults: Optional[Iterable[Any]] = ... +) -> Any: ... diff --git a/test-data/unit/python2eval.test b/test-data/unit/python2eval.test index 6007cad7780c..eb6882097822 100644 --- a/test-data/unit/python2eval.test +++ b/test-data/unit/python2eval.test @@ -262,13 +262,17 @@ s = ''.join([u'']) # Error _program.py:5: error: Incompatible types in assignment (expression has type "str", variable has type "int") _program.py:6: error: Incompatible types in assignment (expression has type "unicode", variable has type "str") -[case testNamedTupleError_python2] -import typing +[case testNamedTuple_python2] +from typing import NamedTuple from collections import namedtuple X = namedtuple('X', ['a', 'b']) x = X(a=1, b='s') x.c x.a + +N = NamedTuple(u'N', [(u'x', int)]) +n = namedtuple(u'n', u'x y') + [out] _program.py:5: error: "X" has no attribute "c" diff --git a/test-data/unit/semanal-namedtuple.test b/test-data/unit/semanal-namedtuple.test index a820a07fe745..6e663344bf1b 100644 --- a/test-data/unit/semanal-namedtuple.test +++ b/test-data/unit/semanal-namedtuple.test @@ -138,10 +138,6 @@ MypyFile:1( from collections import namedtuple N = namedtuple('N') # E: Too few arguments for namedtuple() -[case testNamedTupleWithTooManyArguments] -from collections import namedtuple -N = namedtuple('N', ['x'], 'y') # E: Too many arguments for namedtuple() - [case testNamedTupleWithInvalidName] from collections import namedtuple N = namedtuple(1, ['x']) # E: namedtuple() expects a string literal as the first argument From ec25ae97208fa9a0ed4a8fc7db90a21591750404 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 13 Jun 2018 21:32:37 -0700 Subject: [PATCH 03/11] fix test; remove unnecessary fixture --- test-data/unit/check-namedtuple.test | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index 10f88058dd15..c432d188e318 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -61,8 +61,6 @@ x.z # E: "X" has no attribute "z" x = X(1) # E: Too few arguments for "X" x = X(1, 2, 3) # E: Too many arguments for "X" -[builtins fixtures/list.pyi] - [case testCreateNamedTupleWithKeywordArguments] from collections import namedtuple @@ -88,7 +86,7 @@ B = namedtuple('B', 'a b', rename=1) C = namedtuple('C', 'a b', rename='not a bool') # E: Argument "rename" to "namedtuple" has incompatible type "str"; expected "int" # This works correctly but also produces "mypy/test-data/unit/lib-stub/collections.pyi:3: note: "namedtuple" defined here" # Don't know how to express that in a test. -# D = namedtuple('D', 'a b', unrecognized_arg=False) # E: Unexpected keyword argument "unrecognized_arg" for "namedtuple" +# D = namedtuple('D', 'a b', unrecognized_arg=False) E: Unexpected keyword argument "unrecognized_arg" for "namedtuple" E = namedtuple('E', 'a b', 0) # E: Too many positional arguments for "namedtuple" [builtins fixtures/bool.pyi] From b02b21ea3280feee607206aeeab329d78d311af7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 13 Jun 2018 21:56:30 -0700 Subject: [PATCH 04/11] support 3.7 defaults --- mypy/semanal_namedtuple.py | 59 +++++++++++++++++++++++----- test-data/unit/check-namedtuple.test | 5 ++- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 907fddf94023..63ddc3cb8de6 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -148,7 +148,7 @@ def check_namedtuple(self, is_typed = True else: return None - items, types, ok = self.parse_namedtuple_args(call, fullname, is_typed) + items, types, num_defaults, ok = self.parse_namedtuple_args(call, fullname, is_typed) if not ok: # Error. Construct dummy return value. return self.build_namedtuple_typeinfo('namedtuple', [], [], {}) @@ -156,7 +156,14 @@ def check_namedtuple(self, if name != var_name or is_func_scope: # Give it a unique name derived from the line number. name += '@' + str(call.line) - info = self.build_namedtuple_typeinfo(name, items, types, {}) + if num_defaults > 0: + default_items = { + arg_name: EllipsisExpr() + for arg_name in items[-num_defaults:] + } + else: + default_items = {} + info = self.build_namedtuple_typeinfo(name, items, types, default_items) # Store it as a global just in case it would remain anonymous. # (Or in the nearest class if there is one.) stnode = SymbolTableNode(GDEF, info) @@ -166,15 +173,39 @@ def check_namedtuple(self, return info def parse_namedtuple_args(self, call: CallExpr, fullname: str, - is_typed: bool) -> Tuple[List[str], List[Type], bool]: + is_typed: bool) -> Tuple[List[str], List[Type], int, bool]: + """Parse a namedtuple() call into data needed to construct a type. + + Returns a 4-tuple: + - List of argument names + - List of argument types + - Number of arguments that have a default value + - Whether the definition typechecked. + + """ # TODO: Share code with check_argument_count in checkexpr.py? args = call.args if len(args) < 2: return self.fail_namedtuple_arg("Too few arguments for namedtuple()", call) + num_defaults = 0 if len(args) > 2: # Typed namedtuple doesn't support additional arguments. if is_typed: return self.fail_namedtuple_arg("Too many arguments for namedtuple()", call) + for i, arg_name in enumerate(call.arg_names[2:], 2): + if arg_name == 'defaults': + arg = args[i] + # We don't care what the values are, as long as the argument is an iterable + # and we can count how many defaults there are. + if isinstance(arg, (ListExpr, TupleExpr)): + num_defaults = len(arg.items) + else: + self.fail( + "List or tuple literal expected as the defaults argument to " + "namedtuple()", + arg + ) + break if call.arg_kinds[:2] != [ARG_POS, ARG_POS]: return self.fail_namedtuple_arg("Unexpected arguments to namedtuple()", call) if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)): @@ -201,17 +232,27 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str, items = [cast(StrExpr, item).value for item in listexpr.items] else: # The fields argument contains (name, type) tuples. - items, types, ok = self.parse_namedtuple_fields_with_types(listexpr.items, call) + items, types, _, ok = self.parse_namedtuple_fields_with_types(listexpr.items, call) if not types: types = [AnyType(TypeOfAny.unannotated) for _ in items] underscore = [item for item in items if item.startswith('_')] if underscore: self.fail("namedtuple() field names cannot start with an underscore: " + ', '.join(underscore), call) - return items, types, ok + if num_defaults > len(items): + self.fail("Too many defaults given in call to namedtuple()", call) + num_defaults = len(items) + return items, types, num_defaults, ok + + def extract_defaults_from_arg(self, call: CallExpr, arg: Expression) -> int: + if not isinstance(arg, (ListExpr, TupleExpr)): + self.fail("List or tuple literal expected as the defaults argument to namedtuple()", + call) + return 0 + def parse_namedtuple_fields_with_types(self, nodes: List[Expression], - context: Context) -> Tuple[List[str], List[Type], bool]: + context: Context) -> Tuple[List[str], List[Type], int, bool]: items = [] # type: List[str] types = [] # type: List[Type] for item in nodes: @@ -231,12 +272,12 @@ def parse_namedtuple_fields_with_types(self, nodes: List[Expression], types.append(self.api.anal_type(type)) else: return self.fail_namedtuple_arg("Tuple expected as NamedTuple() field", item) - return items, types, True + return items, types, 0, True def fail_namedtuple_arg(self, message: str, - context: Context) -> Tuple[List[str], List[Type], bool]: + context: Context) -> Tuple[List[str], List[Type], int, bool]: self.fail(message, context) - return [], [], False + return [], [], 0, False def build_namedtuple_typeinfo(self, name: str, items: List[str], types: List[Type], default_items: Dict[str, Expression]) -> TypeInfo: diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index c432d188e318..ffceebf1c8ff 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -91,7 +91,7 @@ E = namedtuple('E', 'a b', 0) # E: Too many positional arguments for "namedtupl [builtins fixtures/bool.pyi] -[case testNamedTupleDefaults-skip] +[case testNamedTupleDefaults] # flags: --python-version 3.7 from collections import namedtuple @@ -102,6 +102,9 @@ X(0) # ok X(0, 1) # ok X(0, 1, 2) # E: Too many arguments for "X" +Y = namedtuple('Y', ['x', 'y'], defaults=(1, 2, 3)) # E: Too many defaults given in call to namedtuple() +Z = namedtuple('Z', ['x', 'y'], defaults='not a tuple') # E: Argument "defaults" to "namedtuple" has incompatible type "str"; expected "Optional[Iterable[Any]]" # E: List or tuple literal expected as the defaults argument to namedtuple() + [builtins fixtures/list.pyi] [case testNamedTupleWithItemTypes] From 4613aa3cfec8e4d479f96f0347911c395642f73d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 13 Jun 2018 22:11:10 -0700 Subject: [PATCH 05/11] remove function I didn't need --- mypy/semanal_namedtuple.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 63ddc3cb8de6..bf445f4dacd7 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -244,13 +244,6 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str, num_defaults = len(items) return items, types, num_defaults, ok - def extract_defaults_from_arg(self, call: CallExpr, arg: Expression) -> int: - if not isinstance(arg, (ListExpr, TupleExpr)): - self.fail("List or tuple literal expected as the defaults argument to namedtuple()", - call) - return 0 - - def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: Context) -> Tuple[List[str], List[Type], int, bool]: items = [] # type: List[str] From 6c9a8b5ed21a9779a81309af9faa86f11fe9f5f6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 13 Jun 2018 22:22:00 -0700 Subject: [PATCH 06/11] fix long line --- mypy/semanal_namedtuple.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index bf445f4dacd7..504062840fff 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -244,8 +244,9 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str, num_defaults = len(items) return items, types, num_defaults, ok - def parse_namedtuple_fields_with_types(self, nodes: List[Expression], - context: Context) -> Tuple[List[str], List[Type], int, bool]: + def parse_namedtuple_fields_with_types( + self, nodes: List[Expression], context: Context + ) -> Tuple[List[str], List[Type], int, bool]: items = [] # type: List[str] types = [] # type: List[Type] for item in nodes: From e977b71c35de709297c002ff0a7712dafe0873e6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 13 Jun 2018 22:24:24 -0700 Subject: [PATCH 07/11] remove redundant argument --- mypy/semanal_namedtuple.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 504062840fff..b103520658a9 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -148,7 +148,7 @@ def check_namedtuple(self, is_typed = True else: return None - items, types, num_defaults, ok = self.parse_namedtuple_args(call, fullname, is_typed) + items, types, num_defaults, ok = self.parse_namedtuple_args(call, fullname) if not ok: # Error. Construct dummy return value. return self.build_namedtuple_typeinfo('namedtuple', [], [], {}) @@ -172,8 +172,8 @@ def check_namedtuple(self, call.analyzed.set_line(call.line, call.column) return info - def parse_namedtuple_args(self, call: CallExpr, fullname: str, - is_typed: bool) -> Tuple[List[str], List[Type], int, bool]: + def parse_namedtuple_args(self, call: CallExpr, + fullname: str) -> Tuple[List[str], List[Type], int, bool]: """Parse a namedtuple() call into data needed to construct a type. Returns a 4-tuple: @@ -190,7 +190,7 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str, num_defaults = 0 if len(args) > 2: # Typed namedtuple doesn't support additional arguments. - if is_typed: + if fullname == 'typing.NamedTuple': return self.fail_namedtuple_arg("Too many arguments for namedtuple()", call) for i, arg_name in enumerate(call.arg_names[2:], 2): if arg_name == 'defaults': From 4c47a0a12d9dcd194979e24fa1b333d198dd6c4f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 13 Jun 2018 22:41:40 -0700 Subject: [PATCH 08/11] fix error in self-check --- mypy/semanal_namedtuple.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index b103520658a9..498f3bfea8b2 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -3,7 +3,7 @@ This is conceptually part of mypy.semanal (semantic analyzer pass 2). """ -from typing import Tuple, List, Dict, Optional, cast +from typing import Tuple, List, Dict, Mapping, Optional, cast from mypy.types import ( Type, TupleType, NoneTyp, AnyType, TypeOfAny, TypeVarType, TypeVarDef, CallableType, TypeType @@ -274,7 +274,7 @@ def fail_namedtuple_arg(self, message: str, return [], [], 0, False def build_namedtuple_typeinfo(self, name: str, items: List[str], types: List[Type], - default_items: Dict[str, Expression]) -> TypeInfo: + default_items: Mapping[str, Expression]) -> TypeInfo: strtype = self.api.named_type('__builtins__.str') implicit_any = AnyType(TypeOfAny.special_form) basetuple_type = self.api.named_type('__builtins__.tuple', [implicit_any]) From 138b2fd11640c1ddeab11800a48369ab362dc6e7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 3 Aug 2018 08:31:37 -0700 Subject: [PATCH 09/11] respond to code review --- mypy/checkexpr.py | 3 ++- mypy/semanal_namedtuple.py | 4 +++- test-data/unit/check-namedtuple.test | 14 +++++++++----- test-data/unit/lib-stub/collections.pyi | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index ac337aa16da6..ca692e12c38c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -201,7 +201,8 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: """Type check a call expression.""" if e.analyzed: if isinstance(e.analyzed, NamedTupleExpr) and not e.analyzed.is_typed: - # Type check the arguments, but ignore the results. + # Type check the arguments, but ignore the results. This relies + # on the typeshed stubs to type check the arguments. self.visit_call_expr_inner(e) # It's really a special form that only looks like a call. return self.accept(e.analyzed, self.type_context[-1]) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 498f3bfea8b2..c7078d3b0140 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -158,6 +158,8 @@ def check_namedtuple(self, name += '@' + str(call.line) if num_defaults > 0: default_items = { + # We don't have the actual argument expressions any more, so we + # just use Ellipsis for all defaults. arg_name: EllipsisExpr() for arg_name in items[-num_defaults:] } @@ -191,7 +193,7 @@ def parse_namedtuple_args(self, call: CallExpr, if len(args) > 2: # Typed namedtuple doesn't support additional arguments. if fullname == 'typing.NamedTuple': - return self.fail_namedtuple_arg("Too many arguments for namedtuple()", call) + return self.fail_namedtuple_arg("Too many arguments for NamedTuple()", call) for i, arg_name in enumerate(call.arg_names[2:], 2): if arg_name == 'defaults': arg = args[i] diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index ffceebf1c8ff..c71a6be4a084 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -83,14 +83,18 @@ from collections import namedtuple A = namedtuple('A', 'a b') B = namedtuple('B', 'a b', rename=1) -C = namedtuple('C', 'a b', rename='not a bool') # E: Argument "rename" to "namedtuple" has incompatible type "str"; expected "int" -# This works correctly but also produces "mypy/test-data/unit/lib-stub/collections.pyi:3: note: "namedtuple" defined here" -# Don't know how to express that in a test. -# D = namedtuple('D', 'a b', unrecognized_arg=False) E: Unexpected keyword argument "unrecognized_arg" for "namedtuple" -E = namedtuple('E', 'a b', 0) # E: Too many positional arguments for "namedtuple" +C = namedtuple('C', 'a b', rename='not a bool') +D = namedtuple('D', 'a b', unrecognized_arg=False) +E = namedtuple('E', 'a b', 0) [builtins fixtures/bool.pyi] +[out] +main:5: error: Argument "rename" to "namedtuple" has incompatible type "str"; expected "int" +main:6: error: Unexpected keyword argument "unrecognized_arg" for "namedtuple" +/test-data/unit/lib-stub/collections.pyi:3: note: "namedtuple" defined here +main:7: error: Too many positional arguments for "namedtuple" + [case testNamedTupleDefaults] # flags: --python-version 3.7 from collections import namedtuple diff --git a/test-data/unit/lib-stub/collections.pyi b/test-data/unit/lib-stub/collections.pyi index 1f2c4ad26e75..580e7c2f9798 100644 --- a/test-data/unit/lib-stub/collections.pyi +++ b/test-data/unit/lib-stub/collections.pyi @@ -4,7 +4,7 @@ def namedtuple( typename: str, field_names: Union[str, Iterable[str]], *, - # really int but many tests don't have bool available + # really bool but many tests don't have bool available rename: int = ..., module: Optional[str] = ..., defaults: Optional[Iterable[Any]] = ... From ebc66d28099ad0241dfdac172a7a5c8241eeabda Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 3 Aug 2018 08:36:15 -0700 Subject: [PATCH 10/11] pass the actual defaults --- mypy/semanal_namedtuple.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index c7078d3b0140..148a3b579966 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -148,7 +148,7 @@ def check_namedtuple(self, is_typed = True else: return None - items, types, num_defaults, ok = self.parse_namedtuple_args(call, fullname) + items, types, defaults, ok = self.parse_namedtuple_args(call, fullname) if not ok: # Error. Construct dummy return value. return self.build_namedtuple_typeinfo('namedtuple', [], [], {}) @@ -156,12 +156,12 @@ def check_namedtuple(self, if name != var_name or is_func_scope: # Give it a unique name derived from the line number. name += '@' + str(call.line) - if num_defaults > 0: + if len(defaults) > 0: default_items = { # We don't have the actual argument expressions any more, so we # just use Ellipsis for all defaults. - arg_name: EllipsisExpr() - for arg_name in items[-num_defaults:] + arg_name: default + for arg_name, default in zip(items[-len(defaults):], defaults) } else: default_items = {} @@ -175,7 +175,7 @@ def check_namedtuple(self, return info def parse_namedtuple_args(self, call: CallExpr, - fullname: str) -> Tuple[List[str], List[Type], int, bool]: + fullname: str) -> Tuple[List[str], List[Type], List[Expression], bool]: """Parse a namedtuple() call into data needed to construct a type. Returns a 4-tuple: @@ -189,7 +189,7 @@ def parse_namedtuple_args(self, call: CallExpr, args = call.args if len(args) < 2: return self.fail_namedtuple_arg("Too few arguments for namedtuple()", call) - num_defaults = 0 + defaults = [] # type: List[Expression] if len(args) > 2: # Typed namedtuple doesn't support additional arguments. if fullname == 'typing.NamedTuple': @@ -200,7 +200,7 @@ def parse_namedtuple_args(self, call: CallExpr, # We don't care what the values are, as long as the argument is an iterable # and we can count how many defaults there are. if isinstance(arg, (ListExpr, TupleExpr)): - num_defaults = len(arg.items) + defaults = list(arg.items) else: self.fail( "List or tuple literal expected as the defaults argument to " @@ -241,10 +241,10 @@ def parse_namedtuple_args(self, call: CallExpr, if underscore: self.fail("namedtuple() field names cannot start with an underscore: " + ', '.join(underscore), call) - if num_defaults > len(items): + if len(defaults) > len(items): self.fail("Too many defaults given in call to namedtuple()", call) - num_defaults = len(items) - return items, types, num_defaults, ok + defaults = defaults[:len(items)] + return items, types, defaults, ok def parse_namedtuple_fields_with_types( self, nodes: List[Expression], context: Context From ff63b1ba24e2d1c1c4bac176e9440ea91c650210 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 3 Aug 2018 10:14:47 -0700 Subject: [PATCH 11/11] fix CI; remove obsolete comment --- mypy/semanal_namedtuple.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 148a3b579966..4c3ddc43be1d 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -158,8 +158,6 @@ def check_namedtuple(self, name += '@' + str(call.line) if len(defaults) > 0: default_items = { - # We don't have the actual argument expressions any more, so we - # just use Ellipsis for all defaults. arg_name: default for arg_name, default in zip(items[-len(defaults):], defaults) } @@ -174,8 +172,8 @@ def check_namedtuple(self, call.analyzed.set_line(call.line, call.column) return info - def parse_namedtuple_args(self, call: CallExpr, - fullname: str) -> Tuple[List[str], List[Type], List[Expression], bool]: + def parse_namedtuple_args(self, call: CallExpr, fullname: str + ) -> Tuple[List[str], List[Type], List[Expression], bool]: """Parse a namedtuple() call into data needed to construct a type. Returns a 4-tuple: @@ -246,9 +244,9 @@ def parse_namedtuple_args(self, call: CallExpr, defaults = defaults[:len(items)] return items, types, defaults, ok - def parse_namedtuple_fields_with_types( - self, nodes: List[Expression], context: Context - ) -> Tuple[List[str], List[Type], int, bool]: + def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: Context + ) -> Tuple[List[str], List[Type], List[Expression], + bool]: items = [] # type: List[str] types = [] # type: List[Type] for item in nodes: @@ -268,12 +266,12 @@ def parse_namedtuple_fields_with_types( types.append(self.api.anal_type(type)) else: return self.fail_namedtuple_arg("Tuple expected as NamedTuple() field", item) - return items, types, 0, True + return items, types, [], True - def fail_namedtuple_arg(self, message: str, - context: Context) -> Tuple[List[str], List[Type], int, bool]: + def fail_namedtuple_arg(self, message: str, context: Context + ) -> Tuple[List[str], List[Type], List[Expression], bool]: self.fail(message, context) - return [], [], 0, False + return [], [], [], False def build_namedtuple_typeinfo(self, name: str, items: List[str], types: List[Type], default_items: Mapping[str, Expression]) -> TypeInfo: