From 1077e651caede1cecf66b5d00a48cde76de4c947 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Mar 2017 12:35:29 -0700 Subject: [PATCH 01/12] methods mostly work (TODO: more tests) --- mypy/semanal.py | 67 ++++++++++++++++------ test-data/unit/check-class-namedtuple.test | 29 +++++++++- 2 files changed, 76 insertions(+), 20 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index ea5a350cdfd51..1548ff769c137 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -151,6 +151,11 @@ FUNCTION_FIRST_PHASE_POSTPONE_SECOND = 1 # Add to symbol table but postpone body FUNCTION_SECOND_PHASE = 2 # Only analyze body +# Matches "_prohibited" in typing.py +NAMEDTUPLE_PROHIBITED_NAMES = ('__new__', '__init__', '__slots__', '__getnewargs__', + '_fields', '_field_defaults', '_field_types', + '_make', '_replace', '_asdict') + class SemanticAnalyzer(NodeVisitor): """Semantically analyze parsed mypy files. @@ -655,15 +660,32 @@ def visit_class_def(self, defn: ClassDef) -> None: self.clean_up_bases_and_infer_type_variables(defn) if self.analyze_typeddict_classdef(defn): return - if self.analyze_namedtuple_classdef(defn): - # just analyze the class body so we catch type errors in default values - self.enter_class(defn) + named_tuple_info = self.analyze_namedtuple_classdef(defn) + if named_tuple_info is not None: + # temporarily clear the names dict so we don't get errors about duplicate names that + # were set in build_namedtuple_typeinfo + nt_names = named_tuple_info.names + named_tuple_info.names = SymbolTable() + + self.bind_class_type_vars(named_tuple_info) + self.enter_class(named_tuple_info) + defn.defs.accept(self) + self.leave_class() + self.unbind_class_type_vars() + + # make sure we didn't use illegal names, then reset the names in the typeinfo + for prohibited in NAMEDTUPLE_PROHIBITED_NAMES: + if prohibited in named_tuple_info.names: + self.fail('Cannot overwrite NamedTuple attribute "{}"'.format(prohibited), + named_tuple_info.names[prohibited].node) + + named_tuple_info.names.update(nt_names) else: self.setup_class_def_analysis(defn) - self.bind_class_type_vars(defn) + self.bind_class_type_vars(defn.info) self.analyze_base_classes(defn) self.analyze_metaclass(defn) @@ -671,7 +693,7 @@ def visit_class_def(self, defn: ClassDef) -> None: for decorator in defn.decorators: self.analyze_class_decorator(defn, decorator) - self.enter_class(defn) + self.enter_class(defn.info) # Analyze class body. defn.defs.accept(self) @@ -683,13 +705,13 @@ def visit_class_def(self, defn: ClassDef) -> None: self.unbind_class_type_vars() - def enter_class(self, defn: ClassDef) -> None: + def enter_class(self, info: TypeInfo) -> None: # Remember previous active class self.type_stack.append(self.type) self.locals.append(None) # Add class scope self.block_depth.append(-1) # The class body increments this to 0 self.postpone_nested_functions_stack.append(FUNCTION_BOTH_PHASES) - self.type = defn.info + self.type = info def leave_class(self) -> None: """ Restore analyzer state. """ @@ -698,14 +720,14 @@ def leave_class(self) -> None: self.locals.pop() self.type = self.type_stack.pop() - def bind_class_type_vars(self, defn: ClassDef) -> None: + def bind_class_type_vars(self, info: TypeInfo) -> None: """ Unbind type variables of previously active class and bind the type variables for the active class. """ if self.bound_tvars: disable_typevars(self.bound_tvars) self.tvar_stack.append(self.bound_tvars) - self.bound_tvars = self.bind_class_type_variables_in_symbol_table(defn.info) + self.bound_tvars = self.bind_class_type_variables_in_symbol_table(info) def unbind_class_type_vars(self) -> None: """ Unbind the active class' type vars and rebind the @@ -882,7 +904,7 @@ def remove_dups(self, tvars: List[T]) -> List[T]: all_tvars.remove(t) return new_tvars - def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool: + def analyze_namedtuple_classdef(self, defn: ClassDef) -> Optional[TypeInfo]: # special case for NamedTuple for base_expr in defn.base_type_exprs: if isinstance(base_expr, RefExpr): @@ -892,15 +914,17 @@ def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool: if node is not None: node.kind = GDEF # TODO in process_namedtuple_definition also applies here items, types, default_items = self.check_namedtuple_classdef(defn) - node.node = self.build_namedtuple_typeinfo( + info = self.build_namedtuple_typeinfo( defn.name, items, types, default_items) - return True - return False + node.node = info + defn.info = info + return info + return None def check_namedtuple_classdef( self, defn: ClassDef) -> Tuple[List[str], List[Type], Dict[str, Expression]]: NAMEDTUP_CLASS_ERROR = ('Invalid statement in NamedTuple definition; ' - 'expected "field_name: field_type"') + '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 [], [], {} @@ -912,9 +936,11 @@ def check_namedtuple_classdef( for stmt in defn.defs.body: if not isinstance(stmt, AssignmentStmt): # Still allow pass or ... (for empty namedtuples). + # Also allow methods. if (not isinstance(stmt, PassStmt) and - not (isinstance(stmt, ExpressionStmt) and - isinstance(stmt.expr, EllipsisExpr))): + not (isinstance(stmt, ExpressionStmt) and + isinstance(stmt.expr, EllipsisExpr)) and + not isinstance(stmt, FuncBase)): self.fail(NAMEDTUP_CLASS_ERROR, stmt) elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr): # An assignment, but an invalid one. @@ -2128,6 +2154,7 @@ def add_field(var: Var, is_initialized_in_class: bool = False, add_field(Var('_field_types', dictype), is_initialized_in_class=True) add_field(Var('_field_defaults', dictype), is_initialized_in_class=True) add_field(Var('_source', strtype), is_initialized_in_class=True) + add_field(Var('__annotations__', ordereddictype), is_initialized_in_class=True) tvd = TypeVarDef('NT', 1, [], info.tuple_type) selftype = TypeVarType(tvd) @@ -3359,7 +3386,7 @@ def visit_class_def(self, cdef: ClassDef) -> None: self.process_nested_classes(cdef) def process_nested_classes(self, outer_def: ClassDef) -> None: - self.sem.enter_class(outer_def) + self.sem.enter_class(outer_def.info) for node in outer_def.defs.body: if isinstance(node, ClassDef): node.info = TypeInfo(SymbolTable(), node, self.sem.cur_mod_id) @@ -3488,8 +3515,10 @@ def visit_func_def(self, fdef: FuncDef) -> None: self.errors.pop_function() def visit_class_def(self, tdef: ClassDef) -> None: - for type in tdef.info.bases: - self.analyze(type) + # NamedTuple base classes are special; we don't have to check them again here + if not tdef.info.is_named_tuple: + for type in tdef.info.bases: + self.analyze(type) # Recompute MRO now that we have analyzed all modules, to pick # up superclasses of bases imported from other modules in an # import loop. (Only do so if we succeeded the first time.) diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index 46cb87d9a018b..856431b6eea58 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -345,7 +345,7 @@ 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" + def f(self): pass [case testNewNamedTupleWithInvalidItems2] # flags: --python-version 3.6 @@ -482,3 +482,30 @@ Y(y=1, x='1').method() class CallsBaseInit(X): def __init__(self, x: str) -> None: super().__init__(x) + +[case testNewNamedTupleWithMethods] +# flags: --python-version 3.6 +from typing import NamedTuple + +class XMeth(NamedTuple): + x: int + def double(self) -> int: + return self.x + +class XRepr(NamedTuple): + x: int + y: int = 1 + def __str__(self) -> str: + return 'string' + def __add__(self, other: XRepr) -> int: + return 0 + +reveal_type(XMeth(1).double()) # E: Revealed type is 'builtins.int' +reveal_type(XMeth(42).x) # E: Revealed type is 'builtins.int' +reveal_type(XRepr(42).__str__()) # E: Revealed type is 'builtins.str' +reveal_type(XRepr(1, 2).__add__(XRepr(3))) # E: Revealed type is 'builtins.int' + +class XMethBad(NamedTuple): + x: int + def _fields(self): # E: Cannot overwrite NamedTuple attribute "_fields" + return 'no chance for this' From 44c6f06ea89c7361120c4c0f949e69cbd47dd51f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Mar 2017 12:47:31 -0700 Subject: [PATCH 02/12] more tests --- test-data/unit/check-class-namedtuple.test | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index 856431b6eea58..203c55f710a2e 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -491,6 +491,8 @@ class XMeth(NamedTuple): x: int def double(self) -> int: return self.x + async def asyncdouble(self) -> int: + return self.x class XRepr(NamedTuple): x: int @@ -501,11 +503,55 @@ class XRepr(NamedTuple): return 0 reveal_type(XMeth(1).double()) # E: Revealed type is 'builtins.int' +reveal_type(XMeth(1).asyncdouble()) # E: Revealed type is 'typing.Awaitable[builtins.int]' reveal_type(XMeth(42).x) # E: Revealed type is 'builtins.int' reveal_type(XRepr(42).__str__()) # E: Revealed type is 'builtins.str' reveal_type(XRepr(1, 2).__add__(XRepr(3))) # E: Revealed type is 'builtins.int' +[case testNewNamedTupleMethodInheritance] +# flags: --python-version 3.6 +from typing import NamedTuple, TypeVar + +T = TypeVar('T') + +class Base(NamedTuple): + x: int + def copy(self: T) -> T: + return self + def good_override(self) -> int: + return self.x + def bad_override(self) -> int: + return self.x + +class Child(Base): + def new_method(self) -> int: + return self.x + def good_override(self) -> int: + return 0 + def bad_override(self) -> str: # E: Return type of "bad_override" incompatible with supertype "Base" + return 'incompatible' + +def takes_base(base: Base) -> int: + return base.x + +reveal_type(Base(1).copy()) # E: Revealed type is 'Tuple[builtins.int, fallback=__main__.Base]' +reveal_type(Child(1).copy()) # E: Revealed type is 'Tuple[builtins.int, fallback=__main__.Child]' +reveal_type(Base(1).good_override()) # E: Revealed type is 'builtins.int' +reveal_type(Child(1).good_override()) # E: Revealed type is 'builtins.int' +reveal_type(Base(1).bad_override()) # E: Revealed type is 'builtins.int' +reveal_type(takes_base(Base(1))) # E: Revealed type is 'builtins.int' +reveal_type(takes_base(Child(1))) # E: Revealed type is 'builtins.int' + +[case testNewNamedTupleIllegalNames] +# flags: --python-version 3.6 +from typing import NamedTuple + class XMethBad(NamedTuple): x: int def _fields(self): # E: Cannot overwrite NamedTuple attribute "_fields" return 'no chance for this' + +class MagicalFields(NamedTuple): + x: int + def __slots__(self) -> None: ... # E: Cannot overwrite NamedTuple attribute "__slots__" + def __new__(cls) -> None: ... # E: Cannot overwrite NamedTuple attribute "__new__" From f00ef4ce048bb09c03873215797ad6a257be7ba6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Mar 2017 13:01:08 -0700 Subject: [PATCH 03/12] add test for __annotations__ --- test-data/unit/check-class-namedtuple.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index 203c55f710a2e..3c6ac54a21e4f 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -296,6 +296,7 @@ class X(NamedTuple): reveal_type(X._fields) # E: Revealed type is 'Tuple[builtins.str, builtins.str]' reveal_type(X._field_types) # E: Revealed type is 'builtins.dict[builtins.str, Any]' reveal_type(X._field_defaults) # E: Revealed type is 'builtins.dict[builtins.str, Any]' +reveal_type(X.__annotations__) # E: Revealed type is 'builtins.dict[builtins.str, Any]' [builtins fixtures/dict.pyi] From a490805b70bc7cbe0382807c3e84a2179626d744 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Mar 2017 14:45:51 -0700 Subject: [PATCH 04/12] address Guido's comments --- mypy/semanal.py | 21 +++++++++++++-------- test-data/unit/check-class-namedtuple.test | 19 +++++++++++++++---- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 1548ff769c137..a1de75d4868b6 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -662,8 +662,8 @@ def visit_class_def(self, defn: ClassDef) -> None: return named_tuple_info = self.analyze_namedtuple_classdef(defn) if named_tuple_info is not None: - # temporarily clear the names dict so we don't get errors about duplicate names that - # were set in build_namedtuple_typeinfo + # Temporarily clear the names dict so we don't get errors about duplicate names that + # were already set in build_namedtuple_typeinfo. nt_names = named_tuple_info.names named_tuple_info.names = SymbolTable() @@ -681,6 +681,8 @@ def visit_class_def(self, defn: ClassDef) -> None: self.fail('Cannot overwrite NamedTuple attribute "{}"'.format(prohibited), named_tuple_info.names[prohibited].node) + # Restore the names in the original symbol table. This ensures that the symbol + # table contains the field objects created by build_namedtuple_typeinfo. named_tuple_info.names.update(nt_names) else: self.setup_class_def_analysis(defn) @@ -936,12 +938,14 @@ def check_namedtuple_classdef( for stmt in defn.defs.body: if not isinstance(stmt, AssignmentStmt): # Still allow pass or ... (for empty namedtuples). + if (isinstance(stmt, PassStmt) or + (isinstance(stmt, ExpressionStmt) and + isinstance(stmt.expr, EllipsisExpr))): + continue # Also allow methods. - if (not isinstance(stmt, PassStmt) and - not (isinstance(stmt, ExpressionStmt) and - isinstance(stmt.expr, EllipsisExpr)) and - not isinstance(stmt, FuncBase)): - self.fail(NAMEDTUP_CLASS_ERROR, stmt) + if isinstance(stmt, FuncBase): + continue + 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) @@ -3515,7 +3519,8 @@ def visit_func_def(self, fdef: FuncDef) -> None: self.errors.pop_function() def visit_class_def(self, tdef: ClassDef) -> None: - # NamedTuple base classes are special; we don't have to check them again here + # NamedTuple base classes are validated in check_namedtuple_classdef; we don't have to + # check them again here. if not tdef.info.is_named_tuple: for type in tdef.info.bases: self.analyze(type) diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index 3c6ac54a21e4f..db6cd8b92adab 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -485,7 +485,6 @@ class CallsBaseInit(X): super().__init__(x) [case testNewNamedTupleWithMethods] -# flags: --python-version 3.6 from typing import NamedTuple class XMeth(NamedTuple): @@ -510,7 +509,6 @@ reveal_type(XRepr(42).__str__()) # E: Revealed type is 'builtins.str' reveal_type(XRepr(1, 2).__add__(XRepr(3))) # E: Revealed type is 'builtins.int' [case testNewNamedTupleMethodInheritance] -# flags: --python-version 3.6 from typing import NamedTuple, TypeVar T = TypeVar('T') @@ -544,8 +542,7 @@ reveal_type(takes_base(Base(1))) # E: Revealed type is 'builtins.int' reveal_type(takes_base(Child(1))) # E: Revealed type is 'builtins.int' [case testNewNamedTupleIllegalNames] -# flags: --python-version 3.6 -from typing import NamedTuple +from typing import Callable, NamedTuple class XMethBad(NamedTuple): x: int @@ -556,3 +553,17 @@ class MagicalFields(NamedTuple): x: int def __slots__(self) -> None: ... # E: Cannot overwrite NamedTuple attribute "__slots__" def __new__(cls) -> None: ... # E: Cannot overwrite NamedTuple attribute "__new__" + +class ReuseNames(NamedTuple): + x: int + def x(self) -> str: # E: Name 'x' already defined + return '' + + def y(self) -> int: + return 0 + y: str # E: Name 'y' already defined + +class ReuseCallableNamed(NamedTuple): + z: Callable[[ReuseNames], int] + def z(self) -> int: # E: Name 'z' already defined + return 0 From 47852a6b0ef69a6771b8b415b51370bc956723fe Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Mar 2017 15:05:23 -0700 Subject: [PATCH 05/12] Also disallow _source and __annotations__ --- mypy/semanal.py | 6 ++++-- test-data/unit/check-class-namedtuple.test | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index a1de75d4868b6..27db26021c3f4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -151,10 +151,12 @@ FUNCTION_FIRST_PHASE_POSTPONE_SECOND = 1 # Add to symbol table but postpone body FUNCTION_SECOND_PHASE = 2 # Only analyze body -# Matches "_prohibited" in typing.py +# Matches "_prohibited" in typing.py, but adds _source, which is allowed but ignored at runtime, and +# __annotations__, which works at runtime but can't easily be supported in a static checker. NAMEDTUPLE_PROHIBITED_NAMES = ('__new__', '__init__', '__slots__', '__getnewargs__', '_fields', '_field_defaults', '_field_types', - '_make', '_replace', '_asdict') + '_make', '_replace', '_asdict', + '_source', '__annotations__') class SemanticAnalyzer(NodeVisitor): diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index db6cd8b92adab..b9a7f49a1bbe2 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -551,8 +551,18 @@ class XMethBad(NamedTuple): class MagicalFields(NamedTuple): x: int - def __slots__(self) -> None: ... # E: Cannot overwrite NamedTuple attribute "__slots__" - def __new__(cls) -> None: ... # E: Cannot overwrite NamedTuple attribute "__new__" + def __slots__(self) -> None: pass # E: Cannot overwrite NamedTuple attribute "__slots__" + def __new__(cls) -> None: pass # E: Cannot overwrite NamedTuple attribute "__new__" + 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" \ + # E: Cannot overwrite NamedTuple attribute "__annotations__" + +class AnnotationsAsAMethod(NamedTuple): + x: int + # This fails at runtime because typing.py assumes that __annotations__ is a dictionary. + def __annotations__(self) -> float: # E: Cannot overwrite NamedTuple attribute "__annotations__" + return 1.0 class ReuseNames(NamedTuple): x: int @@ -567,3 +577,5 @@ class ReuseCallableNamed(NamedTuple): z: Callable[[ReuseNames], int] def z(self) -> int: # E: Name 'z' already defined return 0 + +[builtins fixtures/dict.pyi] From 68d16c0d25eea78d81006af28d1866ee60f5bfc0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Mar 2017 15:46:23 -0700 Subject: [PATCH 06/12] allow docstrings (but not __doc__ assignment) --- mypy/semanal.py | 13 ++++++++++--- test-data/unit/check-class-namedtuple.test | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 27db26021c3f4..41c9609729628 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -151,12 +151,14 @@ FUNCTION_FIRST_PHASE_POSTPONE_SECOND = 1 # Add to symbol table but postpone body FUNCTION_SECOND_PHASE = 2 # Only analyze body -# Matches "_prohibited" in typing.py, but adds _source, which is allowed but ignored at runtime, and -# __annotations__, which works at runtime but can't easily be supported in a static checker. +# Matches "_prohibited" in typing.py, but adds _source, which is allowed but ignored at runtime, +# __annotations__, which works at runtime but can't easily be supported in a static checker, +# and __doc__, which works at runtime but is a bit tricky to implement here and lacks a good use +# case. NAMEDTUPLE_PROHIBITED_NAMES = ('__new__', '__init__', '__slots__', '__getnewargs__', '_fields', '_field_defaults', '_field_types', '_make', '_replace', '_asdict', - '_source', '__annotations__') + '_source', '__annotations__', '__doc__') class SemanticAnalyzer(NodeVisitor): @@ -947,6 +949,10 @@ def check_namedtuple_classdef( # Also allow methods. if isinstance(stmt, FuncBase): continue + # And docstrings. + if (isinstance(stmt, ExpressionStmt) and + isinstance(stmt.expr, StrExpr)): + continue self.fail(NAMEDTUP_CLASS_ERROR, stmt) elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr): # An assignment, but an invalid one. @@ -2161,6 +2167,7 @@ def add_field(var: Var, is_initialized_in_class: bool = False, add_field(Var('_field_defaults', dictype), is_initialized_in_class=True) add_field(Var('_source', strtype), is_initialized_in_class=True) add_field(Var('__annotations__', ordereddictype), is_initialized_in_class=True) + add_field(Var('__doc__', strtype), is_initialized_in_class=True) tvd = TypeVarDef('NT', 1, [], info.tuple_type) selftype = TypeVarType(tvd) diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index b9a7f49a1bbe2..f2c121a4c7fd2 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -579,3 +579,18 @@ class ReuseCallableNamed(NamedTuple): return 0 [builtins fixtures/dict.pyi] + +[case testNewNamedTupleDocString] +from typing import NamedTuple + +class Documented(NamedTuple): + """This is a docstring.""" + x: int + +reveal_type(Documented.__doc__) # E: Revealed type is 'builtins.str' +reveal_type(Documented(1).x) # E: Revealed type is 'builtins.int' + +class BadDoc(NamedTuple): + x: int + def __doc__(self) -> str: # E: Cannot overwrite NamedTuple attribute "__doc__" + return '' From 65077369cffd7df91b64b99f567743979f696edf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Mar 2017 15:51:05 -0700 Subject: [PATCH 07/12] add some tests --- test-data/unit/check-class-namedtuple.test | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index f2c121a4c7fd2..3820347c8d744 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -508,6 +508,22 @@ reveal_type(XMeth(42).x) # E: Revealed type is 'builtins.int' reveal_type(XRepr(42).__str__()) # E: Revealed type is 'builtins.str' reveal_type(XRepr(1, 2).__add__(XRepr(3))) # E: Revealed type is 'builtins.int' +[case testNewNamedTupleOverloading] +from typing import NamedTuple, overload + +class Overloader(NamedTuple): + x: int + @overload + def method(self, y: str) -> str: pass + @overload + def method(self, y: int) -> int: pass + def method(self, y): + return y + +reveal_type(Overloader(1).method('string')) # E: Revealed type is 'builtins.str' +reveal_type(Overloader(1).method(1)) # E: Revealed type is 'builtins.int' +Overloader(1).method(('tuple',)) # E: No overload variant of "method" of "Overloader" matches argument types [Tuple[builtins.str]] + [case testNewNamedTupleMethodInheritance] from typing import NamedTuple, TypeVar @@ -516,14 +532,28 @@ T = TypeVar('T') class Base(NamedTuple): x: int def copy(self: T) -> T: + reveal_type(self) # E: Revealed type is 'T`-1' return self def good_override(self) -> int: + reveal_type(self) # E: Revealed type is 'Tuple[builtins.int, fallback=__main__.Base]' + reveal_type(self[0]) # E: Revealed type is 'builtins.int' + self[0] = 3 # E: Unsupported target for indexed assignment + reveal_type(self.x) # E: Revealed type is 'builtins.int' + self.x = 3 # E: Property "x" defined in "Base" is read-only + self[1] # E: Tuple index out of range + self[T] # E: Tuple index must be an integer literal return self.x def bad_override(self) -> int: return self.x class Child(Base): def new_method(self) -> int: + reveal_type(self) # E: Revealed type is 'Tuple[builtins.int, fallback=__main__.Child]' + reveal_type(self[0]) # E: Revealed type is 'builtins.int' + self[0] = 3 # E: Unsupported target for indexed assignment + reveal_type(self.x) # E: Revealed type is 'builtins.int' + self.x = 3 # E: Property "x" defined in "Child" is read-only + self[1] # E: Tuple index out of range return self.x def good_override(self) -> int: return 0 From 6728123b514e8fa07a03c44d29998c93418fee93 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Mar 2017 18:04:43 -0700 Subject: [PATCH 08/12] allow overriding _source and __doc__ I was testing with an outdated version of typing. Replacing _source does work. I'm also OK with deciding that all of this is too obscure to support and we shouldn't care about special-casing __doc__ and _source. --- mypy/semanal.py | 16 +++++++++------- test-data/unit/check-class-namedtuple.test | 6 ++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 41c9609729628..1842c5014326a 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -151,14 +151,12 @@ FUNCTION_FIRST_PHASE_POSTPONE_SECOND = 1 # Add to symbol table but postpone body FUNCTION_SECOND_PHASE = 2 # Only analyze body -# Matches "_prohibited" in typing.py, but adds _source, which is allowed but ignored at runtime, -# __annotations__, which works at runtime but can't easily be supported in a static checker, -# and __doc__, which works at runtime but is a bit tricky to implement here and lacks a good use -# case. +# Matches "_prohibited" in typing.py, but adds __annotations__, which works at runtime but can't +# easily be supported in a static checker. NAMEDTUPLE_PROHIBITED_NAMES = ('__new__', '__init__', '__slots__', '__getnewargs__', '_fields', '_field_defaults', '_field_types', '_make', '_replace', '_asdict', - '_source', '__annotations__', '__doc__') + '__annotations__') class SemanticAnalyzer(NodeVisitor): @@ -686,8 +684,12 @@ def visit_class_def(self, defn: ClassDef) -> None: named_tuple_info.names[prohibited].node) # Restore the names in the original symbol table. This ensures that the symbol - # table contains the field objects created by build_namedtuple_typeinfo. - named_tuple_info.names.update(nt_names) + # table contains the field objects created by build_namedtuple_typeinfo. Exclude + # _source and __doc__, which can legally be overwritten by the class. + named_tuple_info.names.update({ + key: value for key, value in nt_names.items() + if key not in named_tuple_info.names or key not in ('_source', '__doc__') + }) else: self.setup_class_def_analysis(defn) diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index 3820347c8d744..c2a0a58116529 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -583,7 +583,7 @@ 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 _source(self) -> int: pass # E: Cannot overwrite NamedTuple attribute "_source" + def _source(self) -> int: pass # _source is ok __annotations__ = {'x': float} # E: NamedTuple field name cannot start with an underscore: __annotations__ \ # E: Invalid statement in NamedTuple definition; expected "field_name: field_type" \ # E: Cannot overwrite NamedTuple attribute "__annotations__" @@ -622,5 +622,7 @@ reveal_type(Documented(1).x) # E: Revealed type is 'builtins.int' class BadDoc(NamedTuple): x: int - def __doc__(self) -> str: # E: Cannot overwrite NamedTuple attribute "__doc__" + def __doc__(self) -> str: return '' + +reveal_type(BadDoc(1).__doc__()) # E: Revealed type is 'builtins.str' From a80fe2589b33b0744851498e793da0a0c67a75a5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 30 Mar 2017 20:09:40 -0700 Subject: [PATCH 09/12] disallow _source again --- mypy/semanal.py | 6 +++--- test-data/unit/check-class-namedtuple.test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 1842c5014326a..4e72071f13e0c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -155,7 +155,7 @@ # easily be supported in a static checker. NAMEDTUPLE_PROHIBITED_NAMES = ('__new__', '__init__', '__slots__', '__getnewargs__', '_fields', '_field_defaults', '_field_types', - '_make', '_replace', '_asdict', + '_make', '_replace', '_asdict', '_source', '__annotations__') @@ -685,10 +685,10 @@ def visit_class_def(self, defn: ClassDef) -> None: # Restore the names in the original symbol table. This ensures that the symbol # table contains the field objects created by build_namedtuple_typeinfo. Exclude - # _source and __doc__, which can legally be overwritten by the class. + # __doc__, which can legally be overwritten by the class. named_tuple_info.names.update({ key: value for key, value in nt_names.items() - if key not in named_tuple_info.names or key not in ('_source', '__doc__') + if key not in named_tuple_info.names or key != '__doc__' }) else: self.setup_class_def_analysis(defn) diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index c2a0a58116529..101a6cc96e459 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -583,7 +583,7 @@ 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 _source(self) -> int: pass # _source is ok + 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" \ # E: Cannot overwrite NamedTuple attribute "__annotations__" From ca6a0b5c471f2d417003a4e8ef624625035b60a0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 31 Mar 2017 19:16:39 -0700 Subject: [PATCH 10/12] update error message --- mypy/semanal.py | 2 +- test-data/unit/check-class-namedtuple.test | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 4e72071f13e0c..68ee80e4c2ba9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -932,7 +932,7 @@ def analyze_namedtuple_classdef(self, defn: ClassDef) -> Optional[TypeInfo]: def check_namedtuple_classdef( self, defn: ClassDef) -> Tuple[List[str], List[Type], Dict[str, Expression]]: NAMEDTUP_CLASS_ERROR = ('Invalid statement in NamedTuple definition; ' - 'expected "field_name: field_type"') + 'expected "field_name: field_type [= default]"') if self.options.python_version < (3, 6): self.fail('NamedTuple class syntax is only supported in Python 3.6', defn) return [], [], {} diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index 101a6cc96e459..7bc1030cadbf7 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -345,7 +345,7 @@ from typing import NamedTuple class X(NamedTuple): x: int - y = z = 2 # E: Invalid statement in NamedTuple definition; expected "field_name: field_type" + y = z = 2 # E: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" def f(self): pass [case testNewNamedTupleWithInvalidItems2] @@ -360,8 +360,8 @@ class X(typing.NamedTuple): aa: int [out] -main:6: error: Invalid statement in NamedTuple definition; expected "field_name: field_type" -main:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type" +main:6: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" +main:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" main:7: error: Type cannot be declared in assignment to non-self attribute main:7: error: "int" has no attribute "x" main:9: error: Non-default NamedTuple fields cannot follow default fields @@ -374,7 +374,7 @@ from typing import NamedTuple class X(NamedTuple): x: int - y = 2 # E: Invalid statement in NamedTuple definition; expected "field_name: field_type" + y = 2 # E: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" [case testTypeUsingTypeCNamedTuple] # flags: --python-version 3.6 @@ -585,7 +585,7 @@ class MagicalFields(NamedTuple): def __new__(cls) -> None: pass # E: Cannot overwrite NamedTuple attribute "__new__" 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" \ + # E: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" \ # E: Cannot overwrite NamedTuple attribute "__annotations__" class AnnotationsAsAMethod(NamedTuple): From 138d7807354584489d4fa6a69dd186d3a2a35531 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 29 Apr 2017 23:59:29 -0700 Subject: [PATCH 11/12] fix tests post-merge --- mypy/semanal.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 65056c28abb29..727224c36c206 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -311,7 +311,7 @@ def file_context(self, file_node: MypyFile, fnam: str, options: Options, self.is_stub_file = fnam.lower().endswith('.pyi') self.globals = file_node.names if active_type: - self.enter_class(active_type.defn) + self.enter_class(active_type.defn.info) # TODO: Bind class type vars yield @@ -635,18 +635,16 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: return named_tuple_info = self.analyze_namedtuple_classdef(defn) if named_tuple_info is not None: - # Temporarily clear the names dict so we don't get errors about duplicate names that - # were already set in build_namedtuple_typeinfo. + # Temporarily clear the names dict so we don't get errors about duplicate names + # that were already set in build_namedtuple_typeinfo. nt_names = named_tuple_info.names named_tuple_info.names = SymbolTable() - self.bind_class_type_vars(named_tuple_info) self.enter_class(named_tuple_info) - defn.defs.accept(self) + yield True self.leave_class() - self.unbind_class_type_vars() # make sure we didn't use illegal names, then reset the names in the typeinfo for prohibited in NAMEDTUPLE_PROHIBITED_NAMES: @@ -669,7 +667,7 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: for decorator in defn.decorators: self.analyze_class_decorator(defn, decorator) - self.enter_class(defn) + self.enter_class(defn.info) yield True self.calculate_abstract_status(defn.info) From a03513e02f7c0e8294228c22b77db5071e4a9d32 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 4 May 2017 07:48:28 -0700 Subject: [PATCH 12/12] add tests for classmethod, staticmethod, and property and fix --- mypy/semanal.py | 8 +++-- test-data/unit/check-class-namedtuple.test | 39 ++++++++++++++++++++++ test-data/unit/fixtures/classmethod.pyi | 4 +++ test-data/unit/fixtures/property.pyi | 5 ++- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 727224c36c206..a72b1a75cf7ae 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -639,6 +639,8 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: # that were already set in build_namedtuple_typeinfo. 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__'] self.enter_class(named_tuple_info) @@ -649,6 +651,8 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: # make sure we didn't use illegal names, then reset the names in the typeinfo for prohibited in NAMEDTUPLE_PROHIBITED_NAMES: if prohibited in named_tuple_info.names: + if nt_names.get(prohibited) is named_tuple_info.names[prohibited]: + continue self.fail('Cannot overwrite NamedTuple attribute "{}"'.format(prohibited), named_tuple_info.names[prohibited].node) @@ -869,8 +873,8 @@ def check_namedtuple_classdef( (isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr))): continue - # Also allow methods. - if isinstance(stmt, FuncBase): + # Also allow methods, including decorated ones. + if isinstance(stmt, (Decorator, FuncBase)): continue # And docstrings. if (isinstance(stmt, ExpressionStmt) and diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index 7bc1030cadbf7..5330258621672 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -626,3 +626,42 @@ class BadDoc(NamedTuple): return '' reveal_type(BadDoc(1).__doc__()) # E: Revealed type is 'builtins.str' + +[case testNewNamedTupleClassMethod] +from typing import NamedTuple + +class HasClassMethod(NamedTuple): + x: str + + @classmethod + def new(cls, f: str) -> 'HasClassMethod': + reveal_type(cls) # E: Revealed type is 'def (x: builtins.str) -> Tuple[builtins.str, fallback=__main__.HasClassMethod]' + reveal_type(HasClassMethod) # E: Revealed type is 'def (x: builtins.str) -> Tuple[builtins.str, fallback=__main__.HasClassMethod]' + return cls(x=f) + +[builtins fixtures/classmethod.pyi] + +[case testNewNamedTupleStaticMethod] +from typing import NamedTuple + +class HasStaticMethod(NamedTuple): + x: str + + @staticmethod + def new(f: str) -> 'HasStaticMethod': + return HasStaticMethod(x=f) + +[builtins fixtures/classmethod.pyi] + +[case testNewNamedTupleProperty] +from typing import NamedTuple + +class HasStaticMethod(NamedTuple): + x: str + + @property + def size(self) -> int: + reveal_type(self) # E: Revealed type is 'Tuple[builtins.str, fallback=__main__.HasStaticMethod]' + return 4 + +[builtins fixtures/property.pyi] diff --git a/test-data/unit/fixtures/classmethod.pyi b/test-data/unit/fixtures/classmethod.pyi index 282839dcef28f..6d7f71bd52ff4 100644 --- a/test-data/unit/fixtures/classmethod.pyi +++ b/test-data/unit/fixtures/classmethod.pyi @@ -1,5 +1,7 @@ import typing +_T = typing.TypeVar('_T') + class object: def __init__(self) -> None: pass @@ -20,3 +22,5 @@ class int: class str: pass class bytes: pass class bool: pass + +class tuple(typing.Generic[_T]): pass diff --git a/test-data/unit/fixtures/property.pyi b/test-data/unit/fixtures/property.pyi index b2e747bbbd3e4..994874b93b79d 100644 --- a/test-data/unit/fixtures/property.pyi +++ b/test-data/unit/fixtures/property.pyi @@ -1,5 +1,7 @@ import typing +_T = typing.TypeVar('_T') + class object: def __init__(self) -> None: pass @@ -13,5 +15,6 @@ property = object() # Dummy definition. class int: pass class str: pass class bytes: pass -class tuple: pass class bool: pass + +class tuple(typing.Generic[_T]): pass