diff --git a/mypy/semanal.py b/mypy/semanal.py index ea195b5fe5ca..0551506582ce 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -53,7 +53,7 @@ ImportFrom, ImportAll, Block, LDEF, NameExpr, MemberExpr, IndexExpr, TupleExpr, ListExpr, ExpressionStmt, ReturnStmt, RaiseStmt, AssertStmt, OperatorAssignmentStmt, WhileStmt, - ForStmt, BreakStmt, ContinueStmt, IfStmt, TryStmt, WithStmt, DelStmt, + ForStmt, BreakStmt, ContinueStmt, IfStmt, TryStmt, WithStmt, DelStmt, PassStmt, GlobalDecl, SuperExpr, DictExpr, CallExpr, RefExpr, OpExpr, UnaryExpr, SliceExpr, CastExpr, RevealTypeExpr, TypeApplication, Context, SymbolTable, SymbolTableNode, BOUND_TVAR, UNBOUND_TVAR, ListComprehension, GeneratorExpr, @@ -63,7 +63,7 @@ YieldFromExpr, NamedTupleExpr, NonlocalDecl, SymbolNode, SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr, YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, AwaitExpr, - IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr, + IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr, TempNode, COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES, ) from mypy.visitor import NodeVisitor @@ -547,6 +547,8 @@ def check_function_signature(self, fdef: FuncItem) -> None: def visit_class_def(self, defn: ClassDef) -> None: self.clean_up_bases_and_infer_type_variables(defn) + if self.analyze_namedtuple_classdef(defn): + return self.setup_class_def_analysis(defn) self.bind_class_type_vars(defn) @@ -724,6 +726,56 @@ def analyze_unbound_tvar(self, t: Type) -> Tuple[str, TypeVarExpr]: return unbound.name, cast(TypeVarExpr, sym.node) return None + def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool: + # special case for NamedTuple + for base_expr in defn.base_type_exprs: + if isinstance(base_expr, RefExpr): + base_expr.accept(self) + if base_expr.fullname == 'typing.NamedTuple': + node = self.lookup(defn.name, defn) + if node is not None: + node.kind = GDEF # TODO in process_namedtuple_definition also applies here + items, types = self.check_namedtuple_classdef(defn) + node.node = self.build_namedtuple_typeinfo(defn.name, items, types) + return True + return False + + def check_namedtuple_classdef(self, defn: ClassDef) -> Tuple[List[str], List[Type]]: + NAMEDTUP_CLASS_ERROR = ('Invalid statement in NamedTuple definition; ' + 'expected "field_name: field_type"') + if self.options.python_version < (3, 6): + self.fail('NamedTuple class syntax is only supported in Python 3.6', defn) + return [], [] + if len(defn.base_type_exprs) > 1: + self.fail('NamedTuple should be a single base', defn) + items = [] # type: List[str] + types = [] # type: List[Type] + for stmt in defn.defs.body: + if not isinstance(stmt, AssignmentStmt): + # Still allow pass or ... (for empty namedtuples). + if (not isinstance(stmt, PassStmt) and + not (isinstance(stmt, ExpressionStmt) and + isinstance(stmt.expr, EllipsisExpr))): + self.fail(NAMEDTUP_CLASS_ERROR, stmt) + elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr): + # An assignment, but an invalid one. + self.fail(NAMEDTUP_CLASS_ERROR, stmt) + else: + # Append name and type in this case... + name = stmt.lvalues[0].name + items.append(name) + types.append(AnyType() if stmt.type is None else self.anal_type(stmt.type)) + # ...despite possible minor failures that allow further analyzis. + if name.startswith('_'): + self.fail('NamedTuple field name cannot start with an underscore: {}' + .format(name), stmt) + if stmt.type is None or hasattr(stmt, 'new_syntax') and not stmt.new_syntax: + self.fail(NAMEDTUP_CLASS_ERROR, stmt) + elif not isinstance(stmt.rvalue, TempNode): + # x: int assigns rvalue to TempNode(AnyType()) + self.fail('Right hand side values are not supported in NamedTuple', stmt) + return items, types + def setup_class_def_analysis(self, defn: ClassDef) -> None: """Prepare for the analysis of a class definition.""" if not defn.info: diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 209076ecddf2..c5fe0c6316e4 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -66,6 +66,7 @@ 'check-warnings.test', 'check-async-await.test', 'check-newtype.test', + 'check-class-namedtuple.test', 'check-columns.test', ] diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test new file mode 100644 index 000000000000..d5575fcb8649 --- /dev/null +++ b/test-data/unit/check-class-namedtuple.test @@ -0,0 +1,381 @@ +[case testNewNamedTupleOldPythonVersion] +# flags: --fast-parser --python-version 3.5 +from typing import NamedTuple + +class E(NamedTuple): # E: NamedTuple class syntax is only supported in Python 3.6 + pass + +[case testNewNamedTupleNoUnderscoreFields] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + _y: int # E: NamedTuple field name cannot start with an underscore: _y + _z: int # E: NamedTuple field name cannot start with an underscore: _z + +[case testNewNamedTupleAccessingAttributes] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y: str + +x: X +x.x +x.y +x.z # E: "X" has no attribute "z" + +[case testNewNamedTupleAttributesAreReadOnly] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + +x: X +x.x = 5 # E: Property "x" defined in "X" is read-only +x.y = 5 # E: "X" has no attribute "y" + +class A(X): pass +a: A +a.x = 5 # E: Property "x" defined in "A" is read-only + +[case testNewNamedTupleCreateWithPositionalArguments] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y: str + +x = X(1, '2') +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" + +[case testNewNamedTupleShouldBeSingleBase] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class A: ... +class X(NamedTuple, A): # E: NamedTuple should be a single base + pass + +[case testCreateNewNamedTupleWithKeywordArguments] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y: str + +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='x') # E: Missing positional argument "x" in call to "X" + +[case testNewNamedTupleCreateAndUseAsTuple] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y: str + +x = X(1, 'x') +a, b = x +a, b, c = x # E: Need more than 2 values to unpack (3 expected) + +[case testNewNamedTupleWithItemTypes] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class N(NamedTuple): + a: int + b: str + +n = N(1, 'x') +s: str = n.a # E: Incompatible types in assignment (expression has type "int", \ + variable has type "str") +i: int = n.b # E: Incompatible types in assignment (expression has type "str", \ + variable has type "int") +x, y = n +x = y # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testNewNamedTupleConstructorArgumentTypes] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class N(NamedTuple): + a: int + b: str + +n = N('x', 'x') # E: Argument 1 to "N" has incompatible type "str"; expected "int" +n = N(1, b=2) # E: Argument 2 to "N" has incompatible type "int"; expected "str" +N(1, 'x') +N(b='x', a=1) + +[case testNewNamedTupleAsBaseClass] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class N(NamedTuple): + a: int + b: str + +class X(N): + pass +x = X(1, 2) # E: Argument 2 to "X" has incompatible type "int"; expected "str" +s = '' +i = 0 +s = x.a # E: Incompatible types in assignment (expression has type "int", variable has type "str") +i, s = x +s, s = x # E: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testNewNamedTupleSelfTypeWithNamedTupleAsBase] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class A(NamedTuple): + a: int + b: str + +class B(A): + def f(self, x: int) -> None: + self.f(self.a) + self.f(self.b) # E: Argument 1 to "f" of "B" has incompatible type "str"; expected "int" + i = 0 + s = '' + i, s = self + i, i = self # E: Incompatible types in assignment (expression has type "str", \ + variable has type "int") +[out] +main: note: In member "f" of class "B": + +[case testNewNamedTupleTypeReferenceToClassDerivedFrom] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class A(NamedTuple): + a: int + b: str + +class B(A): + def f(self, x: 'B') -> None: + i = 0 + s = '' + self = x + i, s = x + i, s = x.a, x.b + i, s = x.a, x.a # E: Incompatible types in assignment (expression has type "int", \ + variable has type "str") + i, i = self # E: Incompatible types in assignment (expression has type "str", \ + variable has type "int") + +[out] +main: note: In member "f" of class "B": + +[case testNewNamedTupleSubtyping] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple, Tuple + +class A(NamedTuple): + a: int + b: str + +class B(A): pass +a = A(1, '') +b = B(1, '') +t: Tuple[int, str] +b = a # E: Incompatible types in assignment (expression has type "A", variable has type "B") +a = t # E: Incompatible types in assignment (expression has type "Tuple[int, str]", variable has type "A") +b = t # E: Incompatible types in assignment (expression has type "Tuple[int, str]", variable has type "B") +t = a +t = (1, '') +t = b +a = b + +[case testNewNamedTupleSimpleTypeInference] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple, Tuple + +class A(NamedTuple): + a: int + +l = [A(1), A(2)] +a = A(1) +a = l[0] +(i,) = l[0] +i, i = l[0] # E: Need more than 1 value to unpack (2 expected) +l = [A(1)] +a = (1,) # E: Incompatible types in assignment (expression has type "Tuple[int]", \ + variable has type "A") +[builtins fixtures/list.pyi] + +[case testNewNamedTupleMissingClassAttribute] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class MyNamedTuple(NamedTuple): + a: int + b: str + +MyNamedTuple.x # E: "MyNamedTuple" has no attribute "x" + +[case testNewNamedTupleEmptyItems] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class A(NamedTuple): + ... + +[case testNewNamedTupleForwardRef] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class A(NamedTuple): + b: 'B' + +class B: ... + +a = A(B()) +a = A(1) # E: Argument 1 to "A" has incompatible type "int"; expected "B" + +[case testNewNamedTupleProperty] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class A(NamedTuple): + a: int + +class B(A): + @property + def b(self) -> int: + return self.a +class C(B): pass +B(1).b +C(2).b + +[builtins fixtures/property.pyi] + +[case testNewNamedTupleAsDict] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple, Any + +class X(NamedTuple): + x: Any + y: Any + +x: X +reveal_type(x._asdict()) # E: Revealed type is 'builtins.dict[builtins.str, Any]' + +[builtins fixtures/dict.pyi] + +[case testNewNamedTupleReplaceTyped] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y: str + +x: X +reveal_type(x._replace()) # E: Revealed type is 'Tuple[builtins.int, builtins.str, fallback=__main__.X]' +x._replace(x=5) +x._replace(y=5) # E: Argument 1 to X._replace has incompatible type "int"; expected "str" + +[case testNewNamedTupleFields] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y: str + +reveal_type(X._fields) # E: Revealed type is 'Tuple[builtins.str, builtins.str]' + +[case testNewNamedTupleUnit] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + pass + +x: X = X() +x._replace() +x._fields[0] # E: Tuple index out of range + +[case testNewNamedTupleJoinNamedTuple] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y: str +class Y(NamedTuple): + x: int + y: str + +reveal_type([X(3, 'b'), Y(1, 'a')]) # E: Revealed type is 'builtins.list[Tuple[builtins.int, builtins.str]]' + +[builtins fixtures/list.pyi] + +[case testNewNamedTupleJoinTuple] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y: str + +reveal_type([(3, 'b'), X(1, 'a')]) # E: Revealed type is 'builtins.list[Tuple[builtins.int, builtins.str]]' +reveal_type([X(1, 'a'), (3, 'b')]) # E: Revealed type is 'builtins.list[Tuple[builtins.int, builtins.str]]' + +[builtins fixtures/list.pyi] + +[case testNewNamedTupleWithTooManyArguments] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y = z = 2 # E: Invalid statement in NamedTuple definition; expected "field_name: field_type" + def f(self): pass # E: Invalid statement in NamedTuple definition; expected "field_name: field_type" + +[case testNewNamedTupleWithInvalidItems2] +# flags: --fast-parser --python-version 3.6 +import typing + +class X(typing.NamedTuple): + x: int + y: str = 'y' # E: Right hand side values are not supported in NamedTuple + z = None # type: int # E: Invalid statement in NamedTuple definition; expected "field_name: field_type" + x[0]: int # E: Invalid statement in NamedTuple definition; expected "field_name: field_type" + +[builtins fixtures/list.pyi] + +[case testNewNamedTupleWithoutTypesSpecified] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple + +class X(NamedTuple): + x: int + y = 2 # E: Invalid statement in NamedTuple definition; expected "field_name: field_type" + +[case testTypeUsingTypeCNamedTuple] +# flags: --fast-parser --python-version 3.6 +from typing import NamedTuple, Type + +class N(NamedTuple): + x: int + y: str + +def f(a: Type[N]): + a() +[builtins fixtures/list.pyi] +[out] +main: note: In function "f": +main:8: error: Unsupported type Type["N"]