From 0e17fe996e3c5fcf20af1f71fb084336b9e950b1 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 17 Sep 2021 13:13:47 +0300 Subject: [PATCH 1/3] Adds better NamedTuple error messages --- mypy/semanal_namedtuple.py | 25 +++++++------ test-data/unit/check-namedtuple.test | 4 +- test-data/unit/semanal-namedtuple.test | 51 ++++++++++++++++++++++---- 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 968a7d502ebe2..d12ff1caa8b73 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -262,16 +262,17 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str Return None if the definition didn't typecheck. """ + type_name = 'NamedTuple' if fullname == 'typing.NamedTuple' else 'namedtuple' # TODO: Share code with check_argument_count in checkexpr.py? args = call.args if len(args) < 2: - self.fail("Too few arguments for namedtuple()", call) + self.fail('Too few arguments for "{0}()"'.format(type_name), call) return None defaults: List[Expression] = [] if len(args) > 2: # Typed namedtuple doesn't support additional arguments. if fullname == 'typing.NamedTuple': - self.fail("Too many arguments for NamedTuple()", call) + self.fail('Too many arguments for "NamedTuple()"', call) return None for i, arg_name in enumerate(call.arg_names[2:], 2): if arg_name == 'defaults': @@ -283,16 +284,16 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str else: self.fail( "List or tuple literal expected as the defaults argument to " - "namedtuple()", + "{0}()".format(type_name), arg ) break if call.arg_kinds[:2] != [ARG_POS, ARG_POS]: - self.fail("Unexpected arguments to namedtuple()", call) + self.fail('Unexpected arguments to "{0}()"'.format(type_name), call) return None if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)): self.fail( - "namedtuple() expects a string literal as the first argument", call) + '"{0}()" expects a string literal as the first argument'.format(type_name), call) return None typename = cast(Union[StrExpr, BytesExpr, UnicodeExpr], call.args[0]).value types: List[Type] = [] @@ -303,7 +304,7 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str items = str_expr.value.replace(',', ' ').split() else: self.fail( - "List or tuple literal expected as the second argument to namedtuple()", call) + 'List or tuple literal expected as the second argument to "{0}()"'.format(type_name), call) return None else: listexpr = args[1] @@ -311,7 +312,7 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str # The fields argument contains just names, with implicit Any types. if any(not isinstance(item, (StrExpr, BytesExpr, UnicodeExpr)) for item in listexpr.items): - self.fail("String literal expected as namedtuple() item", call) + self.fail('String literal expected as "namedtuple()" item', call) return None items = [cast(Union[StrExpr, BytesExpr, UnicodeExpr], item).value for item in listexpr.items] @@ -328,10 +329,10 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str 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: " + self.fail('"{0}()" field names cannot start with an underscore: '.format(type_name) + ', '.join(underscore), call) if len(defaults) > len(items): - self.fail("Too many defaults given in call to namedtuple()", call) + self.fail('Too many defaults given in call to "{0}()"'.format(type_name), call) defaults = defaults[:len(items)] return items, types, defaults, typename, True @@ -347,13 +348,13 @@ def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: C for item in nodes: if isinstance(item, TupleExpr): if len(item.items) != 2: - self.fail("Invalid NamedTuple field definition", item) + self.fail('Invalid "NamedTuple()" field definition', item) return None name, type_node = item.items if isinstance(name, (StrExpr, BytesExpr, UnicodeExpr)): items.append(name.value) else: - self.fail("Invalid NamedTuple() field name", item) + self.fail('Invalid "NamedTuple()" field name', item) return None try: type = expr_to_unanalyzed_type(type_node, self.options, self.api.is_stub_file) @@ -369,7 +370,7 @@ def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: C return [], [], [], False types.append(analyzed) else: - self.fail("Tuple expected as NamedTuple() field", item) + self.fail('Tuple expected as "NamedTuple()" field', item) return None return items, types, [], True diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index d47b069ea45eb..5437508d5ecbc 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -35,7 +35,7 @@ X = namedtuple('X', ('x', 'y')) # type: ignore [case testNamedTupleNoUnderscoreFields] from collections import namedtuple -X = namedtuple('X', 'x, _y, _z') # E: namedtuple() field names cannot start with an underscore: _y, _z +X = namedtuple('X', 'x, _y, _z') # E: "namedtuple()" field names cannot start with an underscore: _y, _z [builtins fixtures/tuple.pyi] [case testNamedTupleAccessingAttributes] @@ -162,7 +162,7 @@ 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() +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: List or tuple literal expected as the defaults argument to namedtuple() # E: Argument "defaults" to "namedtuple" has incompatible type "str"; expected "Optional[Iterable[Any]]" [builtins fixtures/list.pyi] diff --git a/test-data/unit/semanal-namedtuple.test b/test-data/unit/semanal-namedtuple.test index e018763e77121..1c54725bc6770 100644 --- a/test-data/unit/semanal-namedtuple.test +++ b/test-data/unit/semanal-namedtuple.test @@ -145,39 +145,76 @@ MypyFile:1( [case testNamedTupleWithTooFewArguments] from collections import namedtuple -N = namedtuple('N') # E: Too few arguments for namedtuple() +N = namedtuple('N') # E: Too few arguments for "namedtuple()" [builtins fixtures/tuple.pyi] [case testNamedTupleWithInvalidName] from collections import namedtuple -N = namedtuple(1, ['x']) # E: namedtuple() expects a string literal as the first argument +N = namedtuple(1, ['x']) # E: "namedtuple()" expects a string literal as the first argument [builtins fixtures/tuple.pyi] [case testNamedTupleWithInvalidItems] from collections import namedtuple -N = namedtuple('N', 1) # E: List or tuple literal expected as the second argument to namedtuple() +N = namedtuple('N', 1) # E: List or tuple literal expected as the second argument to "namedtuple()" [builtins fixtures/tuple.pyi] [case testNamedTupleWithInvalidItems2] from collections import namedtuple -N = namedtuple('N', ['x', 1]) # E: String literal expected as namedtuple() item +N = namedtuple('N', ['x', 1]) # E: String literal expected as "namedtuple()" item [builtins fixtures/tuple.pyi] [case testNamedTupleWithUnderscoreItemName] from collections import namedtuple -N = namedtuple('N', ['_fallback']) # E: namedtuple() field names cannot start with an underscore: _fallback +N = namedtuple('N', ['_fallback']) # E: "namedtuple()" field names cannot start with an underscore: _fallback [builtins fixtures/tuple.pyi] -- NOTE: The following code works at runtime but is not yet supported by mypy. -- Keyword arguments may potentially be supported in the future. [case testNamedTupleWithNonpositionalArgs] from collections import namedtuple -N = namedtuple(typename='N', field_names=['x']) # E: Unexpected arguments to namedtuple() +N = namedtuple(typename='N', field_names=['x']) # E: Unexpected arguments to "namedtuple()" +[builtins fixtures/tuple.pyi] + +[case testTypingNamedTupleWithTooFewArguments] +from typing import NamedTuple +N = NamedTuple('N') # E: Too few arguments for "NamedTuple()" +[builtins fixtures/tuple.pyi] + +[case testTypingNamedTupleWithManyArguments] +from typing import NamedTuple +N = NamedTuple('N', [], []) # E: Too many arguments for "NamedTuple()" +[builtins fixtures/tuple.pyi] + +[case testTypingNamedTupleWithInvalidName] +from typing import NamedTuple +N = NamedTuple(1, ['x']) # E: "NamedTuple()" expects a string literal as the first argument +[builtins fixtures/tuple.pyi] + +[case testTypingNamedTupleWithInvalidItems] +from typing import NamedTuple +N = NamedTuple('N', 1) # E: List or tuple literal expected as the second argument to "NamedTuple()" +[builtins fixtures/tuple.pyi] + +[case testTypingNamedTupleWithUnderscoreItemName] +from typing import NamedTuple +N = NamedTuple('N', [('_fallback', int)]) # E: "NamedTuple()" field names cannot start with an underscore: _fallback +[builtins fixtures/tuple.pyi] + +[case testTypingNamedTupleWithUnexpectedNames] +from typing import NamedTuple +N = NamedTuple(name='N', fields=[]) # E: Unexpected arguments to "NamedTuple()" +[builtins fixtures/tuple.pyi] + +-- NOTE: The following code works at runtime but is not yet supported by mypy. +-- Keyword arguments may potentially be supported in the future. +[case testNamedTupleWithNonpositionalArgs] +from collections import namedtuple +N = namedtuple(typename='N', field_names=['x']) # E: Unexpected arguments to "namedtuple()" [builtins fixtures/tuple.pyi] [case testInvalidNamedTupleBaseClass] from typing import NamedTuple -class A(NamedTuple('N', [1])): pass # E: Tuple expected as NamedTuple() field +class A(NamedTuple('N', [1])): pass # E: Tuple expected as "NamedTuple()" field class B(A): pass [builtins fixtures/tuple.pyi] From 8fe91ac0461b18b6c1d4fcb24d8a73f09b856997 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 17 Sep 2021 16:55:36 +0300 Subject: [PATCH 2/3] Fixes flake8 --- mypy/semanal_namedtuple.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index d12ff1caa8b73..41556abcae762 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -304,7 +304,11 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str items = str_expr.value.replace(',', ' ').split() else: self.fail( - 'List or tuple literal expected as the second argument to "{0}()"'.format(type_name), call) + 'List or tuple literal expected as the second argument to "{0}()"'.format( + type_name, + ), + call, + ) return None else: listexpr = args[1] From 12d417d837a7714bd6aad93c1e1ab74733a975eb Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Tue, 21 Sep 2021 20:35:48 +0300 Subject: [PATCH 3/3] Update semanal_namedtuple.py --- mypy/semanal_namedtuple.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 41556abcae762..b543057104eca 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -266,7 +266,7 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str # TODO: Share code with check_argument_count in checkexpr.py? args = call.args if len(args) < 2: - self.fail('Too few arguments for "{0}()"'.format(type_name), call) + self.fail('Too few arguments for "{}()"'.format(type_name), call) return None defaults: List[Expression] = [] if len(args) > 2: @@ -284,16 +284,16 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str else: self.fail( "List or tuple literal expected as the defaults argument to " - "{0}()".format(type_name), + "{}()".format(type_name), arg ) break if call.arg_kinds[:2] != [ARG_POS, ARG_POS]: - self.fail('Unexpected arguments to "{0}()"'.format(type_name), call) + self.fail('Unexpected arguments to "{}()"'.format(type_name), call) return None if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)): self.fail( - '"{0}()" expects a string literal as the first argument'.format(type_name), call) + '"{}()" expects a string literal as the first argument'.format(type_name), call) return None typename = cast(Union[StrExpr, BytesExpr, UnicodeExpr], call.args[0]).value types: List[Type] = [] @@ -304,7 +304,7 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str items = str_expr.value.replace(',', ' ').split() else: self.fail( - 'List or tuple literal expected as the second argument to "{0}()"'.format( + 'List or tuple literal expected as the second argument to "{}()"'.format( type_name, ), call, @@ -333,10 +333,10 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str types = [AnyType(TypeOfAny.unannotated) for _ in items] underscore = [item for item in items if item.startswith('_')] if underscore: - self.fail('"{0}()" field names cannot start with an underscore: '.format(type_name) + self.fail('"{}()" field names cannot start with an underscore: '.format(type_name) + ', '.join(underscore), call) if len(defaults) > len(items): - self.fail('Too many defaults given in call to "{0}()"'.format(type_name), call) + self.fail('Too many defaults given in call to "{}()"'.format(type_name), call) defaults = defaults[:len(items)] return items, types, defaults, typename, True