From c6125f1d62b5bc3a01f501bf0a477c5444fc2677 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 19 Aug 2018 19:43:43 +0100 Subject: [PATCH 01/52] Start adding tests --- test-data/unit/check-final.test | 228 +++++++++++++++++++++++++++++ test-data/unit/diff.test | 16 ++ test-data/unit/fine-grained.test | 2 + test-data/unit/lib-stub/typing.pyi | 3 + test-data/unit/merge.test | 16 ++ 5 files changed, 265 insertions(+) create mode 100644 test-data/unit/check-final.test diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test new file mode 100644 index 000000000000..9fbdf0abc1fe --- /dev/null +++ b/test-data/unit/check-final.test @@ -0,0 +1,228 @@ +-- Test case for final qualifier +-- + +-- Definitions + +[case testFinalDefiningModuleVar] +from typing import Final +x: Final = int() +y: Final[float] = int() +z: Final[int] = int() +bad: Final[str] = str() +reveal_type(x) +reveal_type(y) +reveal_type(z) +[out] + +[case testFinalDefiningInstanceVar] +from typing import Final +class C: + x: Final = int() + y: Final[float] = int() + z: Final[int] = int() + bad: Final[str] = str() +reveal_type(C.x) +reveal_type(C.y) +reveal_type(C.z) +reveal_type(C().x) +reveal_type(C().y) +reveal_type(C().z) +[out] + +[case testFinalDefiningInstanceVarImplicit] +from typing import Final, Tuple, Any + +class C: + def __init__(self, x: Tuple[int, Any]) -> None: + self.x: Final = x + self.y: Final[float] = 1 +reveal_type(C((1, 2)).x) +reveal_type(C((1, 2)).y) +[out] + +[case testFinalDefiningInstanceVarStubs] +# Allow skipping r.h.s. + +[out] + +[case testFinalDefiningFunc] + +[out] + +[case testFinalDefiningFuncOverloaded] + +[out] + +[case testFinalDefiningFuncOverloadedStubs] + +[out] + +[case testFinalDefiningMeth] + +[out] + +[case testFinalDefiningProperty] + +[out] + +[case testFinalDefiningOuterOnly] +from typing import Final, Callable, Tuple +x: Tuple[Final] +y: Callable[[], Tuple[Final[int]]] +[out] + +[case testFinalDefiningNotInMethod] + +[out] + +[case testFinalDefiningNoRhs] +from typing import Final +x: Final +y: Final[int] +class C: + x: Final + y: Final[int] + def __init__(self) -> None: + self.z: Final +reveal_type(x) +reveal_type(y) +reveal_type(C().x) +reveal_type(C().y) +reveal_type(C().z) +[out] + +[case testFinalDefiningNoTypevarsExplicit] +from typing import Final, TypeVar, Generic, Tuple, Any + +T = TypeVar('T') +d: Any + +class C(Generic[T]): + x: Final[Tuple[T, T]] = d +[out] + +[case testFinalDefiningTypevarsImplicit] +from typing import Final, TypeVar, Generic, Tuple, Any + +T = TypeVar('T') + +class C(Generic[T]): + def __init__(self, x: Tuple[T, T]) -> None: + self.x: Final = x + +reveal_type(C((1, 2)).x) +C.x +[out] + +[case testFinalDefiningNotInOtherMethod] +from typing import Final, Any, Tuple + +class C: + def meth(self, x: Tuple[int, Any]) -> None: + self.x: Final = x + self.y: Final[float] = 1 +[out] + +-- Reassignments + +[case testFinalReassignModuleVar] + +[out] + +[case testFinalReassignModuleFunc] + +[out] + +[case testFinalReassignModuleVarExternal] + +[out] + +[case testFinalReassignModuleFuncExternal] + +[out] + +[case testFinalReassignModuleFuncOverloaded] + +[out] + +[case testFinalReassignInstanceVarClassBody] + +[out] + +[case testFinalReassignInstanceVarInit] + +[out] + +[case testFinalReassignInstanceVarMethod] + +[out] + +[case testFinalReassignInstanceVarExternalClass] + +[out] + +[case testFinalReassignInstanceVarExternalInstance] + +[out] + +-- Overriding + +[case testFinalOverridingVarClassBody] + +[out] + +[case testFinalOverridingVarInit] + +[out] + +[case testFinalOverridingVarOtherMethod] + +[out] + +[case testFinalOverridingVarMultipleInheritance] + +[out] + +[case testFinalOverridingVarMultipleInheritance2] + +[out] + +[case testFinalOverridingVarWithMethod] + +[out] + +[case testFinalOverridingMethodWithVar] + +[out] + +[case testFinalOverridingMethodWithVarImplicit] + +[out] + +[case testFinalOverridingMethodMultipleinheritance] + +[out] + +[case testFinalOverridingMethodMultiplaInheritance2] + +[out] + +[case testFinalOverridingClassMethod] + +[out] + +[case testFinalOverridingStaticMethod] + +[out] + +[case testFinalOverridingProperty] + +[out] + +[case testFinalAcessingImplicitVarSubclassObject] + +[out] + +[case testFinalOverridingMethodOverloads] + +[out] diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index d110f8efdb4c..6ca718dccf46 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -944,3 +944,19 @@ class A: [out] __main__.A.(abstract) __main__.A.g + +[case testFinalFlagsTriggerVar] + +[out] + +[case testFinalFlagsTriggerInstanceVar] + +[out] + +[case testFinalFlagsTriggerFunc] + +[out] + +[case testFinalFlagsTriggerMethod] + +[out] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 43233f6d5764..1bbbaca76dd6 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7530,3 +7530,5 @@ x = 1 Func = Callable[..., Any] [out] == + +-- Test cases for final qualifier diff --git a/test-data/unit/lib-stub/typing.pyi b/test-data/unit/lib-stub/typing.pyi index 9c227ec385ea..6bc05958c41f 100644 --- a/test-data/unit/lib-stub/typing.pyi +++ b/test-data/unit/lib-stub/typing.pyi @@ -16,6 +16,7 @@ NamedTuple = 0 Type = 0 no_type_check = 0 ClassVar = 0 +Final = 0 NoReturn = 0 NewType = 0 @@ -55,4 +56,6 @@ class Mapping(Generic[T_contra, T_co]): def runtime(cls: type) -> type: pass +def final(meth: T) -> T: pass + TYPE_CHECKING = 1 diff --git a/test-data/unit/merge.test b/test-data/unit/merge.test index 736fa15bfef7..291197b31e6d 100644 --- a/test-data/unit/merge.test +++ b/test-data/unit/merge.test @@ -1451,3 +1451,19 @@ TypeInfo<0>( X<3> (builtins.int<4>) Y<6> (builtins.int<4>)) MetaclassType(enum.EnumMeta<5>)) + +[case testFinalFlagsUpdatedCorrectlyVar] + +[out] + +[case testFinalFlagsUpdatedCorrectlyInstanceVar] + +[out] + +[case testFinalFlagsUpdatedCorrectlyFunc] + +[out] + +[case testFinalFlagsUpdatedCorrectlyMethod] + +[out] From 8f2bee65a911ce8a20381dc67a3d83cfee034b77 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 19 Aug 2018 23:50:24 +0100 Subject: [PATCH 02/52] Add some flags --- mypy/nodes.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index a508b4256bcb..4db6aed89b83 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -378,7 +378,7 @@ def __str__(self) -> str: FUNCBASE_FLAGS = [ - 'is_property', 'is_class', 'is_static', + 'is_property', 'is_class', 'is_static', 'is_final' ] @@ -390,7 +390,8 @@ class FuncBase(Node): 'info', 'is_property', 'is_class', # Uses "@classmethod" - 'is_static', # USes "@staticmethod" + 'is_static', # Uses "@staticmethod" + 'is_final', # Uses "@final" '_fullname', ) @@ -407,6 +408,7 @@ def __init__(self) -> None: self.is_property = False self.is_class = False self.is_static = False + self.is_final = False # Name with module prefix # TODO: Type should be Optional[str] self._fullname = cast(Bogus[str], None) @@ -442,6 +444,7 @@ def __init__(self, items: List['OverloadPart']) -> None: self.unanalyzed_items = items.copy() self.impl = None self.set_line(items[0].line) + self.is_final = False def name(self) -> str: if self.items: @@ -593,6 +596,7 @@ def __init__(self, self.is_decorated = False self.is_conditional = False # Defined conditionally (within block)? self.is_abstract = False + self.is_final = False # Original conditional definition self.original_def = None # type: Union[None, FuncDef, Var, Decorator] @@ -698,7 +702,7 @@ def deserialize(cls, data: JsonDict) -> 'Decorator': VAR_FLAGS = [ 'is_self', 'is_initialized_in_class', 'is_staticmethod', 'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import', - 'is_classvar', 'is_abstract_var' + 'is_classvar', 'is_abstract_var', 'is_final' ] @@ -722,6 +726,7 @@ class Var(SymbolNode): 'is_settable_property', 'is_classvar', 'is_abstract_var', + 'is_final' 'is_suppressed_import', ) @@ -748,6 +753,8 @@ def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None: # Set to true when this variable refers to a module we were unable to # parse for some reason (eg a silenced module) self.is_suppressed_import = False + # Was this defined as Final[...]? + self.is_final = False def name(self) -> str: return self._name @@ -919,6 +926,8 @@ class AssignmentStmt(Statement): new_syntax = False # type: bool # Does this assignment define a type alias? is_alias_def = False + # Is this a constant definition? + is_final_def = False def __init__(self, lvalues: List[Lvalue], rvalue: Expression, type: 'Optional[mypy.types.Type]' = None, new_syntax: bool = False) -> None: From 61707abd2258cea8c15512adebd1ca7bdf7d247f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 20 Aug 2018 01:25:50 +0100 Subject: [PATCH 03/52] Add semanal logic for (not really) variables --- mypy/semanal.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index d91fcd7093e2..96a98b08430f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1644,9 +1644,45 @@ def add_type_alias_deps(self, aliases_used: Iterable[str], target = self.scope.current_target() self.cur_mod_node.alias_deps[target].update(aliases_used) + def unwrap_final(self, s: AssignmentStmt) -> None: + if not s.type or not self.is_final_type(s.type): + return + assert isinstance(s.type, UnboundType) + if not s.type.args: + s.type = None + else: + s.type = s.type.args[0] + if len(s.type.args) > 1: + self.fail("Final[...] takes at most one type argument", s.type) + if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], RefExpr): + self.fail("Invalid final declaration, ignoring", s) + return + lval = s.lvalues[0] + assert isinstance(lval, RefExpr) + if isinstance(lval, MemberExpr): + if not self.is_self_member_ref(lval): + self.fail("Final can be only applied to a name or an attribute on self", s) + return + else: + assert self.function_stack + if self.function_stack[-1].name() != '__init__': + self.fail("Can only declare final attributes in class body or __init__ method", s) + return + s.is_final_def = True + if self.type and self.type.is_protocol: + self.fail("Protocol members can't be final", s.type) + if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file: + self.fail("Final declaration outside stubs must have right hand side", s) + return + def visit_assignment_stmt(self, s: AssignmentStmt) -> None: + self.unwrap_final(s) + def final_cb() -> None: + self.fail("Final can't redefine an existing name, ignoring", s) + s.is_final_def = False for lval in s.lvalues: - self.analyze_lvalue(lval, explicit_type=s.type is not None) + self.analyze_lvalue(lval, explicit_type=s.type is not None, + final_cb=final_cb if s.is_final_def else None) self.check_classvar(s) s.rvalue.accept(self) if s.type: @@ -1674,6 +1710,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.named_tuple_analyzer.process_namedtuple_definition(s, self.is_func_scope()) self.typed_dict_analyzer.process_typeddict_definition(s, self.is_func_scope()) self.enum_call_analyzer.process_enum_call(s, self.is_func_scope()) + self.store_final_status(s) if not s.type: self.process_module_assignment(s.lvalues, s.rvalue, s) @@ -1682,6 +1719,13 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: isinstance(s.rvalue, (ListExpr, TupleExpr))): self.add_exports(s.rvalue.items) + def store_final_status(self, s: AssignmentStmt) -> None: + if s.is_final_def: + if len(s.lvalues) == 1 and isinstance(s.lvalues[0], RefExpr): + node = s.lvalues[0].node + if isinstance(node, Var): + node.is_final = True + def analyze_simple_literal_type(self, rvalue: Expression) -> Optional[Type]: """Return builtins.int if rvalue is an int literal, etc.""" if self.options.semantic_analysis_only or self.function_stack: @@ -1808,7 +1852,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: def analyze_lvalue(self, lval: Lvalue, nested: bool = False, add_global: bool = False, - explicit_type: bool = False) -> None: + explicit_type: bool = False, + final_cb: Optional[Callable[[], None]] = None) -> None: """Analyze an lvalue or assignment target. Args: @@ -1887,13 +1932,17 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False, original_def = global_def or local_def or type_def self.name_already_defined(lval.name, lval, original_def) + if final_cb is not None: + final_cb() else: # Bind to an existing name. + if final_cb is not None: + final_cb() lval.accept(self) self.check_lvalue_validity(lval.node, lval) elif isinstance(lval, MemberExpr): if not add_global: - self.analyze_member_lvalue(lval, explicit_type) + self.analyze_member_lvalue(lval, explicit_type, final_cb=final_cb) if explicit_type and not self.is_self_member_ref(lval): self.fail('Type cannot be declared in assignment to non-self ' 'attribute', lval) @@ -1931,12 +1980,16 @@ def analyze_tuple_or_list_lvalue(self, lval: TupleExpr, self.analyze_lvalue(i, nested=True, add_global=add_global, explicit_type = explicit_type) - def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False) -> None: + def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False, + final_cb: Optional[Callable[[], None]] = None) -> None: lval.accept(self) if self.is_self_member_ref(lval): assert self.type, "Self member outside a class" cur_node = self.type.names.get(lval.name, None) node = self.type.get(lval.name) + if cur_node and final_cb is not None: + # Overrides will be checked in type checker. + final_cb() # If the attribute of self is not defined in superclasses, create a new Var, ... if ((node is None or isinstance(node.node, Var) and node.node.is_abstract_var) or # ... also an explicit declaration on self also creates a new Var. @@ -2208,6 +2261,14 @@ def is_classvar(self, typ: Type) -> bool: return False return sym.node.fullname() == 'typing.ClassVar' + def is_final_type(self, typ: Type) -> bool: + if not isinstance(typ, UnboundType): + return False + sym = self.lookup_qualified(typ.name, typ) + if not sym or not sym.node: + return False + return sym.node.fullname() == 'typing.Final' + def fail_invalid_classvar(self, context: Context) -> None: self.fail('ClassVar can only be used for assignments in class body', context) From 75ccad313aa9c522dc1bab71724cbca5c936dc9c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 20 Aug 2018 15:03:43 +0100 Subject: [PATCH 04/52] Add checks for re-assignment; self and other fixes; work on tests --- mypy/checker.py | 26 +++++++++++ mypy/nodes.py | 2 +- mypy/semanal.py | 23 +++++---- mypy/test/testcheck.py | 1 + mypy/typeanal.py | 3 ++ test-data/unit/check-final.test | 82 ++++++++++++++++++++++----------- 6 files changed, 100 insertions(+), 37 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5ba197323820..1249bb0afa88 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1586,6 +1586,14 @@ def visit_block(self, b: Block) -> None: break self.accept(s) + def check_final(self, s: AssignmentStmt) -> None: + lvs = self.flatten_lvalues(s.lvalues) + for lv in lvs: + if isinstance(lv, RefExpr) and isinstance(lv.node, Var): + if lv.node.is_final and not s.is_final_def: + name = lv.node.name() + self.fail('Can\'t assign to constant "{}"'.format(name), s) + def visit_assignment_stmt(self, s: AssignmentStmt) -> None: """Type check an assignment statement. @@ -1614,6 +1622,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: for lv in s.lvalues[:-1]: self.check_assignment(lv, rvalue, s.type is None) + self.check_final(s) + def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type: bool = True, new_syntax: bool = False) -> None: """Type check a single assignment: lvalue = rvalue.""" @@ -1722,6 +1732,12 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[ # Show only one error per variable break + if not self.check_compatibility_final_super(lvalue_node, + base, + tnode.node): + # Show only one error per variable + break + for base in lvalue_node.info.mro[1:]: # Only check __slots__ against the 'object' # If a base class defines a Tuple of 3 elements, a child of @@ -1853,6 +1869,16 @@ def check_compatibility_classvar_super(self, node: Var, return False return True + def check_compatibility_final_super(self, node: Var, + base: TypeInfo, base_node: Optional[Node]) -> bool: + if not isinstance(base_node, Var): + return True + if base_node.is_final: + self.fail('Cannot override constant ' + '(previously declared on base class "%s") ' % base.name(), node) + return False + return True + def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression, context: Context, infer_lvalue_type: bool = True) -> None: diff --git a/mypy/nodes.py b/mypy/nodes.py index 4db6aed89b83..ae786bc038e9 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -726,7 +726,7 @@ class Var(SymbolNode): 'is_settable_property', 'is_classvar', 'is_abstract_var', - 'is_final' + 'is_final', 'is_suppressed_import', ) diff --git a/mypy/semanal.py b/mypy/semanal.py index 96a98b08430f..4c015561ddda 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1648,32 +1648,38 @@ def unwrap_final(self, s: AssignmentStmt) -> None: if not s.type or not self.is_final_type(s.type): return assert isinstance(s.type, UnboundType) + if len(s.type.args) > 1: + self.fail("Final[...] takes at most one type argument", s.type) if not s.type.args: s.type = None else: s.type = s.type.args[0] - if len(s.type.args) > 1: - self.fail("Final[...] takes at most one type argument", s.type) if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], RefExpr): self.fail("Invalid final declaration, ignoring", s) return lval = s.lvalues[0] assert isinstance(lval, RefExpr) + s.is_final_def = True + if self.type and self.type.is_protocol: + self.fail("Protocol members can't be final", s) + if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file: + self.fail("Final declaration outside stubs must have right hand side", s) + return + + def check_final_implicit_def(self, s: AssignmentStmt) -> None: + lval = s.lvalues[0] + assert isinstance(lval, RefExpr) if isinstance(lval, MemberExpr): if not self.is_self_member_ref(lval): self.fail("Final can be only applied to a name or an attribute on self", s) + s.is_final_def = False return else: assert self.function_stack if self.function_stack[-1].name() != '__init__': self.fail("Can only declare final attributes in class body or __init__ method", s) + s.is_final_def = False return - s.is_final_def = True - if self.type and self.type.is_protocol: - self.fail("Protocol members can't be final", s.type) - if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file: - self.fail("Final declaration outside stubs must have right hand side", s) - return def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.unwrap_final(s) @@ -1683,6 +1689,7 @@ def final_cb() -> None: for lval in s.lvalues: self.analyze_lvalue(lval, explicit_type=s.type is not None, final_cb=final_cb if s.is_final_def else None) + self.check_final_implicit_def(s) self.check_classvar(s) s.rvalue.accept(self) if s.type: diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 10202fe99eab..ad15179cd684 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -75,6 +75,7 @@ 'check-default-plugin.test', 'check-attr.test', 'check-dataclasses.test', + 'check-final.test', ] diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 6131058070f2..9f622243ba93 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -228,6 +228,9 @@ def visit_unbound_type_nonoptional(self, t: UnboundType) -> Type: return NoneTyp() elif fullname == 'typing.Any' or fullname == 'builtins.Any': return AnyType(TypeOfAny.explicit) + elif fullname == 'typing.Final': + self.fail("Final can be only used as an outermost type qualifier in assignments", t) + return AnyType(TypeOfAny.from_error) elif fullname == 'typing.Tuple': if len(t.args) == 0 and not t.empty_tuple_index: # Bare 'Tuple' is same as 'tuple' diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 9fbdf0abc1fe..fdc38380422c 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -9,9 +9,9 @@ x: Final = int() y: Final[float] = int() z: Final[int] = int() bad: Final[str] = str() -reveal_type(x) -reveal_type(y) -reveal_type(z) +reveal_type(x) # E: Revealed type is 'builtins.int' +reveal_type(y) # E: Revealed type is 'builtins.float' +reveal_type(z) # E: Revealed type is 'builtins.int' [out] [case testFinalDefiningInstanceVar] @@ -20,13 +20,13 @@ class C: x: Final = int() y: Final[float] = int() z: Final[int] = int() - bad: Final[str] = str() -reveal_type(C.x) -reveal_type(C.y) -reveal_type(C.z) -reveal_type(C().x) -reveal_type(C().y) -reveal_type(C().z) + bad: Final[str] = int() # E: Incompatible types in assignment (expression has type "int", variable has type "str") +reveal_type(C.x) # E: Revealed type is 'builtins.int' +reveal_type(C.y) # E: Revealed type is 'builtins.float' +reveal_type(C.z) # E: Revealed type is 'builtins.int' +reveal_type(C().x) # E: Revealed type is 'builtins.int' +reveal_type(C().y) # E: Revealed type is 'builtins.float' +reveal_type(C().z) # E: Revealed type is 'builtins.int' [out] [case testFinalDefiningInstanceVarImplicit] @@ -36,13 +36,22 @@ class C: def __init__(self, x: Tuple[int, Any]) -> None: self.x: Final = x self.y: Final[float] = 1 -reveal_type(C((1, 2)).x) -reveal_type(C((1, 2)).y) +reveal_type(C((1, 2)).x) # E: Revealed type is 'Tuple[builtins.int, Any]' +reveal_type(C((1, 2)).y) # E: Revealed type is 'builtins.float' [out] [case testFinalDefiningInstanceVarStubs] # Allow skipping r.h.s. - +import mod +[file mod.pyi] +from typing import Final +x: Final +y: Final[int] +class C: + x: Final + y: Final[int] + def __init__(self) -> None: + self.z: Final [out] [case testFinalDefiningFunc] @@ -66,29 +75,32 @@ reveal_type(C((1, 2)).y) [out] [case testFinalDefiningOuterOnly] -from typing import Final, Callable, Tuple -x: Tuple[Final] -y: Callable[[], Tuple[Final[int]]] +from typing import Final, Callable, Tuple, Any +x: Tuple[Final] # E: Final can be only used as an outermost type qualifier in assignments +y: Callable[[], Tuple[Final[int]]] # E: Final can be only used as an outermost type qualifier in assignments [out] [case testFinalDefiningNotInMethod] +from typing import Final +def f(x: Final[int]) -> int: ... # E: Final can be only used as an outermost type qualifier in assignments +def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost type qualifier in assignments [out] [case testFinalDefiningNoRhs] from typing import Final -x: Final -y: Final[int] +x: Final # E: Final declaration outside stubs must have right hand side +y: Final[int] # E: Final declaration outside stubs must have right hand side class C: - x: Final - y: Final[int] + x: Final # E: Final declaration outside stubs must have right hand side + y: Final[int] # E: Final declaration outside stubs must have right hand side def __init__(self) -> None: - self.z: Final -reveal_type(x) -reveal_type(y) -reveal_type(C().x) -reveal_type(C().y) -reveal_type(C().z) + self.z: Final # E: Final declaration outside stubs must have right hand side +reveal_type(x) # E: Revealed type is 'Any' +reveal_type(y) # E: Revealed type is 'builtins.int' +reveal_type(C().x) # E: Revealed type is 'Any' +reveal_type(C().y) # E: Revealed type is 'builtins.int' +reveal_type(C().z) # E: Revealed type is 'Any' [out] [case testFinalDefiningNoTypevarsExplicit] @@ -119,8 +131,22 @@ from typing import Final, Any, Tuple class C: def meth(self, x: Tuple[int, Any]) -> None: - self.x: Final = x - self.y: Final[float] = 1 + self.x: Final = x # E: Can only declare final attributes in class body or __init__ method + self.y: Final[float] = 1 # E: Can only declare final attributes in class body or __init__ method +[out] + +[case testFinalDefiningOnlyOnSelf] +from typing import Final, Any, Tuple + +class U: + x: Any + y: Any +class C: + def __init__(self, x: Tuple[int, Any]) -> None: + slf = U() + slf.x: Final = x # E: Final can be only applied to a name or an attribute on self + slf.y: Final[float] = 1 # E: Type cannot be declared in assignment to non-self attribute \ + # E: Final can be only applied to a name or an attribute on self [out] -- Reassignments From 84ac046d181ffa6440216d9d026f1e49ee239581 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 20 Aug 2018 15:45:25 +0100 Subject: [PATCH 05/52] Tests for invalid declarations --- mypy/semanal.py | 2 ++ test-data/unit/check-final.test | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index 4c015561ddda..c3d927f61086 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1667,6 +1667,8 @@ def unwrap_final(self, s: AssignmentStmt) -> None: return def check_final_implicit_def(self, s: AssignmentStmt) -> None: + if not s.is_final_def: + return lval = s.lvalues[0] assert isinstance(lval, RefExpr) if isinstance(lval, MemberExpr): diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index fdc38380422c..29bc92589a0e 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -40,6 +40,27 @@ reveal_type(C((1, 2)).x) # E: Revealed type is 'Tuple[builtins.int, Any]' reveal_type(C((1, 2)).y) # E: Revealed type is 'builtins.float' [out] +[case testFinalBadDefinitionTooManyArgs] +from typing import Final + +x: Final[int, str] # E: Final declaration outside stubs must have right hand side \ + # E: Final[...] takes at most one type argument +reveal_type(x) # E: Revealed type is 'builtins.int' + +class C: + def __init__(self) -> None: + self.x: Final[float, float] = 1 # E: Final[...] takes at most one type argument +reveal_type(C().x) # E: Revealed type is 'builtins.float' +[out] + +[case testFinalInvalidDefinitions] +from typing import Final, Any +x = y = 1 # type: Final[float] # E: Invalid final declaration, ignoring +z: Any +z[0]: Final # E: Unexpected type declaration \ + # E: Invalid final declaration, ignoring +[out] + [case testFinalDefiningInstanceVarStubs] # Allow skipping r.h.s. import mod From 395cbd50107d4f5a32d7b6b0edbd923e7e930062 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 20 Aug 2018 17:39:20 +0100 Subject: [PATCH 06/52] Move the logic for MemberExpr to checkmember.py; more tests --- mypy/checker.py | 17 ++++++++- mypy/checkmember.py | 5 +++ test-data/unit/check-final.test | 65 ++++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1249bb0afa88..9c210b9e5669 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -217,6 +217,7 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option # If True, process function definitions. If False, don't. This is used # for processing module top levels in fine-grained incremental mode. self.recurse_into_functions = True + self._is_final_def = False def reset(self) -> None: """Cleanup stale state that might be left over from a typechecking run. @@ -1586,7 +1587,20 @@ def visit_block(self, b: Block) -> None: break self.accept(s) + def get_final_context(self) -> bool: + return self._is_final_def + + @contextmanager + def set_final_context(self, is_final_def: bool) -> Iterator[None]: + old_ctx = self._is_final_def + self._is_final_def = is_final_def + try: + yield + finally: + self._is_final_def = old_ctx + def check_final(self, s: AssignmentStmt) -> None: + # for class body & modules, other in checkmember lvs = self.flatten_lvalues(s.lvalues) for lv in lvs: if isinstance(lv, RefExpr) and isinstance(lv.node, Var): @@ -1599,7 +1613,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: Handle all kinds of assignment statements (simple, indexed, multiple). """ - self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax) + with self.set_final_context(s.is_final_def): + self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax) if (s.type is not None and self.options.disallow_any_unimported and diff --git a/mypy/checkmember.py b/mypy/checkmember.py index ce978e4b92a3..76ab36e59b79 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -397,6 +397,11 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont if implicit is True, the original Var was created as an assignment to self """ # Found a member variable. + + if is_lvalue and var.is_final and not chk.get_final_context(): + name = var.name() + msg.fail('Can\'t assign to constant "{}"'.format(name), node) + itype = map_instance_to_supertype(itype, var.info) typ = var.type if typ: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 29bc92589a0e..96e9f4444339 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -173,15 +173,49 @@ class C: -- Reassignments [case testFinalReassignModuleVar] +from typing import Final + +x: Final = 1 +x = 2 # E: Can't assign to constant "x" +def f() -> int: + global x + x = 3 # E: Can't assign to constant "x" + return x + +y = 1 +y: Final = 2 # E: Name 'y' already defined on line 10 \ + # E: Final can't redefine an existing name, ignoring +y = 3 # No error here, first definition wins +z: Final = 1 +z: Final = 2 # E: Name 'z' already defined on line 14 \ + # E: Final can't redefine an existing name, ignoring \ + # E: Can't assign to constant "z" +z = 3 # E: Can't assign to constant "z" [out] -[case testFinalReassignModuleFunc] +[case testFinalReassignFuncScope] +from typing import Final + +def f() -> None: + nl: Final = 0 + x: Final = 1 + x = 1 # E: Can't assign to constant "x" + y: Final = 1 + y: Final = 2 # E: Final can't redefine an existing name, ignoring \ + # E: Can't assign to constant "y" + def nested() -> None: + nonlocal nl + nl = 1 # E: Can't assign to constant "nl" [out] [case testFinalReassignModuleVarExternal] - +import mod +mod.x = 2 # E: Can't assign to constant "x" +[file mod.pyi] +from typing import Final +x: Final[int] [out] [case testFinalReassignModuleFuncExternal] @@ -193,10 +227,29 @@ class C: [out] [case testFinalReassignInstanceVarClassBody] +from typing import Final + +class C: + x: Final = 1 + x = 2 # E: Can't assign to constant "x" + y = 1 + y: Final = 2 # E: Final can't redefine an existing name, ignoring [out] [case testFinalReassignInstanceVarInit] +from typing import Final + +class C: + def __init__(self) -> None: + self.x: Final = 1 + self.y = 1 + self.y: Final = 2 # E: Final can't redefine an existing name, ignoring + def meth(self) -> None: + self.x = 2 # E: Can't assign to constant "x" +[out] + +[case testFinalReassignInstanceVarClassVsInit] [out] @@ -212,6 +265,14 @@ class C: [out] +[case testFinalReassignInstanceVarExternalSubclassClass] + +[out] + +[case testFinalReassignInstanceVarExternalSubclassInstance] + +[out] + -- Overriding [case testFinalOverridingVarClassBody] From 67c76bfa8cc3b7fce57d348942cacb220f04ea71 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 20 Aug 2018 18:59:24 +0100 Subject: [PATCH 07/52] Implement special access on class objects --- mypy/checker.py | 3 +++ mypy/checkmember.py | 3 +++ test-data/unit/check-final.test | 9 ++++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9c210b9e5669..303bd8794d36 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1638,6 +1638,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.check_assignment(lv, rvalue, s.type is None) self.check_final(s) + if (s.is_final_def and s.type and not has_no_typevars(s.type) + and self.scope.active_class() is not None): + self.fail("Constant declared in class body can't depend on type variables", s) def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type: bool = True, new_syntax: bool = False) -> None: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 76ab36e59b79..70af8d74b06a 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -540,6 +540,9 @@ def analyze_class_attribute_access(itype: Instance, if isinstance(node.node, TypeInfo): msg.fail(messages.CANNOT_ASSIGN_TO_TYPE, context) + if node.implicit and isinstance(node.node, Var) and node.node.is_final: + msg.fail("Can't access intsance constant on class object", context) + if itype.type.is_enum and not (is_lvalue or is_decorated or is_method): return itype diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 96e9f4444339..89e9e3290066 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -131,7 +131,7 @@ T = TypeVar('T') d: Any class C(Generic[T]): - x: Final[Tuple[T, T]] = d + x: Final[Tuple[T, T]] = d # E: Constant declared in class body can't depend on type variables [out] [case testFinalDefiningTypevarsImplicit] @@ -142,9 +142,12 @@ T = TypeVar('T') class C(Generic[T]): def __init__(self, x: Tuple[T, T]) -> None: self.x: Final = x + self.y: Final = 1 -reveal_type(C((1, 2)).x) -C.x +reveal_type(C((1, 2)).x) # E: Revealed type is 'Tuple[builtins.int*, builtins.int*]' +C.x # E: Can't access intsance constant on class object \ + # E: Access to generic instance variables via class is ambiguous +C.y # E: Can't access intsance constant on class object [out] [case testFinalDefiningNotInOtherMethod] From f6b173e1526d88932d5da774ee4067efb7c7d5da Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 20 Aug 2018 19:25:48 +0100 Subject: [PATCH 08/52] Fix class re-assignment; more tests --- mypy/checkmember.py | 4 +++ test-data/unit/check-final.test | 48 +++++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 70af8d74b06a..bbd9e16e0dd4 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -542,6 +542,10 @@ def analyze_class_attribute_access(itype: Instance, if node.implicit and isinstance(node.node, Var) and node.node.is_final: msg.fail("Can't access intsance constant on class object", context) + if (isinstance(node.node, Var) and is_lvalue and node.node.is_final + and not chk.get_final_context()): + name = node.node.name() + msg.fail('Can\'t assign to constant "{}"'.format(name), context) if itype.type.is_enum and not (is_lvalue or is_decorated or is_method): return itype diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 89e9e3290066..1e9a4d41f31c 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -253,27 +253,65 @@ class C: [out] [case testFinalReassignInstanceVarClassVsInit] +from typing import Final +class C: + y: Final = 1 + def __init__(self) -> None: + self.x: Final = 1 + self.y = 2 # E: Can't assign to constant "y" + x = 2 # E: Can't assign to constant "x" [out] [case testFinalReassignInstanceVarMethod] +from typing import Final +class C: + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 + def meth(self) -> None: + self.x = 2 # E: Can't assign to constant "x" + self.y = 2 # E: Can't assign to constant "y" + def other(self) -> None: + self.x = 2 # E: Can't assign to constant "x" + self.y = 2 # E: Can't assign to constant "y" + @classmethod + def cm(cls) -> None: + cls.x = 2 # E: Can't assign to constant "x" + cls.y # E: Can't access intsance constant on class object +[builtins fixtures/classmethod.pyi] [out] [case testFinalReassignInstanceVarExternalClass] +from typing import Final -[out] +class C: + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 -[case testFinalReassignInstanceVarExternalInstance] +class D(C): pass +C.x = 2 # E: Can't assign to constant "x" +D.x = 2 # E: Can't assign to constant "x" +D.y = 2 # E: Can't access intsance constant on class object \ + # E: Can't assign to constant "y" [out] -[case testFinalReassignInstanceVarExternalSubclassClass] +[case testFinalReassignInstanceVarExternalInstance] +from typing import Final -[out] +class C: + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 -[case testFinalReassignInstanceVarExternalSubclassInstance] +class D(C): pass +C().x = 2 # E: Can't assign to constant "x" +D().x = 2 # E: Can't assign to constant "x" +D().y = 2 # E: Can't assign to constant "y" [out] -- Overriding From 50539023dc77df1e5077c790f51c47728c557cd2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 20 Aug 2018 22:02:15 +0100 Subject: [PATCH 09/52] Create new Var with bare Final on self; search all bases for final; tests --- mypy/checker.py | 11 +++- mypy/checkmember.py | 24 +++++--- mypy/semanal.py | 2 +- test-data/unit/check-final.test | 104 +++++++++++++++++++++++++++++--- 4 files changed, 121 insertions(+), 20 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 303bd8794d36..77ed65b4fe45 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1604,8 +1604,17 @@ def check_final(self, s: AssignmentStmt) -> None: lvs = self.flatten_lvalues(s.lvalues) for lv in lvs: if isinstance(lv, RefExpr) and isinstance(lv.node, Var): + name = lv.node.name() + cls = self.scope.active_class() + if cls is not None: + # This exist to give more errors even if we overriden with a new var + # (which is itself an error) + for base in cls.mro: + sym = base.names.get(name) + if sym and isinstance(sym.node, Var): + if sym.node.is_final and not s.is_final_def: + self.fail('Can\'t assign to constant "{}"'.format(name), s) if lv.node.is_final and not s.is_final_def: - name = lv.node.name() self.fail('Can\'t assign to constant "{}"'.format(name), s) def visit_assignment_stmt(self, s: AssignmentStmt) -> None: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index bbd9e16e0dd4..14ae82c1ea1c 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -262,6 +262,13 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, if isinstance(v, Var): implicit = info[name].implicit + + if is_lvalue and not chk.get_final_context(): + for base in info.mro: + sym = base.names.get(name) + if sym and isinstance(sym.node, Var) and sym.node.is_final: + msg.fail('Can\'t assign to constant "{}"'.format(name), node) + return analyze_var(name, v, itype, info, node, is_lvalue, msg, original_type, builtin_type, not_ready_callback, chk=chk, implicit=implicit) @@ -397,11 +404,6 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont if implicit is True, the original Var was created as an assignment to self """ # Found a member variable. - - if is_lvalue and var.is_final and not chk.get_final_context(): - name = var.name() - msg.fail('Can\'t assign to constant "{}"'.format(name), node) - itype = map_instance_to_supertype(itype, var.info) typ = var.type if typ: @@ -541,11 +543,13 @@ def analyze_class_attribute_access(itype: Instance, msg.fail(messages.CANNOT_ASSIGN_TO_TYPE, context) if node.implicit and isinstance(node.node, Var) and node.node.is_final: - msg.fail("Can't access intsance constant on class object", context) - if (isinstance(node.node, Var) and is_lvalue and node.node.is_final - and not chk.get_final_context()): - name = node.node.name() - msg.fail('Can\'t assign to constant "{}"'.format(name), context) + msg.fail("Can't access instance constant on class object", context) + for base in itype.type.mro: + b_node = base.names.get(name) + if (b_node and isinstance(b_node.node, Var) and is_lvalue and b_node.node.is_final + and not chk.get_final_context()): + name = b_node.node.name() + msg.fail('Can\'t assign to constant "{}"'.format(name), context) if itype.type.is_enum and not (is_lvalue or is_decorated or is_method): return itype diff --git a/mypy/semanal.py b/mypy/semanal.py index c3d927f61086..882048abb2dc 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2002,7 +2002,7 @@ def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False, # If the attribute of self is not defined in superclasses, create a new Var, ... if ((node is None or isinstance(node.node, Var) and node.node.is_abstract_var) or # ... also an explicit declaration on self also creates a new Var. - (cur_node is None and explicit_type)): + (cur_node is None and (explicit_type or final_cb is not None))): if self.type.is_protocol and node is None: self.fail("Protocol members cannot be defined via assignment to self", lval) else: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 1e9a4d41f31c..b74c67eb5b13 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -145,9 +145,9 @@ class C(Generic[T]): self.y: Final = 1 reveal_type(C((1, 2)).x) # E: Revealed type is 'Tuple[builtins.int*, builtins.int*]' -C.x # E: Can't access intsance constant on class object \ +C.x # E: Can't access instance constant on class object \ # E: Access to generic instance variables via class is ambiguous -C.y # E: Can't access intsance constant on class object +C.y # E: Can't access instance constant on class object [out] [case testFinalDefiningNotInOtherMethod] @@ -279,7 +279,7 @@ class C: @classmethod def cm(cls) -> None: cls.x = 2 # E: Can't assign to constant "x" - cls.y # E: Can't access intsance constant on class object + cls.y # E: Can't access instance constant on class object [builtins fixtures/classmethod.pyi] [out] @@ -295,7 +295,7 @@ class D(C): pass C.x = 2 # E: Can't assign to constant "x" D.x = 2 # E: Can't assign to constant "x" -D.y = 2 # E: Can't access intsance constant on class object \ +D.y = 2 # E: Can't access instance constant on class object \ # E: Can't assign to constant "y" [out] @@ -317,26 +317,118 @@ D().y = 2 # E: Can't assign to constant "y" -- Overriding [case testFinalOverridingVarClassBody] +from typing import Final + +class A: + x = 0 + def __init__(self) -> None: + self.y = 0 +class B(A): + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 +class C(B): + x: int = 2 # E: Cannot override constant (previously declared on base class "B") \ + # E: Can't assign to constant "x" + y: int = 2 # E: Cannot override constant (previously declared on base class "B") \ + # E: Can't assign to constant "y" + x = 3 # E: Can't assign to constant "x" + y = 3 # E: Can't assign to constant "y" +class D(C): + pass +D.x = 4 # E: Can't assign to constant "x" +D.y = 4 # E: Can't assign to constant "y" +[out] + +[case testFinalOverridingVarClassBodyExplicit] +from typing import Final +class A: + x = 0 + def __init__(self) -> None: + self.y = 0 +class B(A): + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 +class C(B): + x: Final = 2 # E: Cannot override constant (previously declared on base class "B") + y: Final = 2 # E: Cannot override constant (previously declared on base class "B") [out] [case testFinalOverridingVarInit] +from typing import Final +class A: + x = 0 + def __init__(self) -> None: + self.y = 0 +class B(A): + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 +class C(B): + def __init__(self) -> None: + self.x = 2 # E: Can't assign to constant "x" + self.y = 2 # E: Can't assign to constant "y" + def meth(self) -> None: + self.x = 3 # E: Can't assign to constant "x" + self.y = 3 # E: Can't assign to constant "y" +[out] + +[case testFinalOverridingVarInit2] +from typing import Final + +class A: + x = 0 + def __init__(self) -> None: + self.y = 0 +class B(A): + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 +class C(B): + def __init__(self) -> None: + self.x: Final = 2 # E: Cannot override constant (previously declared on base class "B") + self.y: Final = 2 # E: Cannot override constant (previously declared on base class "B") [out] [case testFinalOverridingVarOtherMethod] +from typing import Final +class A: + x = 0 + def __init__(self) -> None: + self.y = 0 +class B(A): + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 +class C(B): + def meth(self) -> None: + self.x: int = 2 # E: Can't assign to constant "x" \ + # E: Cannot override constant (previously declared on base class "B") + self.y: int = 2 # E: Can't assign to constant "y" \ + # E: Cannot override constant (previously declared on base class "B") + self.x = 3 # E: Can't assign to constant "x" + self.y = 3 # E: Can't assign to constant "y" [out] [case testFinalOverridingVarMultipleInheritance] +from typing import Final + [out] [case testFinalOverridingVarMultipleInheritance2] +from typing import Final + [out] [case testFinalOverridingVarWithMethod] +from typing import Final + [out] @@ -368,10 +460,6 @@ D().y = 2 # E: Can't assign to constant "y" [out] -[case testFinalAcessingImplicitVarSubclassObject] - -[out] - [case testFinalOverridingMethodOverloads] [out] From 8a4ed3721743bffaa310ba7578a7ec5ecb8f492a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 20 Aug 2018 22:51:32 +0100 Subject: [PATCH 10/52] Multiple inheritance; fix lint --- mypy/checker.py | 4 +++ mypy/semanal.py | 5 +++- mypy/typeanal.py | 3 ++- test-data/unit/check-final.test | 43 ++++++++++++++++++++++++++++++--- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 77ed65b4fe45..33545075749f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1545,6 +1545,10 @@ def check_compatibility(self, name: str, base1: TypeInfo, if second_type is None: self.msg.cannot_determine_type_in_base(name, base2.name(), ctx) ok = True + if isinstance(second.node, Var) and second.node.is_final: + # Final can never be overriden, but can override + self.fail('Cannot override constant ' + '(previously declared on base class "%s")' % base2.name(), ctx) # __slots__ is special and the type can vary across class hierarchy. if name == '__slots__': ok = True diff --git a/mypy/semanal.py b/mypy/semanal.py index 882048abb2dc..3410dc2ff9e3 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1679,15 +1679,18 @@ def check_final_implicit_def(self, s: AssignmentStmt) -> None: else: assert self.function_stack if self.function_stack[-1].name() != '__init__': - self.fail("Can only declare final attributes in class body or __init__ method", s) + self.fail("Can only declare final attributes in class body" + " or __init__ method", s) s.is_final_def = False return def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.unwrap_final(s) + def final_cb() -> None: self.fail("Final can't redefine an existing name, ignoring", s) s.is_final_def = False + for lval in s.lvalues: self.analyze_lvalue(lval, explicit_type=s.type is not None, final_cb=final_cb if s.is_final_def else None) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 9f622243ba93..789af524ca4f 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -229,7 +229,8 @@ def visit_unbound_type_nonoptional(self, t: UnboundType) -> Type: elif fullname == 'typing.Any' or fullname == 'builtins.Any': return AnyType(TypeOfAny.explicit) elif fullname == 'typing.Final': - self.fail("Final can be only used as an outermost type qualifier in assignments", t) + self.fail("Final can be only used as an outermost type qualifier" + " in assignments", t) return AnyType(TypeOfAny.from_error) elif fullname == 'typing.Tuple': if len(t.args) == 0 and not t.empty_tuple_index: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index b74c67eb5b13..8d5a4de497f4 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -414,16 +414,53 @@ class C(B): self.y = 3 # E: Can't assign to constant "y" [out] -[case testFinalOverridingVarMultipleInheritance] +[case testFinalOverridingVarMultipleInheritanceClass] from typing import Final - +class A: + x: Final = 1 +class B: + x = 2 +class C(A, B): ... +class D(B, A): ... # E: Cannot override constant (previously declared on base class "A") +C.x = 3 # E: Can't assign to constant "x" +D.x = 3 # E: Can't assign to constant "x" +C().x = 4 # E: Can't assign to constant "x" +D().x = 4 # E: Can't assign to constant "x" [out] -[case testFinalOverridingVarMultipleInheritance2] +[case testFinalOverridingVarMultipleInheritanceInit] from typing import Final +class A: + def __init__(self) -> None: + self.x: Final = 1 +class B: + def __init__(self) -> None: + self.x = 2 +class C(A, B): ... +class D(B, A): ... # E: Cannot override constant (previously declared on base class "A") +C.x = 3 # E: Can't access instance constant on class object \ + # E: Can't assign to constant "x" +D.x = 3 # E: Can't assign to constant "x" +C().x = 4 # E: Can't assign to constant "x" +D().x = 4 # E: Can't assign to constant "x" +[out] + +[case testFinalOverridingVarMultipleInheritanceMixed] +from typing import Final +class A: + x: Final = 1 +class B: + def __init__(self) -> None: + self.x = 2 +class C(A, B): ... +class D(B, A): ... # E: Cannot override constant (previously declared on base class "A") +C.x = 3 # E: Can't assign to constant "x" +D.x = 3 # E: Can't assign to constant "x" +C().x = 4 # E: Can't assign to constant "x" +D().x = 4 # E: Can't assign to constant "x" [out] [case testFinalOverridingVarWithMethod] From c6a2d994fe5844d5014871e5077438a2c2839e0a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 20 Aug 2018 23:55:55 +0100 Subject: [PATCH 11/52] Start writing docs --- docs/source/more_types.rst | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index e16b9a34cd74..a9485393e6f6 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -933,3 +933,85 @@ and non-required keys, such as ``Movie`` above, will only be compatible with another TypedDict if all required keys in the other TypedDict are required keys in the first TypedDict, and all non-required keys of the other TypedDict are also non-required keys in the first TypedDict. + +Final attributes of classes and modules +*************************************** + +Definition syntax +----------------- + +General idea: constants (not the same as being read-only). The idea is to provide a static +guarantee that whenever a given attribute is accessed it is always the same value. + +* ``a: Final = 1``, *not* the same as ``Final[Any]``, will use inference. +* ``Final[float] = 1``, to avoid invariance +* ``Final[float]`` (stubs only) +* ``self.a: Final = 1`` (also with arg), but *only* in ``__init__`` + +Definition rules +---------------- + +* At most one final declaration per module/class for a given name + +.. code-block:: python + + x: Final = 1 + x: Final = 2 # Error! + + class C: + x: Final = 1 + def __init__(self, x: int) -> None: + self.x: Final = x # Error! + +Note related to second: mypy doesn't keep two namespaces, only one common. + +* Exactly one assignment to a final attribute + +.. code-block:: python + + x = 1 + x: Final = 2 # Error! + + y: Final = 1 + y = 2 # Error! + +Using final attributes +---------------------- + +* Can't be re-assigned (or shadowed), both internally and externally: + +.. code-block:: python + + # file mod.py + from typing import Final + + x: Final = 1 + + # file main.py + from typing import Final + + + import mod + mod.x = 2 # Error! + + class C: + x: Final = 1 + + def meth(self) -> None: + self.x = 2 + + class C(D): + ... + + d: D + d.x = 2 # Error! + +* Can't be overriden by a subclass (even with another explicit final). +Final however can override normal attributes. These rules also apply to +multiple inferitance. + +Final methods +------------- + +Methods, class methods, static methods, properies all can be final +(this includes overloaded methods). From 0ca2770995d7cfac8fed6c9e072e513343b7965a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 21 Aug 2018 16:18:56 +0100 Subject: [PATCH 12/52] Final classes and methods (+ some tests) --- docs/source/more_types.rst | 6 ++ mypy/checker.py | 9 ++- mypy/nodes.py | 3 +- mypy/semanal.py | 21 ++++-- test-data/unit/check-final.test | 121 ++++++++++++++++++++++++++++---- 5 files changed, 138 insertions(+), 22 deletions(-) diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index a9485393e6f6..6a66b3625a9e 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -1015,3 +1015,9 @@ Final methods Methods, class methods, static methods, properies all can be final (this includes overloaded methods). + +Final classes +------------- + +As a bonus, final classes can't be subclassed. Mypy doesn't provide any +additional festures for them, but some other tools may have some. diff --git a/mypy/checker.py b/mypy/checker.py index 33545075749f..49d3761774ef 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1274,7 +1274,9 @@ def check_method_override_for_base_with_name( base_attr = base.names.get(name) if base_attr: # The name of the method is defined in the base class. - + if isinstance(base_attr.node, (Var, FuncBase)) and base_attr.node.is_final: + self.fail('Cannot override constant ' + '(previously declared on base class "%s")' % base.name(), defn) # Point errors at the 'def' line (important for backward compatibility # of type ignores). if not isinstance(defn, Decorator): @@ -1434,6 +1436,9 @@ def visit_class_def(self, defn: ClassDef) -> None: typ = defn.info if typ.is_protocol and typ.defn.type_vars: self.check_protocol_variance(defn) + for base in typ.mro[1:]: + if base.is_final: + self.fail('Can\'t inherit from final class "{}"'.format(base.name()), defn) with self.tscope.class_scope(defn.info), self.enter_partial_types(is_class=True): old_binder = self.binder self.binder = ConditionalTypeBinder() @@ -1906,7 +1911,7 @@ def check_compatibility_final_super(self, node: Var, return True if base_node.is_final: self.fail('Cannot override constant ' - '(previously declared on base class "%s") ' % base.name(), node) + '(previously declared on base class "%s")' % base.name(), node) return False return True diff --git a/mypy/nodes.py b/mypy/nodes.py index ae786bc038e9..1751350cc5bb 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2200,7 +2200,7 @@ class is generic then it will be a type constructor of higher kind. FLAGS = [ 'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple', - 'is_newtype', 'is_protocol', 'runtime_protocol' + 'is_newtype', 'is_protocol', 'runtime_protocol', 'is_final', ] # type: ClassVar[List[str]] def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> None: @@ -2220,6 +2220,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No self.inferring = [] self.add_type_vars() self.metadata = {} + self.is_final = False def add_type_vars(self) -> None: if self.defn.type_vars: diff --git a/mypy/semanal.py b/mypy/semanal.py index 3410dc2ff9e3..404100530fc1 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -851,12 +851,14 @@ def leave_class(self) -> None: def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None: decorator.accept(self) - if (isinstance(decorator, RefExpr) and - decorator.fullname in ('typing.runtime', 'typing_extensions.runtime')): - if defn.info.is_protocol: - defn.info.runtime_protocol = True - else: - self.fail('@runtime can only be used with protocol classes', defn) + if isinstance(decorator, RefExpr): + if decorator.fullname in ('typing.runtime', 'typing_extensions.runtime'): + if defn.info.is_protocol: + defn.info.runtime_protocol = True + else: + self.fail('@runtime can only be used with protocol classes', defn) + elif decorator.fullname == 'typing.final': + defn.info.is_final = True def calculate_abstract_status(self, typ: TypeInfo) -> None: """Calculate abstract status of a class. @@ -2383,6 +2385,13 @@ def visit_decorator(self, dec: Decorator) -> None: elif refers_to_fullname(d, 'typing.no_type_check'): dec.var.type = AnyType(TypeOfAny.special_form) no_type_check = True + elif refers_to_fullname(d, 'typing.final'): + if self.is_class_scope(): + dec.func.is_final = True + dec.var.is_final = True + removed.append(i) + else: + self.fail("@final can't be used with non-method functions", d) for i in reversed(removed): del dec.decorators[i] if not dec.is_overload or dec.var.is_property: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 8d5a4de497f4..bd6ec3f8e117 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -76,23 +76,72 @@ class C: [out] [case testFinalDefiningFunc] +from typing import final +@final # E: @final can't be used with non-method functions +def f(x: int) -> None: ... [out] [case testFinalDefiningFuncOverloaded] +from typing import final, overload + +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... +@final # E: @final can't be used with non-method functions +def f(x): + pass +[out] + +[case testFinalDefiningMeth] +from typing import final +class C: + @final + def f(self, x: int) -> None: ... +reveal_type(C().f) # E: Revealed type is 'def (x: builtins.int)' [out] -[case testFinalDefiningFuncOverloadedStubs] +[case testFinalDefiningMethOverloaded] +from typing import final, overload +class C: + @overload + def f(self, x: int) -> int: ... + @overload + def f(self, x: str) -> str: ... + @final + def f(self, x): + pass + +reveal_type(C().f) # E: Revealed type is 'Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)' [out] -[case testFinalDefiningMeth] +[case testFinalDefiningMethOverloadedStubs] +from mod import C +reveal_type(C().f) # E: Revealed type is 'Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)' +[file mod.pyi] +from typing import final, overload + +class C: + @final + @overload + def f(self, x: int) -> int: ... + @overload + def f(self, x: str) -> str: ... [out] [case testFinalDefiningProperty] +from typing import final +class C: + @final + @property + def f(self) -> int: pass +reveal_type(C().f) # E: Revealed type is 'builtins.int' +[builtins fixtures/property.pyi] [out] [case testFinalDefiningOuterOnly] @@ -221,14 +270,6 @@ from typing import Final x: Final[int] [out] -[case testFinalReassignModuleFuncExternal] - -[out] - -[case testFinalReassignModuleFuncOverloaded] - -[out] - [case testFinalReassignInstanceVarClassBody] from typing import Final @@ -464,9 +505,40 @@ D().x = 4 # E: Can't assign to constant "x" [out] [case testFinalOverridingVarWithMethod] -from typing import Final +from typing import Final, Any + +class A: + x: Final[Any] = 1 + def __init__(self) -> None: + self.y: Final[Any] = 1 + +class B(A): + def x(self) -> None: pass # E: Cannot override constant (previously declared on base class "A") + def y(self) -> None: pass # E: Cannot override constant (previously declared on base class "A") +class C(A): + @property # E: Cannot override constant (previously declared on base class "A") + def x(self) -> None: pass + @property # E: Cannot override constant (previously declared on base class "A") + def y(self) -> None: pass +[builtins fixtures/property.pyi] +[out] + +[case testFinalOverridingVarWithMethodClass] +from typing import Final, Any + +class A: + x: Final[Any] = 1 + def __init__(self) -> None: + self.y: Final[Any] = 1 + +class B(A): + @classmethod # E: Cannot override constant (previously declared on base class "A") + def x(self) -> None: pass + @classmethod # E: Cannot override constant (previously declared on base class "A") + def y(self) -> None: pass +[builtins fixtures/classmethod.pyi] [out] [case testFinalOverridingMethodWithVar] @@ -477,11 +549,11 @@ from typing import Final [out] -[case testFinalOverridingMethodMultipleinheritance] +[case testFinalOverridingMethodMultipleInheritance] [out] -[case testFinalOverridingMethodMultiplaInheritance2] +[case testFinalOverridingMethodMultipleInheritanceVar] [out] @@ -500,3 +572,26 @@ from typing import Final [case testFinalOverridingMethodOverloads] [out] + +[case testFinalClassNoInheritance] +from typing import final + +@final +class B: ... +class C(B): # E: Can't inherit from final class "B" + pass +class D(C): # E: Can't inherit from final class "B" + pass +[out] + +[case testFinalClassNoInheritanceMulti] +from typing import final + +class A: ... +@final +class B: ... +class C(B, A): # E: Can't inherit from final class "B" + pass +class D(A, B): # E: Can't inherit from final class "B" + pass +[out] From aa3ab4b60ab74c18593203c94d4f7e0ca3b8d222 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 21 Aug 2018 17:05:02 +0100 Subject: [PATCH 13/52] Minor fixes; more overriding tests --- mypy/checker.py | 15 +++++---- mypy/checkmember.py | 5 +-- mypy/nodes.py | 4 +++ test-data/unit/check-final.test | 57 +++++++++++++++++++++++++++++++-- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 49d3761774ef..b0e7c1a25b97 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1274,7 +1274,8 @@ def check_method_override_for_base_with_name( base_attr = base.names.get(name) if base_attr: # The name of the method is defined in the base class. - if isinstance(base_attr.node, (Var, FuncBase)) and base_attr.node.is_final: + if (isinstance(base_attr.node, (Var, FuncBase)) and base_attr.node.is_final or + isinstance(base_attr.node, Decorator) and base_attr.node.func.is_final): self.fail('Cannot override constant ' '(previously declared on base class "%s")' % base.name(), defn) # Point errors at the 'def' line (important for backward compatibility @@ -1550,7 +1551,8 @@ def check_compatibility(self, name: str, base1: TypeInfo, if second_type is None: self.msg.cannot_determine_type_in_base(name, base2.name(), ctx) ok = True - if isinstance(second.node, Var) and second.node.is_final: + if (isinstance(second.node, (Var, FuncBase)) and second.node.is_final or + isinstance(second.node, Decorator) and second.node.func.is_final): # Final can never be overriden, but can override self.fail('Cannot override constant ' '(previously declared on base class "%s")' % base2.name(), ctx) @@ -1618,10 +1620,11 @@ def check_final(self, s: AssignmentStmt) -> None: if cls is not None: # This exist to give more errors even if we overriden with a new var # (which is itself an error) - for base in cls.mro: + for base in cls.mro[1:]: sym = base.names.get(name) - if sym and isinstance(sym.node, Var): - if sym.node.is_final and not s.is_final_def: + if sym and isinstance(sym.node, (Var, Decorator)): + var = sym.node if isinstance(sym.node, Var) else sym.node.var + if var.is_final: self.fail('Can\'t assign to constant "{}"'.format(name), s) if lv.node.is_final and not s.is_final_def: self.fail('Can\'t assign to constant "{}"'.format(name), s) @@ -1907,7 +1910,7 @@ def check_compatibility_classvar_super(self, node: Var, def check_compatibility_final_super(self, node: Var, base: TypeInfo, base_node: Optional[Node]) -> bool: - if not isinstance(base_node, Var): + if not isinstance(base_node, (Var, FuncBase, Decorator)): return True if base_node.is_final: self.fail('Cannot override constant ' diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 14ae82c1ea1c..bd64b43104c0 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -266,7 +266,7 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, if is_lvalue and not chk.get_final_context(): for base in info.mro: sym = base.names.get(name) - if sym and isinstance(sym.node, Var) and sym.node.is_final: + if sym and isinstance(sym.node, (Var, FuncBase, Decorator)) and sym.node.is_final: msg.fail('Can\'t assign to constant "{}"'.format(name), node) return analyze_var(name, v, itype, info, node, is_lvalue, msg, @@ -546,7 +546,8 @@ def analyze_class_attribute_access(itype: Instance, msg.fail("Can't access instance constant on class object", context) for base in itype.type.mro: b_node = base.names.get(name) - if (b_node and isinstance(b_node.node, Var) and is_lvalue and b_node.node.is_final + if (b_node and isinstance(b_node.node, (Var, FuncBase, Decorator)) and + is_lvalue and b_node.node.is_final and not chk.get_final_context()): name = b_node.node.name() msg.fail('Can\'t assign to constant "{}"'.format(name), context) diff --git a/mypy/nodes.py b/mypy/nodes.py index 1751350cc5bb..9f44fd2e2bac 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -671,6 +671,10 @@ def name(self) -> str: def fullname(self) -> Bogus[str]: return self.func.fullname() + @property + def is_final(self) -> bool: + return self.func.is_final + @property def info(self) -> 'TypeInfo': return self.func.info diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index bd6ec3f8e117..acb4349c4dc5 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -393,8 +393,10 @@ class B(A): def __init__(self) -> None: self.y: Final = 1 class C(B): - x: Final = 2 # E: Cannot override constant (previously declared on base class "B") - y: Final = 2 # E: Cannot override constant (previously declared on base class "B") + x: Final = 2 # E: Cannot override constant (previously declared on base class "B") \ + # E: Can't assign to constant "x" + y: Final = 2 # E: Cannot override constant (previously declared on base class "B") \ + # E: Can't assign to constant "y" [out] [case testFinalOverridingVarInit] @@ -542,11 +544,62 @@ class B(A): [out] [case testFinalOverridingMethodWithVar] +from typing import final, Final, Any + +a: Any + +class A: + @final + def f(self) -> None: pass + @final + @property + def p(self) -> int: pass +class B(A): + f = a # E: Cannot override constant (previously declared on base class "A") \ + # E: Can't assign to constant "f" + p = a # E: Cannot override constant (previously declared on base class "A") \ + # E: Can't assign to constant "p" +class C(A): + f: Any # E: Cannot override constant (previously declared on base class "A") \ + # E: Can't assign to constant "f" + p: Any # E: Cannot override constant (previously declared on base class "A") \ + # E: Can't assign to constant "p" +class D(A): + f: Final = a # E: Cannot override constant (previously declared on base class "A") \ + # E: Can't assign to constant "f" + p: Final = a # E: Cannot override constant (previously declared on base class "A") \ + # E: Can't assign to constant "p" +[builtins fixtures/property.pyi] [out] [case testFinalOverridingMethodWithVarImplicit] +from typing import final, Any, Final + +a: Any +class A: + @final + def f(self) -> None: pass + @final + @classmethod + def c(cls) -> int: pass + +class B(A): + def __init__(self) -> None: + self.f: Any # E: Can't assign to constant "f" \ + # E: Cannot override constant (previously declared on base class "A") + self.c: Any # E: Can't assign to constant "c" \ + # E: Cannot override constant (previously declared on base class "A") + +B().f = a # E: Can't assign to constant "f" +B().c = a # E: Can't assign to constant "c" + +class C(A): + def __init__(self) -> None: + self.f: Final = a # E: Cannot override constant (previously declared on base class "A") + self.c: Final = a # E: Cannot override constant (previously declared on base class "A") +[builtins fixtures/classmethod.pyi] [out] [case testFinalOverridingMethodMultipleInheritance] From ad42674bb99210568037402c611f68038e05ebb4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 21 Aug 2018 17:38:20 +0100 Subject: [PATCH 14/52] An overload fix; more tests --- mypy/semanal.py | 6 +++ test-data/unit/check-final.test | 93 ++++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 404100530fc1..61b744d0c5e6 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -610,6 +610,12 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: # redefinitions already. return + # Check final status + if any(item.is_final for item in defn.items): + defn.is_final = True + if defn.impl is not None and defn.impl.is_final: + defn.is_final = True + # We know this is an overload def -- let's handle classmethod and staticmethod class_status = [] static_status = [] diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index acb4349c4dc5..e686d77d989d 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -5,10 +5,12 @@ [case testFinalDefiningModuleVar] from typing import Final + x: Final = int() y: Final[float] = int() z: Final[int] = int() bad: Final[str] = str() + reveal_type(x) # E: Revealed type is 'builtins.int' reveal_type(y) # E: Revealed type is 'builtins.float' reveal_type(z) # E: Revealed type is 'builtins.int' @@ -16,17 +18,20 @@ reveal_type(z) # E: Revealed type is 'builtins.int' [case testFinalDefiningInstanceVar] from typing import Final + class C: x: Final = int() y: Final[float] = int() z: Final[int] = int() bad: Final[str] = int() # E: Incompatible types in assignment (expression has type "int", variable has type "str") -reveal_type(C.x) # E: Revealed type is 'builtins.int' -reveal_type(C.y) # E: Revealed type is 'builtins.float' -reveal_type(C.z) # E: Revealed type is 'builtins.int' -reveal_type(C().x) # E: Revealed type is 'builtins.int' -reveal_type(C().y) # E: Revealed type is 'builtins.float' -reveal_type(C().z) # E: Revealed type is 'builtins.int' +class D(C): pass + +reveal_type(D.x) # E: Revealed type is 'builtins.int' +reveal_type(D.y) # E: Revealed type is 'builtins.float' +reveal_type(D.z) # E: Revealed type is 'builtins.int' +reveal_type(D().x) # E: Revealed type is 'builtins.int' +reveal_type(D().y) # E: Revealed type is 'builtins.float' +reveal_type(D().z) # E: Revealed type is 'builtins.int' [out] [case testFinalDefiningInstanceVarImplicit] @@ -55,6 +60,7 @@ reveal_type(C().x) # E: Revealed type is 'builtins.float' [case testFinalInvalidDefinitions] from typing import Final, Any + x = y = 1 # type: Final[float] # E: Invalid final declaration, ignoring z: Any z[0]: Final # E: Unexpected type declaration \ @@ -66,6 +72,7 @@ z[0]: Final # E: Unexpected type declaration \ import mod [file mod.pyi] from typing import Final + x: Final y: Final[int] class C: @@ -355,6 +362,15 @@ D().x = 2 # E: Can't assign to constant "x" D().y = 2 # E: Can't assign to constant "y" [out] +[case testFinalWorksWithComplexTargets] +from typing import Final, Any + +y: Final[Any] = 1 +x = a, (b, y), c = 2, (2, 2), 2 # E: Can't assign to constant "y" +t, *y, s = u = [2, 2, 2] # E: Can't assign to constant "y" +[builtins fixtures/list.pyi] +[out] + -- Overriding [case testFinalOverridingVarClassBody] @@ -603,27 +619,92 @@ class C(A): [out] [case testFinalOverridingMethodMultipleInheritance] +from typing import final +class A: + def m(self) -> int: pass +class B: + @final + def m(self) -> int: pass + +class C(A, B): pass # E: Cannot override constant (previously declared on base class "B") +class D(B, A): pass [out] [case testFinalOverridingMethodMultipleInheritanceVar] +from typing import final, Any +class A: + m: Any +class B: + @final + def m(self) -> int: pass + +class C(A, B): pass # E: Cannot override constant (previously declared on base class "B") +class D(B, A): pass [out] [case testFinalOverridingClassMethod] +from typing import final + +class B: + @classmethod + @final + def f(cls) -> int: pass +class C(B): + @classmethod # E: Cannot override constant (previously declared on base class "B") + def f(cls) -> int: pass +[builtins fixtures/classmethod.pyi] [out] [case testFinalOverridingStaticMethod] +from typing import final +class B: + @staticmethod + @final + def f() -> int: pass + +class C(B): + @staticmethod # E: Cannot override constant (previously declared on base class "B") + def f() -> int: pass +[builtins fixtures/staticmethod.pyi] [out] [case testFinalOverridingProperty] +from typing import final + +class B: + @final + @property + def f(self) -> int: pass +class C(B): + @property # E: Cannot override constant (previously declared on base class "B") + def f(self) -> int: pass +[builtins fixtures/property.pyi] [out] [case testFinalOverridingMethodOverloads] +from typing import final, overload + +class B: + @overload + def f(self, x: int) -> int: ... + @overload + def f(self, x: str) -> str: ... + @final + def f(self, x): + pass +class C(B): + @overload # E: Cannot override constant (previously declared on base class "B") + def f(self, x: int) -> int: ... + @overload + def f(self, x: str) -> str: ... + def f(self, x): + pass [out] [case testFinalClassNoInheritance] From a106d085559ff7cb3bc4fa27e4fdba688a05d364 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 21 Aug 2018 18:39:48 +0100 Subject: [PATCH 15/52] Tweak error messages; move error logic to messages.py --- mypy/checker.py | 27 ++--- mypy/checkmember.py | 4 +- mypy/messages.py | 8 ++ test-data/unit/check-final.test | 186 +++++++++++++++++++------------- 4 files changed, 134 insertions(+), 91 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b0e7c1a25b97..1d1b2fc9fd28 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1274,16 +1274,18 @@ def check_method_override_for_base_with_name( base_attr = base.names.get(name) if base_attr: # The name of the method is defined in the base class. - if (isinstance(base_attr.node, (Var, FuncBase)) and base_attr.node.is_final or - isinstance(base_attr.node, Decorator) and base_attr.node.func.is_final): - self.fail('Cannot override constant ' - '(previously declared on base class "%s")' % base.name(), defn) + # Point errors at the 'def' line (important for backward compatibility # of type ignores). if not isinstance(defn, Decorator): context = defn else: context = defn.func + + # First if we don't override a final (always an error, even with Any types) + if (isinstance(base_attr.node, (Var, FuncBase)) and base_attr.node.is_final or + isinstance(base_attr.node, Decorator) and base_attr.node.func.is_final): + self.msg.cant_override_final(name, base.name(), defn) # Construct the type of the overriding method. if isinstance(defn, FuncBase): typ = self.function_type(defn) # type: Type @@ -1551,11 +1553,11 @@ def check_compatibility(self, name: str, base1: TypeInfo, if second_type is None: self.msg.cannot_determine_type_in_base(name, base2.name(), ctx) ok = True + # Final attribute can never be overridden, but can override normal attributes, + # so we only check `second`. if (isinstance(second.node, (Var, FuncBase)) and second.node.is_final or isinstance(second.node, Decorator) and second.node.func.is_final): - # Final can never be overriden, but can override - self.fail('Cannot override constant ' - '(previously declared on base class "%s")' % base2.name(), ctx) + self.msg.cant_override_final(name, base2.name(), ctx) # __slots__ is special and the type can vary across class hierarchy. if name == '__slots__': ok = True @@ -1618,16 +1620,16 @@ def check_final(self, s: AssignmentStmt) -> None: name = lv.node.name() cls = self.scope.active_class() if cls is not None: - # This exist to give more errors even if we overriden with a new var + # This exist to give more errors even if we overridden with a new var # (which is itself an error) for base in cls.mro[1:]: sym = base.names.get(name) if sym and isinstance(sym.node, (Var, Decorator)): var = sym.node if isinstance(sym.node, Var) else sym.node.var - if var.is_final: - self.fail('Can\'t assign to constant "{}"'.format(name), s) + if var.is_final and not s.is_final_def: + self.msg.cant_assign_to_final(name, var.info is not None, s) if lv.node.is_final and not s.is_final_def: - self.fail('Can\'t assign to constant "{}"'.format(name), s) + self.msg.cant_assign_to_final(name, lv.node.info is not None, s) def visit_assignment_stmt(self, s: AssignmentStmt) -> None: """Type check an assignment statement. @@ -1913,8 +1915,7 @@ def check_compatibility_final_super(self, node: Var, if not isinstance(base_node, (Var, FuncBase, Decorator)): return True if base_node.is_final: - self.fail('Cannot override constant ' - '(previously declared on base class "%s")' % base.name(), node) + self.msg.cant_override_final(node.name(), base.name(), node) return False return True diff --git a/mypy/checkmember.py b/mypy/checkmember.py index bd64b43104c0..65e19cebf389 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -267,7 +267,7 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, for base in info.mro: sym = base.names.get(name) if sym and isinstance(sym.node, (Var, FuncBase, Decorator)) and sym.node.is_final: - msg.fail('Can\'t assign to constant "{}"'.format(name), node) + msg.cant_assign_to_final(name, module_level=False, ctx=node) return analyze_var(name, v, itype, info, node, is_lvalue, msg, original_type, builtin_type, not_ready_callback, @@ -550,7 +550,7 @@ def analyze_class_attribute_access(itype: Instance, is_lvalue and b_node.node.is_final and not chk.get_final_context()): name = b_node.node.name() - msg.fail('Can\'t assign to constant "{}"'.format(name), context) + msg.cant_assign_to_final(name, module_level=False, ctx=context) if itype.type.is_enum and not (is_lvalue or is_decorated or is_method): return itype diff --git a/mypy/messages.py b/mypy/messages.py index 0edb95c6694b..25b6bc5be7e1 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -951,6 +951,14 @@ def cant_assign_to_method(self, context: Context) -> None: def cant_assign_to_classvar(self, name: str, context: Context) -> None: self.fail('Cannot assign to class variable "%s" via instance' % name, context) + def cant_override_final(self, name: str, base_name: str, ctx: Context) -> None: + self.fail('Cannot override final attribute "{}"'.format(name), ctx) + self.note('(previously declared on base class "{}")'.format(base_name), ctx) + + def cant_assign_to_final(self, name: str, module_level: bool, ctx: Context) -> None: + kind = "constant" if module_level else "final attribute" + self.fail('Can\'t assign to {} "{}"'.format(kind, name), ctx) + def read_only_property(self, name: str, type: TypeInfo, context: Context) -> None: self.fail('Property "{}" defined in "{}" is read-only'.format( diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index e686d77d989d..3d098fbe7e5f 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -229,6 +229,11 @@ class C: # E: Final can be only applied to a name or an attribute on self [out] +[case testFinalNotInProtocol] +from typing import Final, final, Protocol + +[out] + -- Reassignments [case testFinalReassignModuleVar] @@ -297,7 +302,7 @@ class C: self.y = 1 self.y: Final = 2 # E: Final can't redefine an existing name, ignoring def meth(self) -> None: - self.x = 2 # E: Can't assign to constant "x" + self.x = 2 # E: Can't assign to final attribute "x" [out] [case testFinalReassignInstanceVarClassVsInit] @@ -307,7 +312,7 @@ class C: y: Final = 1 def __init__(self) -> None: self.x: Final = 1 - self.y = 2 # E: Can't assign to constant "y" + self.y = 2 # E: Can't assign to final attribute "y" x = 2 # E: Can't assign to constant "x" [out] @@ -319,14 +324,14 @@ class C: def __init__(self) -> None: self.y: Final = 1 def meth(self) -> None: - self.x = 2 # E: Can't assign to constant "x" - self.y = 2 # E: Can't assign to constant "y" + self.x = 2 # E: Can't assign to final attribute "x" + self.y = 2 # E: Can't assign to final attribute "y" def other(self) -> None: - self.x = 2 # E: Can't assign to constant "x" - self.y = 2 # E: Can't assign to constant "y" + self.x = 2 # E: Can't assign to final attribute "x" + self.y = 2 # E: Can't assign to final attribute "y" @classmethod def cm(cls) -> None: - cls.x = 2 # E: Can't assign to constant "x" + cls.x = 2 # E: Can't assign to final attribute "x" cls.y # E: Can't access instance constant on class object [builtins fixtures/classmethod.pyi] [out] @@ -341,10 +346,10 @@ class C: class D(C): pass -C.x = 2 # E: Can't assign to constant "x" -D.x = 2 # E: Can't assign to constant "x" +C.x = 2 # E: Can't assign to final attribute "x" +D.x = 2 # E: Can't assign to final attribute "x" D.y = 2 # E: Can't access instance constant on class object \ - # E: Can't assign to constant "y" + # E: Can't assign to final attribute "y" [out] [case testFinalReassignInstanceVarExternalInstance] @@ -357,9 +362,9 @@ class C: class D(C): pass -C().x = 2 # E: Can't assign to constant "x" -D().x = 2 # E: Can't assign to constant "x" -D().y = 2 # E: Can't assign to constant "y" +C().x = 2 # E: Can't assign to final attribute "x" +D().x = 2 # E: Can't assign to final attribute "x" +D().y = 2 # E: Can't assign to final attribute "y" [out] [case testFinalWorksWithComplexTargets] @@ -385,16 +390,18 @@ class B(A): def __init__(self) -> None: self.y: Final = 1 class C(B): - x: int = 2 # E: Cannot override constant (previously declared on base class "B") \ + x: int = 2 # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "B") \ # E: Can't assign to constant "x" - y: int = 2 # E: Cannot override constant (previously declared on base class "B") \ + y: int = 2 # E: Cannot override final attribute "y" \ + # N: (previously declared on base class "B") \ # E: Can't assign to constant "y" x = 3 # E: Can't assign to constant "x" y = 3 # E: Can't assign to constant "y" class D(C): pass -D.x = 4 # E: Can't assign to constant "x" -D.y = 4 # E: Can't assign to constant "y" +D.x = 4 # E: Can't assign to final attribute "x" +D.y = 4 # E: Can't assign to final attribute "y" [out] [case testFinalOverridingVarClassBodyExplicit] @@ -409,10 +416,10 @@ class B(A): def __init__(self) -> None: self.y: Final = 1 class C(B): - x: Final = 2 # E: Cannot override constant (previously declared on base class "B") \ - # E: Can't assign to constant "x" - y: Final = 2 # E: Cannot override constant (previously declared on base class "B") \ - # E: Can't assign to constant "y" + x: Final = 2 # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "B") + y: Final = 2 # E: Cannot override final attribute "y" \ + # N: (previously declared on base class "B") [out] [case testFinalOverridingVarInit] @@ -428,11 +435,11 @@ class B(A): self.y: Final = 1 class C(B): def __init__(self) -> None: - self.x = 2 # E: Can't assign to constant "x" - self.y = 2 # E: Can't assign to constant "y" + self.x = 2 # E: Can't assign to final attribute "x" + self.y = 2 # E: Can't assign to final attribute "y" def meth(self) -> None: - self.x = 3 # E: Can't assign to constant "x" - self.y = 3 # E: Can't assign to constant "y" + self.x = 3 # E: Can't assign to final attribute "x" + self.y = 3 # E: Can't assign to final attribute "y" [out] [case testFinalOverridingVarInit2] @@ -448,8 +455,10 @@ class B(A): self.y: Final = 1 class C(B): def __init__(self) -> None: - self.x: Final = 2 # E: Cannot override constant (previously declared on base class "B") - self.y: Final = 2 # E: Cannot override constant (previously declared on base class "B") + self.x: Final = 2 # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "B") + self.y: Final = 2 # E: Cannot override final attribute "y" \ + # N: (previously declared on base class "B") [out] [case testFinalOverridingVarOtherMethod] @@ -465,12 +474,14 @@ class B(A): self.y: Final = 1 class C(B): def meth(self) -> None: - self.x: int = 2 # E: Can't assign to constant "x" \ - # E: Cannot override constant (previously declared on base class "B") - self.y: int = 2 # E: Can't assign to constant "y" \ - # E: Cannot override constant (previously declared on base class "B") - self.x = 3 # E: Can't assign to constant "x" - self.y = 3 # E: Can't assign to constant "y" + self.x: int = 2 # E: Can't assign to final attribute "x" \ + # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "B") + self.y: int = 2 # E: Can't assign to final attribute "y" \ + # E: Cannot override final attribute "y" \ + # N: (previously declared on base class "B") + self.x = 3 # E: Can't assign to final attribute "x" + self.y = 3 # E: Can't assign to final attribute "y" [out] [case testFinalOverridingVarMultipleInheritanceClass] @@ -481,11 +492,12 @@ class A: class B: x = 2 class C(A, B): ... -class D(B, A): ... # E: Cannot override constant (previously declared on base class "A") -C.x = 3 # E: Can't assign to constant "x" -D.x = 3 # E: Can't assign to constant "x" -C().x = 4 # E: Can't assign to constant "x" -D().x = 4 # E: Can't assign to constant "x" +class D(B, A): ... # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "A") +C.x = 3 # E: Can't assign to final attribute "x" +D.x = 3 # E: Can't assign to final attribute "x" +C().x = 4 # E: Can't assign to final attribute "x" +D().x = 4 # E: Can't assign to final attribute "x" [out] [case testFinalOverridingVarMultipleInheritanceInit] @@ -498,12 +510,13 @@ class B: def __init__(self) -> None: self.x = 2 class C(A, B): ... -class D(B, A): ... # E: Cannot override constant (previously declared on base class "A") +class D(B, A): ... # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "A") C.x = 3 # E: Can't access instance constant on class object \ - # E: Can't assign to constant "x" -D.x = 3 # E: Can't assign to constant "x" -C().x = 4 # E: Can't assign to constant "x" -D().x = 4 # E: Can't assign to constant "x" + # E: Can't assign to final attribute "x" +D.x = 3 # E: Can't assign to final attribute "x" +C().x = 4 # E: Can't assign to final attribute "x" +D().x = 4 # E: Can't assign to final attribute "x" [out] [case testFinalOverridingVarMultipleInheritanceMixed] @@ -515,11 +528,12 @@ class B: def __init__(self) -> None: self.x = 2 class C(A, B): ... -class D(B, A): ... # E: Cannot override constant (previously declared on base class "A") -C.x = 3 # E: Can't assign to constant "x" -D.x = 3 # E: Can't assign to constant "x" -C().x = 4 # E: Can't assign to constant "x" -D().x = 4 # E: Can't assign to constant "x" +class D(B, A): ... # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "A") +C.x = 3 # E: Can't assign to final attribute "x" +D.x = 3 # E: Can't assign to final attribute "x" +C().x = 4 # E: Can't assign to final attribute "x" +D().x = 4 # E: Can't assign to final attribute "x" [out] [case testFinalOverridingVarWithMethod] @@ -531,13 +545,17 @@ class A: self.y: Final[Any] = 1 class B(A): - def x(self) -> None: pass # E: Cannot override constant (previously declared on base class "A") - def y(self) -> None: pass # E: Cannot override constant (previously declared on base class "A") + def x(self) -> None: pass # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "A") + def y(self) -> None: pass # E: Cannot override final attribute "y" \ + # N: (previously declared on base class "A") class C(A): - @property # E: Cannot override constant (previously declared on base class "A") + @property # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "A") def x(self) -> None: pass - @property # E: Cannot override constant (previously declared on base class "A") + @property # E: Cannot override final attribute "y" \ + # N: (previously declared on base class "A") def y(self) -> None: pass [builtins fixtures/property.pyi] [out] @@ -551,9 +569,11 @@ class A: self.y: Final[Any] = 1 class B(A): - @classmethod # E: Cannot override constant (previously declared on base class "A") + @classmethod # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "A") def x(self) -> None: pass - @classmethod # E: Cannot override constant (previously declared on base class "A") + @classmethod # E: Cannot override final attribute "y" \ + # N: (previously declared on base class "A") def y(self) -> None: pass [builtins fixtures/classmethod.pyi] @@ -572,20 +592,24 @@ class A: def p(self) -> int: pass class B(A): - f = a # E: Cannot override constant (previously declared on base class "A") \ + f = a # E: Cannot override final attribute "f" \ + # N: (previously declared on base class "A") \ # E: Can't assign to constant "f" - p = a # E: Cannot override constant (previously declared on base class "A") \ + p = a # E: Cannot override final attribute "p" \ + # N: (previously declared on base class "A") \ # E: Can't assign to constant "p" class C(A): - f: Any # E: Cannot override constant (previously declared on base class "A") \ + f: Any # E: Cannot override final attribute "f" \ + # N: (previously declared on base class "A") \ # E: Can't assign to constant "f" - p: Any # E: Cannot override constant (previously declared on base class "A") \ + p: Any # E: Cannot override final attribute "p" \ + # N: (previously declared on base class "A") \ # E: Can't assign to constant "p" class D(A): - f: Final = a # E: Cannot override constant (previously declared on base class "A") \ - # E: Can't assign to constant "f" - p: Final = a # E: Cannot override constant (previously declared on base class "A") \ - # E: Can't assign to constant "p" + f: Final = a # E: Cannot override final attribute "f" \ + # N: (previously declared on base class "A") + p: Final = a # E: Cannot override final attribute "p" \ + # N: (previously declared on base class "A") [builtins fixtures/property.pyi] [out] @@ -603,18 +627,22 @@ class A: class B(A): def __init__(self) -> None: - self.f: Any # E: Can't assign to constant "f" \ - # E: Cannot override constant (previously declared on base class "A") - self.c: Any # E: Can't assign to constant "c" \ - # E: Cannot override constant (previously declared on base class "A") + self.f: Any # E: Can't assign to final attribute "f" \ + # E: Cannot override final attribute "f" \ + # N: (previously declared on base class "A") + self.c: Any # E: Can't assign to final attribute "c" \ + # E: Cannot override final attribute "c" \ + # N: (previously declared on base class "A") -B().f = a # E: Can't assign to constant "f" -B().c = a # E: Can't assign to constant "c" +B().f = a # E: Can't assign to final attribute "f" +B().c = a # E: Can't assign to final attribute "c" class C(A): def __init__(self) -> None: - self.f: Final = a # E: Cannot override constant (previously declared on base class "A") - self.c: Final = a # E: Cannot override constant (previously declared on base class "A") + self.f: Final = a # E: Cannot override final attribute "f" \ + # N: (previously declared on base class "A") + self.c: Final = a # E: Cannot override final attribute "c" \ + # N: (previously declared on base class "A") [builtins fixtures/classmethod.pyi] [out] @@ -627,7 +655,8 @@ class B: @final def m(self) -> int: pass -class C(A, B): pass # E: Cannot override constant (previously declared on base class "B") +class C(A, B): pass # E: Cannot override final attribute "m" \ + # N: (previously declared on base class "B") class D(B, A): pass [out] @@ -640,7 +669,8 @@ class B: @final def m(self) -> int: pass -class C(A, B): pass # E: Cannot override constant (previously declared on base class "B") +class C(A, B): pass # E: Cannot override final attribute "m" \ + # N: (previously declared on base class "B") class D(B, A): pass [out] @@ -653,7 +683,8 @@ class B: def f(cls) -> int: pass class C(B): - @classmethod # E: Cannot override constant (previously declared on base class "B") + @classmethod # E: Cannot override final attribute "f" \ + # N: (previously declared on base class "B") def f(cls) -> int: pass [builtins fixtures/classmethod.pyi] [out] @@ -667,7 +698,8 @@ class B: def f() -> int: pass class C(B): - @staticmethod # E: Cannot override constant (previously declared on base class "B") + @staticmethod # E: Cannot override final attribute "f" \ + # N: (previously declared on base class "B") def f() -> int: pass [builtins fixtures/staticmethod.pyi] [out] @@ -681,7 +713,8 @@ class B: def f(self) -> int: pass class C(B): - @property # E: Cannot override constant (previously declared on base class "B") + @property # E: Cannot override final attribute "f" \ + # N: (previously declared on base class "B") def f(self) -> int: pass [builtins fixtures/property.pyi] [out] @@ -699,7 +732,8 @@ class B: pass class C(B): - @overload # E: Cannot override constant (previously declared on base class "B") + @overload # E: Cannot override final attribute "f" \ + # N: (previously declared on base class "B") def f(self, x: int) -> int: ... @overload def f(self, x: str) -> str: ... From 37c4bb66c67e26c02088c12ef657314d7563ec5c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 21 Aug 2018 18:53:23 +0100 Subject: [PATCH 16/52] Add protocol tests --- mypy/messages.py | 3 +++ mypy/semanal.py | 10 +++++++--- test-data/unit/check-final.test | 14 +++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 25b6bc5be7e1..9b16e2d31d29 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -959,6 +959,9 @@ def cant_assign_to_final(self, name: str, module_level: bool, ctx: Context) -> N kind = "constant" if module_level else "final attribute" self.fail('Can\'t assign to {} "{}"'.format(kind, name), ctx) + def protocol_members_cant_be_final(self, ctx: Context) -> None: + self.fail("Protocol members can't be final", ctx) + def read_only_property(self, name: str, type: TypeInfo, context: Context) -> None: self.fail('Property "{}" defined in "{}" is read-only'.format( diff --git a/mypy/semanal.py b/mypy/semanal.py index 61b744d0c5e6..e953c03b864c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1669,7 +1669,7 @@ def unwrap_final(self, s: AssignmentStmt) -> None: assert isinstance(lval, RefExpr) s.is_final_def = True if self.type and self.type.is_protocol: - self.fail("Protocol members can't be final", s) + self.msg.protocol_members_cant_be_final(s) if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file: self.fail("Final declaration outside stubs must have right hand side", s) return @@ -2393,8 +2393,12 @@ def visit_decorator(self, dec: Decorator) -> None: no_type_check = True elif refers_to_fullname(d, 'typing.final'): if self.is_class_scope(): - dec.func.is_final = True - dec.var.is_final = True + assert self.type is not None, "No type set at class scope" + if self.type.is_protocol: + self.msg.protocol_members_cant_be_final(d) + else: + dec.func.is_final = True + dec.var.is_final = True removed.append(i) else: self.fail("@final can't be used with non-method functions", d) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 3d098fbe7e5f..31d440c38902 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -230,8 +230,20 @@ class C: [out] [case testFinalNotInProtocol] -from typing import Final, final, Protocol +from typing import Final, final, Protocol, overload +class P(Protocol): + x: Final[float] = 1 # E: Protocol members can't be final + @final # E: Protocol members can't be final + def meth(self, x) -> int: + pass + @overload + def other(self, x: int) -> int: ... + @overload + def other(self, x: str) -> str: ... + @final # E: Protocol members can't be final + def other(self, x): + pass [out] -- Reassignments From b6531d37ce4c082113fcaa70704a6a7fd3b6fd16 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 21 Aug 2018 20:39:21 +0100 Subject: [PATCH 17/52] Add some comments and docstrings; some reorg --- mypy/checker.py | 82 ++++++++++++----------- mypy/checkmember.py | 7 ++ mypy/messages.py | 4 ++ mypy/nodes.py | 9 ++- mypy/semanal.py | 96 +++++++++++++++------------ test-data/unit/check-final.test | 2 +- test-data/unit/check-incremental.test | 2 + test-data/unit/merge.test | 16 ----- 8 files changed, 121 insertions(+), 97 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1d1b2fc9fd28..bb3eb3303d88 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -217,6 +217,9 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option # If True, process function definitions. If False, don't. This is used # for processing module top levels in fine-grained incremental mode. self.recurse_into_functions = True + # This internal flag is used to indicate whether we a currently type-checking + # a final declaration (assignment). Should not be set manually, use + # get_final_context/set_final_context instead. self._is_final_def = False def reset(self) -> None: @@ -1282,10 +1285,10 @@ def check_method_override_for_base_with_name( else: context = defn.func - # First if we don't override a final (always an error, even with Any types) - if (isinstance(base_attr.node, (Var, FuncBase)) and base_attr.node.is_final or - isinstance(base_attr.node, Decorator) and base_attr.node.func.is_final): + # First, check if we don't override a final (always an error, even with Any types). + if isinstance(base_attr.node, (Var, FuncBase, Decorator)) and base_attr.node.is_final: self.msg.cant_override_final(name, base.name(), defn) + # Construct the type of the overriding method. if isinstance(defn, FuncBase): typ = self.function_type(defn) # type: Type @@ -1553,10 +1556,9 @@ def check_compatibility(self, name: str, base1: TypeInfo, if second_type is None: self.msg.cannot_determine_type_in_base(name, base2.name(), ctx) ok = True - # Final attribute can never be overridden, but can override normal attributes, + # Final attributes can never be overridden, but can override normal attributes, # so we only check `second`. - if (isinstance(second.node, (Var, FuncBase)) and second.node.is_final or - isinstance(second.node, Decorator) and second.node.func.is_final): + if isinstance(second.node, (Var, FuncBase, Decorator)) and second.node.is_final: self.msg.cant_override_final(name, base2.name(), ctx) # __slots__ is special and the type can vary across class hierarchy. if name == '__slots__': @@ -1600,37 +1602,6 @@ def visit_block(self, b: Block) -> None: break self.accept(s) - def get_final_context(self) -> bool: - return self._is_final_def - - @contextmanager - def set_final_context(self, is_final_def: bool) -> Iterator[None]: - old_ctx = self._is_final_def - self._is_final_def = is_final_def - try: - yield - finally: - self._is_final_def = old_ctx - - def check_final(self, s: AssignmentStmt) -> None: - # for class body & modules, other in checkmember - lvs = self.flatten_lvalues(s.lvalues) - for lv in lvs: - if isinstance(lv, RefExpr) and isinstance(lv.node, Var): - name = lv.node.name() - cls = self.scope.active_class() - if cls is not None: - # This exist to give more errors even if we overridden with a new var - # (which is itself an error) - for base in cls.mro[1:]: - sym = base.names.get(name) - if sym and isinstance(sym.node, (Var, Decorator)): - var = sym.node if isinstance(sym.node, Var) else sym.node.var - if var.is_final and not s.is_final_def: - self.msg.cant_assign_to_final(name, var.info is not None, s) - if lv.node.is_final and not s.is_final_def: - self.msg.cant_assign_to_final(name, lv.node.info is not None, s) - def visit_assignment_stmt(self, s: AssignmentStmt) -> None: """Type check an assignment statement. @@ -1919,6 +1890,43 @@ def check_compatibility_final_super(self, node: Var, return False return True + def get_final_context(self) -> bool: + return self._is_final_def + + @contextmanager + def set_final_context(self, is_final_def: bool) -> Iterator[None]: + old_ctx = self._is_final_def + self._is_final_def = is_final_def + try: + yield + finally: + self._is_final_def = old_ctx + + def check_final(self, s: AssignmentStmt) -> None: + """Check if this assignment does not assign to a final attribute. + + This function perfoms the check only for name assignments at module + and class scope. The assignments to `obj.attr` and `Cls.attr` are checked + in checkmember.py. + """ + lvs = self.flatten_lvalues(s.lvalues) + for lv in lvs: + if isinstance(lv, RefExpr) and isinstance(lv.node, Var): + name = lv.node.name() + cls = self.scope.active_class() + if cls is not None: + # Theses additional checks exist to give more errors messages + # even if the final attribute was overridden with a new symbol + # (which is itself an error). + for base in cls.mro[1:]: + sym = base.names.get(name) + if sym and isinstance(sym.node, (Var, Decorator)): + var = sym.node if isinstance(sym.node, Var) else sym.node.var + if var.is_final and not s.is_final_def: + self.msg.cant_assign_to_final(name, var.info is not None, s) + if lv.node.is_final and not s.is_final_def: + self.msg.cant_assign_to_final(name, lv.node.info is not None, s) + def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression, context: Context, infer_lvalue_type: bool = True) -> None: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 65e19cebf389..3dc4d95c3cc6 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -263,6 +263,8 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, if isinstance(v, Var): implicit = info[name].implicit + # An assignment to final attribute is always an error, + # independently of types. if is_lvalue and not chk.get_final_context(): for base in info.mro: sym = base.names.get(name) @@ -542,8 +544,13 @@ def analyze_class_attribute_access(itype: Instance, if isinstance(node.node, TypeInfo): msg.fail(messages.CANNOT_ASSIGN_TO_TYPE, context) + # If a constant was declared on `self` in `__init__`, then it + # can't be accessed on the class object. if node.implicit and isinstance(node.node, Var) and node.node.is_final: msg.fail("Can't access instance constant on class object", context) + + # An assignment to final attribute on class object is also always an error, + # independently of types. for base in itype.type.mro: b_node = base.names.get(name) if (b_node and isinstance(b_node.node, (Var, FuncBase, Decorator)) and diff --git a/mypy/messages.py b/mypy/messages.py index 9b16e2d31d29..1d1a37ce21c7 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -956,6 +956,10 @@ def cant_override_final(self, name: str, base_name: str, ctx: Context) -> None: self.note('(previously declared on base class "{}")'.format(base_name), ctx) def cant_assign_to_final(self, name: str, module_level: bool, ctx: Context) -> None: + """Warn about a prohibited assignment to a final attribute. + + Pass `module_level=True` if the assignment assigns to a module attribute. + """ kind = "constant" if module_level else "final attribute" self.fail('Can\'t assign to {} "{}"'.format(kind, name), ctx) diff --git a/mypy/nodes.py b/mypy/nodes.py index 9f44fd2e2bac..50e04f02aac3 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -757,7 +757,7 @@ def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None: # Set to true when this variable refers to a module we were unable to # parse for some reason (eg a silenced module) self.is_suppressed_import = False - # Was this defined as Final[...]? + # Was this "variable" (rather a constant) defined as Final[...]? self.is_final = False def name(self) -> str: @@ -930,7 +930,12 @@ class AssignmentStmt(Statement): new_syntax = False # type: bool # Does this assignment define a type alias? is_alias_def = False - # Is this a constant definition? + # Is this a final definition? + # Final attributes can't be re-assigned once set, and can't be overridden + # in a subclass. This flag is not set if an attempted declaration was found to + # be invalid during semantic analysis. It is still set to `True` if + # a final declaration overrides another final declaration (this is checked + # during type checking when MROs are known). is_final_def = False def __init__(self, lvalues: List[Lvalue], rvalue: Expression, diff --git a/mypy/semanal.py b/mypy/semanal.py index e953c03b864c..d5f2642aec8b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -610,7 +610,8 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: # redefinitions already. return - # Check final status + # Check final status, if at least one item or the implementation is marked + # as @final, then the whole overloaded definition if @final. if any(item.is_final for item in defn.items): defn.is_final = True if defn.impl is not None and defn.impl.is_final: @@ -1652,46 +1653,6 @@ def add_type_alias_deps(self, aliases_used: Iterable[str], target = self.scope.current_target() self.cur_mod_node.alias_deps[target].update(aliases_used) - def unwrap_final(self, s: AssignmentStmt) -> None: - if not s.type or not self.is_final_type(s.type): - return - assert isinstance(s.type, UnboundType) - if len(s.type.args) > 1: - self.fail("Final[...] takes at most one type argument", s.type) - if not s.type.args: - s.type = None - else: - s.type = s.type.args[0] - if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], RefExpr): - self.fail("Invalid final declaration, ignoring", s) - return - lval = s.lvalues[0] - assert isinstance(lval, RefExpr) - s.is_final_def = True - if self.type and self.type.is_protocol: - self.msg.protocol_members_cant_be_final(s) - if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file: - self.fail("Final declaration outside stubs must have right hand side", s) - return - - def check_final_implicit_def(self, s: AssignmentStmt) -> None: - if not s.is_final_def: - return - lval = s.lvalues[0] - assert isinstance(lval, RefExpr) - if isinstance(lval, MemberExpr): - if not self.is_self_member_ref(lval): - self.fail("Final can be only applied to a name or an attribute on self", s) - s.is_final_def = False - return - else: - assert self.function_stack - if self.function_stack[-1].name() != '__init__': - self.fail("Can only declare final attributes in class body" - " or __init__ method", s) - s.is_final_def = False - return - def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.unwrap_final(s) @@ -1739,7 +1700,58 @@ def final_cb() -> None: isinstance(s.rvalue, (ListExpr, TupleExpr))): self.add_exports(s.rvalue.items) + def unwrap_final(self, s: AssignmentStmt) -> None: + """Strip Final[...] if present in an assignment. + + This is done to invoke type inference during type checking phase for this + assignment. Also, Final[...] desn't affect type in any way, it is rather an + access qualifier for given `Var`. + """ + if not s.type or not self.is_final_type(s.type): + return + assert isinstance(s.type, UnboundType) + if len(s.type.args) > 1: + self.fail("Final[...] takes at most one type argument", s.type) + if not s.type.args: + s.type = None + else: + s.type = s.type.args[0] + if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], RefExpr): + self.fail("Invalid final declaration, ignoring", s) + return + lval = s.lvalues[0] + assert isinstance(lval, RefExpr) + s.is_final_def = True + if self.type and self.type.is_protocol: + self.msg.protocol_members_cant_be_final(s) + if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file: + self.fail("Final declaration outside stubs must have right hand side", s) + return + + def check_final_implicit_def(self, s: AssignmentStmt) -> None: + """Do basic checks for final declaration on self in __init__. + + Additional re-definition checks are performed by `analyze_lvalue`. + """ + if not s.is_final_def: + return + lval = s.lvalues[0] + assert isinstance(lval, RefExpr) + if isinstance(lval, MemberExpr): + if not self.is_self_member_ref(lval): + self.fail("Final can be only applied to a name or an attribute on self", s) + s.is_final_def = False + return + else: + assert self.function_stack + if self.function_stack[-1].name() != '__init__': + self.fail("Can only declare final attributes in class body" + " or __init__ method", s) + s.is_final_def = False + return + def store_final_status(self, s: AssignmentStmt) -> None: + """If this is a locally valid final declaration, set the corresponding flag on `Var`.""" if s.is_final_def: if len(s.lvalues) == 1 and isinstance(s.lvalues[0], RefExpr): node = s.lvalues[0].node @@ -2013,6 +2025,8 @@ def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False, # If the attribute of self is not defined in superclasses, create a new Var, ... if ((node is None or isinstance(node.node, Var) and node.node.is_abstract_var) or # ... also an explicit declaration on self also creates a new Var. + # Note that `explicit_type` might has been erased for bare `Final`, + # so we alse check if `final_cb` is passed. (cur_node is None and (explicit_type or final_cb is not None))): if self.type.is_protocol and node is None: self.fail("Protocol members cannot be defined via assignment to self", lval) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 31d440c38902..1e4f665344d2 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -1,4 +1,4 @@ --- Test case for final qualifier +-- Test cases for final qualifier -- -- Definitions diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 926e05d249da..bba2e9d265e7 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4884,3 +4884,5 @@ def f(x: str) -> None: pass [out] [out2] main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str" + +-- Test cases for final qualifier diff --git a/test-data/unit/merge.test b/test-data/unit/merge.test index 291197b31e6d..736fa15bfef7 100644 --- a/test-data/unit/merge.test +++ b/test-data/unit/merge.test @@ -1451,19 +1451,3 @@ TypeInfo<0>( X<3> (builtins.int<4>) Y<6> (builtins.int<4>)) MetaclassType(enum.EnumMeta<5>)) - -[case testFinalFlagsUpdatedCorrectlyVar] - -[out] - -[case testFinalFlagsUpdatedCorrectlyInstanceVar] - -[out] - -[case testFinalFlagsUpdatedCorrectlyFunc] - -[out] - -[case testFinalFlagsUpdatedCorrectlyMethod] - -[out] From 8366d3fb9c05aaa65fedc17dd003de7025a46532 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 21 Aug 2018 23:15:14 +0100 Subject: [PATCH 18/52] Add more docs --- docs/source/more_types.rst | 198 ++++++++++++++++++++++++++++--------- 1 file changed, 153 insertions(+), 45 deletions(-) diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 6a66b3625a9e..954f907b8b7a 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -937,87 +937,195 @@ in the first TypedDict. Final attributes of classes and modules *************************************** +There are several situations where static guarantees about non-redefinition +of certain names (or references in general) can be useful. One such example +is module or class level constants, user might want to guard them against +unintentional modifications: + +.. code-block:: python + + RATE = 3000 + + class Base: + DEFAULT_ID = 0 + + # 1000 lines later + + class Derived(Base): + DEFAULT_ID = 1 # this may be unintentional + + RATE = 300 # this too + +Another example is where a user might want to protect certain attributes +from overriding in a subclass: + +.. code-block:: python + + import uuid + + class Snowflake: + """An absolutely unique object in the database""" + def __init__(self) -> None: + self.id = uuid.uuid4() + + # 1000 lines later + + class User(Snowflake): + id = uuid.uuid4() # This has valid type, but the meaning + # may be wrong + +Some other use cases might be solved by using ``@property``, but note that both +above use cases can't be solved this way. For such situations, one might want +to use ``typing.Final``. + Definition syntax ----------------- -General idea: constants (not the same as being read-only). The idea is to provide a static -guarantee that whenever a given attribute is accessed it is always the same value. +The ``typing.Final`` type qualifier indicates that a given name or attribute +should never be re-assigned, re-defined, nor overridden. It can be used in +one of these forms: + +* The simplest one is ``ID: Final = 1``. Note that unlike gor generic classes + this is *not* the same as ``Final[Any]``. Here mypy will infer type ``int``. -* ``a: Final = 1``, *not* the same as ``Final[Any]``, will use inference. -* ``Final[float] = 1``, to avoid invariance -* ``Final[float]`` (stubs only) -* ``self.a: Final = 1`` (also with arg), but *only* in ``__init__`` +* An explicit type ``ID: Final[float] = 1`` can be used as in any + normal assignment. + +* In stub files one can omit the right hand side and just write + ``ID: Final[float]``. + +* Finally, one can define ``self.id: Final = 1`` (also with a type argument), + but this is allowed *only* in ``__init__`` methods. Definition rules ---------------- -* At most one final declaration per module/class for a given name +The are two rules that should be always followed when defining a final name: -.. code-block:: python +* There can be *at most one* final declaration per module or class for + a given attribute: - x: Final = 1 - x: Final = 2 # Error! + .. code-block:: python - class C: - x: Final = 1 - def __init__(self, x: int) -> None: - self.x: Final = x # Error! + ID: Final = 1 + ID: Final = 2 # Error! -Note related to second: mypy doesn't keep two namespaces, only one common. + class SomeCls: + id: Final = 1 + def __init__(self, x: int) -> None: + self.id: Final = x # Error! -* Exactly one assignment to a final attribute + Note that mypy has a single namespace for a class. So there can't be two + class-level and instance-level constants with the same name. -.. code-block:: python +* There must be *exactly one* assignment to a final attribute: + + .. code-block:: python - x = 1 - x: Final = 2 # Error! + ID = 1 + ID: Final = 2 # Error! - y: Final = 1 - y = 2 # Error! + class SomeCls: + ID = 1 + ID: Final = 2 # Error! Using final attributes ---------------------- -* Can't be re-assigned (or shadowed), both internally and externally: +As a result of a final declaration mypy strives to provide the +two following guarantees: -.. code-block:: python +* A final attribute can't be re-assigned (or otherwise re-defined), both + internally and externally: - # file mod.py - from typing import Final + .. code-block:: python - x: Final = 1 + # file mod.py + from typing import Final - # file main.py - from typing import Final + ID: Final = 1 + # file main.py + from typing import Final - import mod - mod.x = 2 # Error! + import mod + mod.ID = 2 # Error, can't assign to constant. - class C: - x: Final = 1 + class SomeCls: + ID: Final = 1 - def meth(self) -> None: - self.x = 2 + def meth(self) -> None: + self.ID = 2 # Error, can't assign to final attribute - class C(D): - ... + class DerivedCls(SomeCls): + ... + + DerivedCls.ID = 2 # Error! + obj: DerivedCls + obj.ID = 2 # Error! + +* A final attribute can't be overridden by a subclass (even with another + explicit final declaration). Note however, that final attributes can + override normal attributes. This also applies to multiple inheritance: + + .. code-block:: python - d: D - d.x = 2 # Error! + class Base: + ID = 0 -* Can't be overriden by a subclass (even with another explicit final). -Final however can override normal attributes. These rules also apply to -multiple inferitance. + class One: + ID: Final = 1 # OK + + class Other: + ID: Final = 2 # OK + + class Combo(One, Other): # Error, cannot override final attribute. + pass Final methods ------------- -Methods, class methods, static methods, properies all can be final -(this includes overloaded methods). +Like with attributes, sometimes it is useful to protect a method from +overriding. In such situations one can use a ``typing.final`` decorator: + +.. code-block:: python + + from typing import final + + class Base: + @final + def common_name(self) -> None: # common signature + ... + + # 1000 lines later + + class Derived(Base): + def common_name(self) -> None: # Error, this overriding might break + # invariants in the base class. + ... + +This ``@final`` decorator can be used with instance methods, class methods, +static methods, and properties (this includes overloaded methods). Final classes ------------- -As a bonus, final classes can't be subclassed. Mypy doesn't provide any -additional festures for them, but some other tools may have some. +As a bonus, applying a ``typing.final`` decorator to a class indicates to mypy +that it can't be subclassed. Mypy doesn't provide any additional features for +final classes, but some other tools may use this information for their benefits. +Plus it serves a verifiable documentation purpose: + +.. code-block:: python + + # file lib.pyi + from typing import final + + @final + class Leaf: + ... + + # file main.py + from lib import Leaf + + class MyLeaf(Leaf): # Error, library author believes this is unsafe + ... From 312cbc7a169320879eb8904e3a95eaf6e237d56a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 21 Aug 2018 23:22:59 +0100 Subject: [PATCH 19/52] Minor fixes in docs --- docs/source/more_types.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 954f907b8b7a..5d81c8246655 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -956,8 +956,8 @@ unintentional modifications: RATE = 300 # this too -Another example is where a user might want to protect certain attributes -from overriding in a subclass: +Another example is where a user might want to protect certain instance +attributes from overriding in a subclass: .. code-block:: python @@ -1073,10 +1073,10 @@ two following guarantees: class Base: ID = 0 - class One: + class One(Base): ID: Final = 1 # OK - class Other: + class Other(Base): ID: Final = 2 # OK class Combo(One, Other): # Error, cannot override final attribute. From ec15a6392e5b359e233f433e587ae5a8b258a5f6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 22 Aug 2018 11:59:45 +0100 Subject: [PATCH 20/52] Some fine-grained stuff; add diff tests --- mypy/server/astdiff.py | 9 ++- mypy/server/aststrip.py | 7 +++ test-data/unit/diff.test | 120 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 129 insertions(+), 7 deletions(-) diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index d95aa1d49c20..2aa2618c5043 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -175,9 +175,14 @@ def snapshot_definition(node: Optional[SymbolNode], signature = snapshot_type(node.type) else: signature = snapshot_untyped_signature(node) - return ('Func', common, node.is_property, node.is_class, node.is_static, signature) + return ('Func', common, + node.is_property, node.is_final, + node.is_class, node.is_static, + signature) elif isinstance(node, Var): - return ('Var', common, snapshot_optional_type(node.type)) + return ('Var', common, + snapshot_optional_type(node.type), + node.is_final) elif isinstance(node, Decorator): # Note that decorated methods are represented by Decorator instances in # a symbol table since we need to preserve information about the diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py index c072cfd9bfc4..e06fbd7a9511 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -111,6 +111,8 @@ def visit_func_def(self, node: FuncDef) -> None: return node.expanded = [] node.type = node.unanalyzed_type + # All nodes are non-final after the first pass. + node.is_final = False # Type variable binder binds tvars before the type is analyzed. # It should be refactored, before that we just undo this change here. # TODO: this will be not necessary when #4814 is fixed. @@ -125,6 +127,9 @@ def visit_decorator(self, node: Decorator) -> None: for expr in node.decorators: expr.accept(self) if self.recurse_into_functions: + # Only touch the final status if we re-process + # a method target + node.var.is_final = False node.func.accept(self) def visit_overloaded_func_def(self, node: OverloadedFuncDef) -> None: @@ -132,6 +137,7 @@ def visit_overloaded_func_def(self, node: OverloadedFuncDef) -> None: return # Revert change made during semantic analysis pass 2. node.items = node.unanalyzed_items.copy() + node.is_final = False super().visit_overloaded_func_def(node) @contextlib.contextmanager @@ -160,6 +166,7 @@ def enter_method(self, info: TypeInfo) -> Iterator[None]: def visit_assignment_stmt(self, node: AssignmentStmt) -> None: node.type = node.unanalyzed_type + node.is_final_def = False if self.type and not self.is_class_body: for lvalue in node.lvalues: self.process_lvalue_in_method(lvalue) diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index 6ca718dccf46..0c618d1ed07e 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -946,17 +946,127 @@ __main__.A.(abstract) __main__.A.g [case testFinalFlagsTriggerVar] +from typing import Final + +x: Final = 1 +y: Final[int] = 1 +same: Final = 0 +class C: + x: Final = 1 + y: Final[int] = 1 + same: Final = 0 + def __init__(self) -> None: + self.z: Final = 1 + self.t: Final[int] = 1 + self.also_same: Final[int] = 0 -[out] +[file next.py] +from typing import Final -[case testFinalFlagsTriggerInstanceVar] +x = 1 +y: int = 1 +same: Final = 0 +class C: + x = 1 + y: int = 1 + same: Final = 0 + def __init__(self) -> None: + self.z = 1 + self.t: int = 1 + self.also_same: Final = 0 +[out] +__main__.C.t +__main__.C.x +__main__.C.y +__main__.C.z +__main__.x +__main__.y -[out] +[case testFinalFlagsTriggerMethod] +from typing import final -[case testFinalFlagsTriggerFunc] +class C: + def meth(self) -> int: pass + @final + def same(self) -> int: pass + @classmethod + def cmeth(cls) -> int: pass +[file next.py] +from typing import final + +class C: + @final + def meth(self) -> int: pass + @final + def same(self) -> int: pass + @final + @classmethod + def cmeth(cls) -> int: pass +[builtins fixtures/classmethod.pyi] +[out] +__main__.C.cmeth +__main__.C.meth + +[case testFinalFlagsTriggerProperty] +from typing import final + +class C: + @final + @property + def p(self) -> int: pass + @final + @property + def same(self) -> str: pass + +[file next.py] +from typing import final + +class C: + @property + def p(self) -> int: pass + @final + @property + def same(self) -> str: pass +[builtins fixtures/property.pyi] [out] +__main__.C.p -[case testFinalFlagsTriggerMethod] +[case testFinalFlagsTriggerMethodOverload] +from typing import final, overload + +class C: + @overload + def m(self, x: int) -> int: ... + @overload + def m(self, x: str) -> str: ... + @final + def m(self, x): + pass + @overload + def same(self, x: int) -> int: ... + @overload + def same(self, x: str) -> str: ... + @final + def same(self, x): + pass +[file next.py] +from typing import final, overload + +class C: + @overload + def m(self, x: int) -> int: ... + @overload + def m(self, x: str) -> str: ... + def m(self, x): + pass + @overload + def same(self, x: int) -> int: ... + @overload + def same(self, x: str) -> str: ... + @final + def same(self, x): + pass [out] +__main__.C.m From c8a8d9b453c544496a5e399f97061e64babce2bb Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 22 Aug 2018 14:44:24 +0100 Subject: [PATCH 21/52] Clarify constant re-export; add few tests --- docs/source/more_types.rst | 7 +++ mypy/semanal.py | 2 + test-data/unit/check-final.test | 24 ++++++++++ test-data/unit/check-incremental.test | 64 +++++++++++++++++++++++++++ 4 files changed, 97 insertions(+) diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 5d81c8246655..502e6408fe58 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -1129,3 +1129,10 @@ Plus it serves a verifiable documentation purpose: class MyLeaf(Leaf): # Error, library author believes this is unsafe ... + +.. note:: + + Mypy treats re-exported final names as final. In other words, once declared, + the final status can't be "stripped". Such behaviour is typically desired + for larger libraries where constants are defined in a separate module and + then re-exported. diff --git a/mypy/semanal.py b/mypy/semanal.py index d5f2642aec8b..1d67be27c8a5 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1455,6 +1455,8 @@ def visit_import_from(self, imp: ImportFrom) -> None: # 'from m import x as x' exports x in a stub file. module_public = not self.is_stub_file or as_id is not None module_hidden = not module_public and possible_module_id not in self.modules + # NOTE: we take the original node even for final `Var`s. This is to support + # a common pattern when constants are re-exported (same applies to import *). symbol = SymbolTableNode(node.kind, node.node, module_public=module_public, module_hidden=module_hidden) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 1e4f665344d2..13d43a47eca7 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -270,6 +270,30 @@ z: Final = 2 # E: Name 'z' already defined on line 14 \ z = 3 # E: Can't assign to constant "z" [out] +[case testFinalReassignModuleReexport] +from typing import Final + +from lib import X +from lib.mod import ID + +X = 1 # E: Can't assign to constant "X" +ID: Final = 1 # E: Name 'ID' already defined (possibly by an import) \ + # E: Final can't redefine an existing name, ignoring \ + # E: Can't assign to constant "ID" +ID = 1 # E: Can't assign to constant "ID" +[file lib/__init__.pyi] +from lib.const import X as X + +[file lib/mod.pyi] +from lib.const import * + +[file lib/const.pyi] +from typing import Final + +ID: Final +X: Final[int] +[out] + [case testFinalReassignFuncScope] from typing import Final diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index bba2e9d265e7..e4f40680591e 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4886,3 +4886,67 @@ def f(x: str) -> None: pass main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str" -- Test cases for final qualifier + +[case testFinalAddFinalVarAssign] +import mod +from a import D +from mod import x + +mod.x = 2 # This an all below are errors. +x = 2 +d: D +d.y = 2 +d.z = 2 +D.y = 2 +[file a.py] +import mod + +class D(mod.C): + pass +[file mod.py] +x = 1 +class C: + y = 2 + def __init__(self) -> None: + self.z = 1 + +[file mod.py.2] +from typing import Final + +x: Final = 1 +class C: + y: Final = 2 + def __init__(self) -> None: + self.z: Final = 1 +[out] +[out2] +main:5: error: Can't assign to constant "x" +main:6: error: Can't assign to constant "x" +main:8: error: Can't assign to final attribute "y" +main:9: error: Can't assign to final attribute "z" +main:10: error: Can't assign to final attribute "y" + +[case testFinalAddFinalVarOverride] + +[out] +[out2] + +[case testFinalAddFinalMethodOverride] + +[out] +[out2] + +[case testFinalAddFinalMethodOverrideWithVar] + +[out] +[out2] + +[case testFinalAddFinalMethodOverrideOverload] + +[out] +[out2] + +[case testFinalAddFinalPropertyWithVar] + +[out] +[out2] From 157a93640547d6ad704a9543bf5ffe24125e77d7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 22 Aug 2018 15:12:25 +0100 Subject: [PATCH 22/52] Add more incremental tests (coarse and fine) --- test-data/unit/check-incremental.test | 134 +++++++++++++++++- test-data/unit/fine-grained.test | 194 ++++++++++++++++++++++++++ 2 files changed, 326 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index e4f40680591e..a98a704cf78c 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4906,7 +4906,7 @@ class D(mod.C): [file mod.py] x = 1 class C: - y = 2 + y = 1 def __init__(self) -> None: self.z = 1 @@ -4915,7 +4915,7 @@ from typing import Final x: Final = 1 class C: - y: Final = 2 + y: Final = 1 def __init__(self) -> None: self.z: Final = 1 [out] @@ -4927,26 +4927,156 @@ main:9: error: Can't assign to final attribute "z" main:10: error: Can't assign to final attribute "y" [case testFinalAddFinalVarOverride] +from mod import C + +class D(C): + x = 2 + def __init__(self) -> None: + self.y = 2 +class E(C): + y = 2 + def __init__(self) -> None: + self.x = 2 + +[file mod.py] +class C: + x = 1 + def __init__(self) -> None: + self.y = 1 +[file mod.py.2] +from typing import Final + +class C: + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 [out] [out2] +main:4: error: Cannot override final attribute "x" +main:4: note: (previously declared on base class "C") +main:4: error: Can't assign to constant "x" +main:6: error: Can't assign to final attribute "y" +main:8: error: Cannot override final attribute "y" +main:8: note: (previously declared on base class "C") +main:8: error: Can't assign to constant "y" +main:10: error: Can't assign to final attribute "x" [case testFinalAddFinalMethodOverride] +from mod import C + +class D(C): + def meth(self) -> int: ... + +[file mod.py] +class C: + def meth(self) -> int: ... + +[file mod.py.2] +from typing import final +class C: + @final + def meth(self) -> int: ... [out] [out2] +main:4: error: Cannot override final attribute "meth" +main:4: note: (previously declared on base class "C") [case testFinalAddFinalMethodOverrideWithVar] +from mod import C +from typing import Any + +class D(C): + meth: Any = 2 + def __init__(self) -> None: + self.other: Any = 2 + +[file mod.py] +class C: + def meth(self) -> int: ... + def other(self) -> int: ... +[file mod.py.2] +from typing import final + +class C: + @final + def meth(self) -> int: ... + @final + def other(self) -> int: ... [out] [out2] +main:5: error: Cannot override final attribute "meth" +main:5: note: (previously declared on base class "C") +main:5: error: Can't assign to constant "meth" +main:7: error: Can't assign to final attribute "other" +main:7: error: Cannot override final attribute "other" +main:7: note: (previously declared on base class "C") [case testFinalAddFinalMethodOverrideOverload] +from typing import overload +from mod import C + +class D(C): + @overload + def meth(self, x: int) -> int: ... + @overload + def meth(self, x: str) -> str: ... + def meth(self, x): + pass + +[file mod.pyi] +from typing import overload +class C: + @overload + def meth(self, x: int) -> int: ... + @overload + def meth(self, x: str) -> str: ... +[file mod.pyi.2] +from typing import final, overload + +class C: + @final + @overload + def meth(self, x: int) -> int: ... + @overload + def meth(self, x: str) -> str: ... [out] [out2] +main:5: error: Cannot override final attribute "meth" +main:5: note: (previously declared on base class "C") [case testFinalAddFinalPropertyWithVar] +from mod import C + +class D(C): + p = 2 +class E(C): + def __init__(self) -> None: + self.p: int = 2 + +[file mod.py] +class C: + @property + def p(self) -> int: + pass + +[file mod.py.2] +from typing import final +class C: + @final + @property + def p(self) -> int: + pass +[builtins fixtures/property.pyi] [out] [out2] +main:4: error: Cannot override final attribute "p" +main:4: note: (previously declared on base class "C") +main:4: error: Can't assign to constant "p" +main:7: error: Can't assign to final attribute "p" +main:7: error: Cannot override final attribute "p" +main:7: note: (previously declared on base class "C") diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 1bbbaca76dd6..cfb7c946de7b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7532,3 +7532,197 @@ Func = Callable[..., Any] == -- Test cases for final qualifier + +[case testFinalAddFinalVarAssignFine] +import mod +from a import D +from mod import x + +mod.x = 2 # This an all below are errors. +x = 2 +d: D +d.y = 2 +d.z = 2 +D.y = 2 +[file a.py] +import mod + +class D(mod.C): + pass +[file mod.py] +x = 1 +class C: + y = 1 + def __init__(self) -> None: + self.z = 1 + +[file mod.py.2] +from typing import Final + +x: Final = 1 +class C: + y: Final = 1 + def __init__(self) -> None: + self.z: Final = 1 +[out] +== +main:5: error: Can't assign to constant "x" +main:6: error: Can't assign to constant "x" +main:8: error: Can't assign to final attribute "y" +main:9: error: Can't assign to final attribute "z" +main:10: error: Can't assign to final attribute "y" + +[case testFinalAddFinalVarOverrideFine] +from mod import C + +class D(C): + x = 2 + def __init__(self) -> None: + self.y = 2 +class E(C): + y = 2 + def __init__(self) -> None: + self.x = 2 + +[file mod.py] +class C: + x = 1 + def __init__(self) -> None: + self.y = 1 + +[file mod.py.2] +from typing import Final + +class C: + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 +[out] +== +main:4: error: Cannot override final attribute "x" +main:4: note: (previously declared on base class "C") +main:4: error: Can't assign to constant "x" +main:6: error: Can't assign to final attribute "y" +main:8: error: Cannot override final attribute "y" +main:8: note: (previously declared on base class "C") +main:8: error: Can't assign to constant "y" +main:10: error: Can't assign to final attribute "x" + +[case testFinalAddFinalMethodOverrideFine] +from mod import C + +class D(C): + def meth(self) -> int: ... + +[file mod.py] +class C: + def meth(self) -> int: ... + +[file mod.py.2] +from typing import final + +class C: + @final + def meth(self) -> int: ... +[out] +== +main:4: error: Cannot override final attribute "meth" +main:4: note: (previously declared on base class "C") + +[case testFinalAddFinalMethodOverrideWithVarFine] +from mod import C +from typing import Any + +class D(C): + meth: Any = 2 + def __init__(self) -> None: + self.other: Any = 2 + +[file mod.py] +class C: + def meth(self) -> int: ... + def other(self) -> int: ... + +[file mod.py.2] +from typing import final + +class C: + @final + def meth(self) -> int: ... + @final + def other(self) -> int: ... +[out] +== +main:5: error: Cannot override final attribute "meth" +main:5: note: (previously declared on base class "C") +main:5: error: Can't assign to constant "meth" +main:7: error: Can't assign to final attribute "other" +main:7: error: Cannot override final attribute "other" +main:7: note: (previously declared on base class "C") + +[case testFinalAddFinalMethodOverrideOverloadFine] +from typing import overload +from mod import C + +class D(C): + @overload + def meth(self, x: int) -> int: ... + @overload + def meth(self, x: str) -> str: ... + def meth(self, x): + pass + +[file mod.pyi] +from typing import overload +class C: + @overload + def meth(self, x: int) -> int: ... + @overload + def meth(self, x: str) -> str: ... + +[file mod.pyi.2] +from typing import final, overload + +class C: + @final + @overload + def meth(self, x: int) -> int: ... + @overload + def meth(self, x: str) -> str: ... +[out] +== +main:5: error: Cannot override final attribute "meth" +main:5: note: (previously declared on base class "C") + +[case testFinalAddFinalPropertyWithVarFine] +from mod import C + +class D(C): + p = 2 +class E(C): + def __init__(self) -> None: + self.p: int = 2 + +[file mod.py] +class C: + @property + def p(self) -> int: + pass + +[file mod.py.2] +from typing import final + +class C: + @final + @property + def p(self) -> int: + pass +[builtins fixtures/property.pyi] +[out] +== +main:4: error: Cannot override final attribute "p" +main:4: note: (previously declared on base class "C") +main:4: error: Can't assign to constant "p" +main:7: error: Can't assign to final attribute "p" +main:7: error: Cannot override final attribute "p" +main:7: note: (previously declared on base class "C") From eb945c910d38084904493aa9b927c9578d7bdf1b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 22 Aug 2018 15:26:31 +0100 Subject: [PATCH 23/52] Tweak some targets in fine-grained tests --- test-data/unit/fine-grained.test | 60 +++++++++++++++++--------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index cfb7c946de7b..e7c7256947f0 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7538,12 +7538,14 @@ import mod from a import D from mod import x -mod.x = 2 # This an all below are errors. x = 2 -d: D -d.y = 2 -d.z = 2 -D.y = 2 +def outer() -> None: + mod.x = 2 + x = 2 # This is OK because it creates a local variable + d: D + d.y = 2 + d.z = 2 + D.y = 2 [file a.py] import mod @@ -7567,10 +7569,10 @@ class C: [out] == main:5: error: Can't assign to constant "x" -main:6: error: Can't assign to constant "x" -main:8: error: Can't assign to final attribute "y" -main:9: error: Can't assign to final attribute "z" +main:7: error: Can't assign to constant "x" main:10: error: Can't assign to final attribute "y" +main:11: error: Can't assign to final attribute "z" +main:12: error: Can't assign to final attribute "y" [case testFinalAddFinalVarOverrideFine] from mod import C @@ -7664,13 +7666,14 @@ main:7: note: (previously declared on base class "C") from typing import overload from mod import C -class D(C): - @overload - def meth(self, x: int) -> int: ... - @overload - def meth(self, x: str) -> str: ... - def meth(self, x): - pass +def outer() -> None: + class D(C): + @overload + def meth(self, x: int) -> int: ... + @overload + def meth(self, x: str) -> str: ... + def meth(self, x): + pass [file mod.pyi] from typing import overload @@ -7691,17 +7694,18 @@ class C: def meth(self, x: str) -> str: ... [out] == -main:5: error: Cannot override final attribute "meth" -main:5: note: (previously declared on base class "C") +main:6: error: Cannot override final attribute "meth" +main:6: note: (previously declared on base class "C") [case testFinalAddFinalPropertyWithVarFine] from mod import C -class D(C): - p = 2 -class E(C): - def __init__(self) -> None: - self.p: int = 2 +def outer() -> None: + class D(C): + p = 2 + class E(C): + def __init__(self) -> None: + self.p: int = 2 [file mod.py] class C: @@ -7720,9 +7724,9 @@ class C: [builtins fixtures/property.pyi] [out] == -main:4: error: Cannot override final attribute "p" -main:4: note: (previously declared on base class "C") -main:4: error: Can't assign to constant "p" -main:7: error: Can't assign to final attribute "p" -main:7: error: Cannot override final attribute "p" -main:7: note: (previously declared on base class "C") +main:5: error: Cannot override final attribute "p" +main:5: note: (previously declared on base class "C") +main:5: error: Can't assign to constant "p" +main:8: error: Can't assign to final attribute "p" +main:8: error: Cannot override final attribute "p" +main:8: note: (previously declared on base class "C") From 58e4243bfa4920e5d550ff839c246d65fc1b040c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 22 Aug 2018 15:32:15 +0100 Subject: [PATCH 24/52] Only keep basic tests in coarse-grained --- test-data/unit/check-incremental.test | 98 --------------------------- 1 file changed, 98 deletions(-) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index a98a704cf78c..5e5ddbf2b6e6 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4982,101 +4982,3 @@ class C: [out2] main:4: error: Cannot override final attribute "meth" main:4: note: (previously declared on base class "C") - -[case testFinalAddFinalMethodOverrideWithVar] -from mod import C -from typing import Any - -class D(C): - meth: Any = 2 - def __init__(self) -> None: - self.other: Any = 2 - -[file mod.py] -class C: - def meth(self) -> int: ... - def other(self) -> int: ... - -[file mod.py.2] -from typing import final - -class C: - @final - def meth(self) -> int: ... - @final - def other(self) -> int: ... -[out] -[out2] -main:5: error: Cannot override final attribute "meth" -main:5: note: (previously declared on base class "C") -main:5: error: Can't assign to constant "meth" -main:7: error: Can't assign to final attribute "other" -main:7: error: Cannot override final attribute "other" -main:7: note: (previously declared on base class "C") - -[case testFinalAddFinalMethodOverrideOverload] -from typing import overload -from mod import C - -class D(C): - @overload - def meth(self, x: int) -> int: ... - @overload - def meth(self, x: str) -> str: ... - def meth(self, x): - pass - -[file mod.pyi] -from typing import overload -class C: - @overload - def meth(self, x: int) -> int: ... - @overload - def meth(self, x: str) -> str: ... - -[file mod.pyi.2] -from typing import final, overload - -class C: - @final - @overload - def meth(self, x: int) -> int: ... - @overload - def meth(self, x: str) -> str: ... -[out] -[out2] -main:5: error: Cannot override final attribute "meth" -main:5: note: (previously declared on base class "C") - -[case testFinalAddFinalPropertyWithVar] -from mod import C - -class D(C): - p = 2 -class E(C): - def __init__(self) -> None: - self.p: int = 2 - -[file mod.py] -class C: - @property - def p(self) -> int: - pass - -[file mod.py.2] -from typing import final - -class C: - @final - @property - def p(self) -> int: - pass -[builtins fixtures/property.pyi] -[out] -[out2] -main:4: error: Cannot override final attribute "p" -main:4: note: (previously declared on base class "C") -main:4: error: Can't assign to constant "p" -main:7: error: Can't assign to final attribute "p" -main:7: error: Cannot override final attribute "p" -main:7: note: (previously declared on base class "C") From 8602c61fae4569f3bf54ea757dd8c4e73b425089 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 22 Aug 2018 18:15:32 +0100 Subject: [PATCH 25/52] Store final value for simple literals --- mypy/nodes.py | 5 +++++ mypy/semanal.py | 8 +++++++ mypy/strconv.py | 2 ++ mypy/treetransform.py | 5 +++++ test-data/unit/semanal-basic.test | 36 +++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+) diff --git a/mypy/nodes.py b/mypy/nodes.py index 50e04f02aac3..4a0bf57aac2c 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -720,6 +720,7 @@ class Var(SymbolNode): '_fullname', 'info', 'type', + 'final_value', 'is_self', 'is_ready', 'is_inferred', @@ -759,6 +760,10 @@ def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None: self.is_suppressed_import = False # Was this "variable" (rather a constant) defined as Final[...]? self.is_final = False + # If constant value is a simple literal, + # store the literal value (unboxed) for the benefit of + # tools like mypyc. + self.final_value = None # type: Optional[Union[int, float, bool, str]] def name(self) -> str: return self._name diff --git a/mypy/semanal.py b/mypy/semanal.py index 1d67be27c8a5..d1bc4749726e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1759,6 +1759,14 @@ def store_final_status(self, s: AssignmentStmt) -> None: node = s.lvalues[0].node if isinstance(node, Var): node.is_final = True + node.final_value = self.unbox_literal(s.rvalue) + + def unbox_literal(self, e: Expression) -> Optional[Union[int, float, bool, str]]: + if isinstance(e, (IntExpr, FloatExpr, StrExpr)): + return e.value + elif isinstance(e, NameExpr) and e.name in ('True', 'False'): + return True if e.name == 'True' else False + return None def analyze_simple_literal_type(self, rvalue: Expression) -> Optional[Type]: """Return builtins.int if rvalue is an int literal, etc.""" diff --git a/mypy/strconv.py b/mypy/strconv.py index 1a2b5911aa5d..58771a9f6466 100644 --- a/mypy/strconv.py +++ b/mypy/strconv.py @@ -344,6 +344,8 @@ def visit_star_expr(self, o: 'mypy.nodes.StarExpr') -> str: def visit_name_expr(self, o: 'mypy.nodes.NameExpr') -> str: pretty = self.pretty_name(o.name, o.kind, o.fullname, o.is_inferred_def, o.node) + if isinstance(o.node, mypy.nodes.Var) and o.node.is_final: + pretty += ' = {}'.format(o.node.final_value) return short_type(o) + '(' + pretty + ')' def pretty_name(self, name: str, kind: Optional[int], fullname: Optional[str], diff --git a/mypy/treetransform.py b/mypy/treetransform.py index df1c678b98d7..2a2410a14472 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -116,6 +116,7 @@ def visit_func_def(self, node: FuncDef) -> FuncDef: new.is_static = node.is_static new.is_class = node.is_class new.is_property = node.is_property + new.is_final = node.is_final new.original_def = node.original_def if node in self.func_placeholder_map: @@ -156,6 +157,7 @@ def visit_overloaded_func_def(self, node: OverloadedFuncDef) -> OverloadedFuncDe new.is_static = node.is_static new.is_class = node.is_class new.is_property = node.is_property + new.is_final = node.is_final if node.impl: new.impl = cast(OverloadPart, node.impl.accept(self)) return new @@ -204,6 +206,8 @@ def visit_var(self, node: Var) -> Var: new.is_staticmethod = node.is_staticmethod new.is_classmethod = node.is_classmethod new.is_property = node.is_property + new.is_final = node.is_final + new.final_value = node.final_value new.set_line(node.line) self.var_map[node] = new return new @@ -219,6 +223,7 @@ def duplicate_assignment(self, node: AssignmentStmt) -> AssignmentStmt: self.expr(node.rvalue), self.optional_type(node.type)) new.line = node.line + new.is_final_def = node.is_final_def return new def visit_operator_assignment_stmt(self, diff --git a/test-data/unit/semanal-basic.test b/test-data/unit/semanal-basic.test index 08b27e843d26..238544ff8ea9 100644 --- a/test-data/unit/semanal-basic.test +++ b/test-data/unit/semanal-basic.test @@ -454,3 +454,39 @@ MypyFile:1( AssignmentStmt:3( NameExpr(x* [l]) IntExpr(1))))))) + +[case testFinalValuesOnVar] +from typing import Final, Any + +def func() -> Any: ... +x: Final = 1 +y: Final = 1.0 +s: Final = "hi" +t: Final = True +n: Final = func() +[out] +MypyFile:1( + ImportFrom:1(typing, [Final, Any]) + FuncDef:3( + func + def () -> Any + Block:3( + ExpressionStmt:3( + Ellipsis))) + AssignmentStmt:4( + NameExpr(x* [__main__.x] = 1) + IntExpr(1)) + AssignmentStmt:5( + NameExpr(y* [__main__.y] = 1.0) + FloatExpr(1.0)) + AssignmentStmt:6( + NameExpr(s* [__main__.s] = hi) + StrExpr(hi)) + AssignmentStmt:7( + NameExpr(t* [__main__.t] = True) + NameExpr(True [builtins.True])) + AssignmentStmt:8( + NameExpr(n* [__main__.n] = None) + CallExpr:8( + NameExpr(func [__main__.func]) + Args()))) From 7e05f7ddf3f62b87230bb0ddbe6e915cf232e60f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 23 Aug 2018 18:40:45 +0100 Subject: [PATCH 26/52] Prohibit augmented assignments and final within loops --- docs/source/more_types.rst | 4 ++++ mypy/checker.py | 20 ++++++++++++++----- mypy/checkexpr.py | 5 ++++- mypy/semanal.py | 2 ++ test-data/unit/check-final.test | 34 +++++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 502e6408fe58..2a917b13d245 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -1029,6 +1029,10 @@ The are two rules that should be always followed when defining a final name: ID = 1 ID: Final = 2 # Error! +.. note:: + Conditional final declarations and final declarations within loops are + not supported. + Using final attributes ---------------------- diff --git a/mypy/checker.py b/mypy/checker.py index bb3eb3303d88..05f1080304c9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1902,14 +1902,18 @@ def set_final_context(self, is_final_def: bool) -> Iterator[None]: finally: self._is_final_def = old_ctx - def check_final(self, s: AssignmentStmt) -> None: + def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: """Check if this assignment does not assign to a final attribute. This function perfoms the check only for name assignments at module and class scope. The assignments to `obj.attr` and `Cls.attr` are checked in checkmember.py. """ - lvs = self.flatten_lvalues(s.lvalues) + if isinstance(s, AssignmentStmt): + lvs = self.flatten_lvalues(s.lvalues) + else: + lvs = [s.lvalue] + is_final_decl = s.is_final_def if isinstance(s, AssignmentStmt) else False for lv in lvs: if isinstance(lv, RefExpr) and isinstance(lv.node, Var): name = lv.node.name() @@ -1922,9 +1926,9 @@ def check_final(self, s: AssignmentStmt) -> None: sym = base.names.get(name) if sym and isinstance(sym.node, (Var, Decorator)): var = sym.node if isinstance(sym.node, Var) else sym.node.var - if var.is_final and not s.is_final_def: + if var.is_final and not is_final_decl: self.msg.cant_assign_to_final(name, var.info is not None, s) - if lv.node.is_final and not s.is_final_def: + if lv.node.is_final and not is_final_decl: self.msg.cant_assign_to_final(name, lv.node.info is not None, s) def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression, @@ -2574,7 +2578,12 @@ def visit_while_stmt(self, s: WhileStmt) -> None: def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: """Type check an operator assignment statement, e.g. x += 1.""" - lvalue_type = self.expr_checker.accept(s.lvalue) + if isinstance(s.lvalue, MemberExpr): + # Special case, some additional errors may be given for + # assignments to read-only or final attributes. + lvalue_type = self.expr_checker._visit_member_expr(s.lvalue, True) + else: + lvalue_type = self.expr_checker.accept(s.lvalue) inplace, method = infer_operator_assignment_method(lvalue_type, s.op) if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) @@ -2588,6 +2597,7 @@ def visit_operator_assignment_stmt(self, expr.set_line(s) self.check_assignment(lvalue=s.lvalue, rvalue=expr, infer_lvalue_type=True, new_syntax=False) + self.check_final(s) def visit_assert_stmt(self, s: AssertStmt) -> None: self.expr_checker.accept(s.expr) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index ee9f28ec9e83..5b265a48965f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1647,8 +1647,11 @@ def apply_generic_arguments(self, callable: CallableType, types: Sequence[Option def visit_member_expr(self, e: MemberExpr) -> Type: """Visit member expression (of form e.id).""" + return self._visit_member_expr(e, False) + + def _visit_member_expr(self, e: MemberExpr, is_lvalue: bool) -> Type: self.chk.module_refs.update(extract_refexpr_names(e)) - result = self.analyze_ordinary_member_access(e, False) + result = self.analyze_ordinary_member_access(e, is_lvalue) return self.narrow_type_from_binder(e, result) def analyze_ordinary_member_access(self, e: MemberExpr, diff --git a/mypy/semanal.py b/mypy/semanal.py index d1bc4749726e..f2dfed79732e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1724,6 +1724,8 @@ def unwrap_final(self, s: AssignmentStmt) -> None: lval = s.lvalues[0] assert isinstance(lval, RefExpr) s.is_final_def = True + if self.loop_depth > 0: + self.fail("Final declarations are prohibited within loops", s) if self.type and self.type.is_protocol: self.msg.protocol_members_cant_be_final(s) if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 13d43a47eca7..5d1a5729d72b 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -246,6 +246,17 @@ class P(Protocol): pass [out] +[case testFinalNotInLoops] +from typing import Final + +for i in [1, 2, 3]: + x: Final = i # E: Final declarations are prohibited within loops + +while True: + y: Final = True # E: Final declarations are prohibited within loops +[builtins fixtures/list.pyi] +[out] + -- Reassignments [case testFinalReassignModuleVar] @@ -412,6 +423,29 @@ t, *y, s = u = [2, 2, 2] # E: Can't assign to constant "y" [builtins fixtures/list.pyi] [out] +[case testFinalInplaceAssign] +from typing import Final + +class A: # no such things in fixtures + def __add__(self, other: A) -> A: ... +class B: + def __add__(self, other: B) -> B: ... + def __iadd__(self, other: B) -> B: ... + +a: Final = A() +b: Final = B() +class C: + a: Final = A() + b: Final = B() +class D(C): + pass + +a += A() # E: Can't assign to constant "a" +b += B() # E: Can't assign to constant "b" +D().a += A() # E: Can't assign to final attribute "a" +D().b += B() # E: Can't assign to final attribute "b" +[out] + -- Overriding [case testFinalOverridingVarClassBody] From fa7f8d5a5cd729d5160079242d1b16a947bde7eb Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 27 Aug 2018 00:10:05 +0100 Subject: [PATCH 27/52] Address CR --- mypy/checker.py | 8 +++++--- mypy/checkmember.py | 22 +++++++++++----------- mypy/test/data.py | 9 ++++++++- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 05f1080304c9..2c511d7d337a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -217,9 +217,11 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option # If True, process function definitions. If False, don't. This is used # for processing module top levels in fine-grained incremental mode. self.recurse_into_functions = True - # This internal flag is used to indicate whether we a currently type-checking - # a final declaration (assignment). Should not be set manually, use - # get_final_context/set_final_context instead. + # This internal flag is used to track whether we a currently type-checking + # a final declaration (assignment), so that some errors should be suppressed. + # Should not be set manually, use get_final_context/set_final_context instead. + # NOTE: we use the context manager to avoid "threading" an additional `is_final_def` + # argument through various `checker` and `checkmember` functions. self._is_final_def = False def reset(self) -> None: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 3dc4d95c3cc6..ca660ea33a64 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -266,10 +266,7 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, # An assignment to final attribute is always an error, # independently of types. if is_lvalue and not chk.get_final_context(): - for base in info.mro: - sym = base.names.get(name) - if sym and isinstance(sym.node, (Var, FuncBase, Decorator)) and sym.node.is_final: - msg.cant_assign_to_final(name, module_level=False, ctx=node) + check_final_member(name, info, msg, node) return analyze_var(name, v, itype, info, node, is_lvalue, msg, original_type, builtin_type, not_ready_callback, @@ -313,6 +310,14 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, return msg.has_no_attr(original_type, itype, name, node) +def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Context) -> None: + """Give an error if the name being assigned was declared as final.""" + for base in info.mro: + sym = base.names.get(name) + if sym and isinstance(sym.node, (Var, FuncBase, Decorator)) and sym.node.is_final: + msg.cant_assign_to_final(name, module_level=False, ctx=ctx) + + def analyze_descriptor_access(instance_type: Type, descriptor_type: Type, builtin_type: Callable[[str], Instance], msg: MessageBuilder, @@ -551,13 +556,8 @@ def analyze_class_attribute_access(itype: Instance, # An assignment to final attribute on class object is also always an error, # independently of types. - for base in itype.type.mro: - b_node = base.names.get(name) - if (b_node and isinstance(b_node.node, (Var, FuncBase, Decorator)) and - is_lvalue and b_node.node.is_final - and not chk.get_final_context()): - name = b_node.node.name() - msg.cant_assign_to_final(name, module_level=False, ctx=context) + if is_lvalue and not chk.get_final_context(): + check_final_member(name, itype.type, msg, context) if itype.type.is_enum and not (is_lvalue or is_decorated or is_method): return itype diff --git a/mypy/test/data.py b/mypy/test/data.py index 873ded377b6e..6b144681cfaf 100644 --- a/mypy/test/data.py +++ b/mypy/test/data.py @@ -5,6 +5,7 @@ import tempfile import posixpath import re +import sys from os import remove, rmdir import shutil from abc import abstractmethod @@ -187,6 +188,12 @@ def parse_test_cases(parent: 'DataSuiteCollector', suite: 'DataSuite', path, p[i0].line)) +if sys.version_info >= (3, 5): + TmpDir = tempfile.TemporaryDirectory[str] +else: + TmpDir = tempfile.TemporaryDirectory + + class DataDrivenTestCase(pytest.Item): # type: ignore # inheriting from Any """Holds parsed data-driven test cases, and handles directory setup and teardown.""" @@ -229,7 +236,7 @@ def __init__(self, super().__init__(name, parent) self.skip = skip self.old_cwd = None # type: Optional[str] - self.tmpdir = None # type: Optional[tempfile.TemporaryDirectory[str]] + self.tmpdir = None # type: Optional[TmpDir] self.input = input self.output = output self.output2 = output2 From 8d2973cd2651f95e495f7dbbf6a9b6fbafdfeba6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 27 Aug 2018 00:22:02 +0100 Subject: [PATCH 28/52] Fix alias on newer Python versions --- mypy/test/data.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mypy/test/data.py b/mypy/test/data.py index 6b144681cfaf..f785b2e3948e 100644 --- a/mypy/test/data.py +++ b/mypy/test/data.py @@ -188,10 +188,12 @@ def parse_test_cases(parent: 'DataSuiteCollector', suite: 'DataSuite', path, p[i0].line)) -if sys.version_info >= (3, 5): - TmpDir = tempfile.TemporaryDirectory[str] -else: - TmpDir = tempfile.TemporaryDirectory +MYPY = False +if MYPY: + if sys.version_info >= (3, 5): + TmpDir = tempfile.TemporaryDirectory[str] + else: + TmpDir = tempfile.TemporaryDirectory class DataDrivenTestCase(pytest.Item): # type: ignore # inheriting from Any From 11a157ecf60ba11d7f82a7409d22a5f3fe6f5c81 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 31 Aug 2018 13:21:58 +0100 Subject: [PATCH 29/52] Move final docs to a separate file --- docs/source/index.rst | 1 + docs/source/more_types.rst | 207 ------------------------------- docs/source/type_quals.rst | 245 +++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 207 deletions(-) create mode 100644 docs/source/type_quals.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 9c7e043f2822..3a80e8f09fe4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,6 +39,7 @@ Mypy is a static type checker for Python 3 and Python 2.7. stubs generics more_types + type_quals metaclasses .. toctree:: diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 2a917b13d245..e16b9a34cd74 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -933,210 +933,3 @@ and non-required keys, such as ``Movie`` above, will only be compatible with another TypedDict if all required keys in the other TypedDict are required keys in the first TypedDict, and all non-required keys of the other TypedDict are also non-required keys in the first TypedDict. - -Final attributes of classes and modules -*************************************** - -There are several situations where static guarantees about non-redefinition -of certain names (or references in general) can be useful. One such example -is module or class level constants, user might want to guard them against -unintentional modifications: - -.. code-block:: python - - RATE = 3000 - - class Base: - DEFAULT_ID = 0 - - # 1000 lines later - - class Derived(Base): - DEFAULT_ID = 1 # this may be unintentional - - RATE = 300 # this too - -Another example is where a user might want to protect certain instance -attributes from overriding in a subclass: - -.. code-block:: python - - import uuid - - class Snowflake: - """An absolutely unique object in the database""" - def __init__(self) -> None: - self.id = uuid.uuid4() - - # 1000 lines later - - class User(Snowflake): - id = uuid.uuid4() # This has valid type, but the meaning - # may be wrong - -Some other use cases might be solved by using ``@property``, but note that both -above use cases can't be solved this way. For such situations, one might want -to use ``typing.Final``. - -Definition syntax ------------------ - -The ``typing.Final`` type qualifier indicates that a given name or attribute -should never be re-assigned, re-defined, nor overridden. It can be used in -one of these forms: - -* The simplest one is ``ID: Final = 1``. Note that unlike gor generic classes - this is *not* the same as ``Final[Any]``. Here mypy will infer type ``int``. - -* An explicit type ``ID: Final[float] = 1`` can be used as in any - normal assignment. - -* In stub files one can omit the right hand side and just write - ``ID: Final[float]``. - -* Finally, one can define ``self.id: Final = 1`` (also with a type argument), - but this is allowed *only* in ``__init__`` methods. - -Definition rules ----------------- - -The are two rules that should be always followed when defining a final name: - -* There can be *at most one* final declaration per module or class for - a given attribute: - - .. code-block:: python - - ID: Final = 1 - ID: Final = 2 # Error! - - class SomeCls: - id: Final = 1 - def __init__(self, x: int) -> None: - self.id: Final = x # Error! - - Note that mypy has a single namespace for a class. So there can't be two - class-level and instance-level constants with the same name. - -* There must be *exactly one* assignment to a final attribute: - - .. code-block:: python - - ID = 1 - ID: Final = 2 # Error! - - class SomeCls: - ID = 1 - ID: Final = 2 # Error! - -.. note:: - Conditional final declarations and final declarations within loops are - not supported. - -Using final attributes ----------------------- - -As a result of a final declaration mypy strives to provide the -two following guarantees: - -* A final attribute can't be re-assigned (or otherwise re-defined), both - internally and externally: - - .. code-block:: python - - # file mod.py - from typing import Final - - ID: Final = 1 - - # file main.py - from typing import Final - - import mod - mod.ID = 2 # Error, can't assign to constant. - - class SomeCls: - ID: Final = 1 - - def meth(self) -> None: - self.ID = 2 # Error, can't assign to final attribute - - class DerivedCls(SomeCls): - ... - - DerivedCls.ID = 2 # Error! - obj: DerivedCls - obj.ID = 2 # Error! - -* A final attribute can't be overridden by a subclass (even with another - explicit final declaration). Note however, that final attributes can - override normal attributes. This also applies to multiple inheritance: - - .. code-block:: python - - class Base: - ID = 0 - - class One(Base): - ID: Final = 1 # OK - - class Other(Base): - ID: Final = 2 # OK - - class Combo(One, Other): # Error, cannot override final attribute. - pass - -Final methods -------------- - -Like with attributes, sometimes it is useful to protect a method from -overriding. In such situations one can use a ``typing.final`` decorator: - -.. code-block:: python - - from typing import final - - class Base: - @final - def common_name(self) -> None: # common signature - ... - - # 1000 lines later - - class Derived(Base): - def common_name(self) -> None: # Error, this overriding might break - # invariants in the base class. - ... - -This ``@final`` decorator can be used with instance methods, class methods, -static methods, and properties (this includes overloaded methods). - -Final classes -------------- - -As a bonus, applying a ``typing.final`` decorator to a class indicates to mypy -that it can't be subclassed. Mypy doesn't provide any additional features for -final classes, but some other tools may use this information for their benefits. -Plus it serves a verifiable documentation purpose: - -.. code-block:: python - - # file lib.pyi - from typing import final - - @final - class Leaf: - ... - - # file main.py - from lib import Leaf - - class MyLeaf(Leaf): # Error, library author believes this is unsafe - ... - -.. note:: - - Mypy treats re-exported final names as final. In other words, once declared, - the final status can't be "stripped". Such behaviour is typically desired - for larger libraries where constants are defined in a separate module and - then re-exported. diff --git a/docs/source/type_quals.rst b/docs/source/type_quals.rst new file mode 100644 index 000000000000..222c91e30cdb --- /dev/null +++ b/docs/source/type_quals.rst @@ -0,0 +1,245 @@ +Class and instance variables +**************************** + +By default mypy assumes that a variable declared in the class body is +an instance variable. One can mark names intended to be used as class variables +with a special type qualifier ``ClassVar``. For example: + +.. code-block:: python + + from typing import ClassVar + + class Base: + attr: int # This is an instance variable + num_subclasses: ClassVar[int] # This is a class variable + + def foo(self) -> None: + self.attr = 0 # OK + self.num_subclasses = 0 # Error: Cannot assign to class variable via instance + + Base.num_subclasses = 0 # OK + Base.attr = 0 # Also OK, sets default value for an instance variable + +Note that ``ClassVar`` is not valid as a nested type, and in any position +other than assignment in class body. For example: + +.. code-block:: python + + x: ClassVar[int] # Error: ClassVar can't be used at module scope + + class C: + y: List[ClassVar[int]] # Error: can't use ClassVar as nested type + + +Final attributes of classes and modules +*************************************** + +.. note:: + + This is an experimental feature. Some details might change in later + versions of mypy. The final qualifiers are available in ``typing_extensions`` + module. When the semantics is stable, they will be added to ``typing``. + +There are several situations where static guarantees about non-redefinition +of certain names (or references in general) can be useful. One such example +is module or class level constants, user might want to guard them against +unintentional modifications: + +.. code-block:: python + + RATE = 3000 + + class Base: + DEFAULT_ID = 0 + + # 1000 lines later + + class Derived(Base): + DEFAULT_ID = 1 # this may be unintentional + + RATE = 300 # this too + +Another example is where a user might want to protect certain instance +attributes from overriding in a subclass: + +.. code-block:: python + + import uuid + + class Snowflake: + """An absolutely unique object in the database""" + def __init__(self) -> None: + self.id = uuid.uuid4() + + # 1000 lines later + + class User(Snowflake): + id = uuid.uuid4() # This has valid type, but the meaning + # may be wrong + +Some other use cases might be solved by using ``@property``, but note that both +above use cases can't be solved this way. For such situations, one might want +to use ``typing.Final``. + +Definition syntax +----------------- + +The ``typing.Final`` type qualifier indicates that a given name or attribute +should never be re-assigned, re-defined, nor overridden. It can be used in +one of these forms: + +* The simplest one is ``ID: Final = 1``. Note that unlike gor generic classes + this is *not* the same as ``Final[Any]``. Here mypy will infer type ``int``. + +* An explicit type ``ID: Final[float] = 1`` can be used as in any + normal assignment. + +* In stub files one can omit the right hand side and just write + ``ID: Final[float]``. + +* Finally, one can define ``self.id: Final = 1`` (also with a type argument), + but this is allowed *only* in ``__init__`` methods. + +Definition rules +---------------- + +The are two rules that should be always followed when defining a final name: + +* There can be *at most one* final declaration per module or class for + a given attribute: + + .. code-block:: python + + ID: Final = 1 + ID: Final = 2 # Error! + + class SomeCls: + id: Final = 1 + def __init__(self, x: int) -> None: + self.id: Final = x # Error! + + Note that mypy has a single namespace for a class. So there can't be two + class-level and instance-level constants with the same name. + +* There must be *exactly one* assignment to a final attribute: + + .. code-block:: python + + ID = 1 + ID: Final = 2 # Error! + + class SomeCls: + ID = 1 + ID: Final = 2 # Error! + +.. note:: + Conditional final declarations and final declarations within loops are + not supported. + +Using final attributes +---------------------- + +As a result of a final declaration mypy strives to provide the +two following guarantees: + +* A final attribute can't be re-assigned (or otherwise re-defined), both + internally and externally: + + .. code-block:: python + + # file mod.py + from typing import Final + + ID: Final = 1 + + # file main.py + from typing import Final + + import mod + mod.ID = 2 # Error, can't assign to constant. + + class SomeCls: + ID: Final = 1 + + def meth(self) -> None: + self.ID = 2 # Error, can't assign to final attribute + + class DerivedCls(SomeCls): + ... + + DerivedCls.ID = 2 # Error! + obj: DerivedCls + obj.ID = 2 # Error! + +* A final attribute can't be overridden by a subclass (even with another + explicit final declaration). Note however, that final attributes can + override normal attributes. This also applies to multiple inheritance: + + .. code-block:: python + + class Base: + ID = 0 + + class One(Base): + ID: Final = 1 # OK + + class Other(Base): + ID: Final = 2 # OK + + class Combo(One, Other): # Error, cannot override final attribute. + pass + +.. note:: + + Mypy treats re-exported final names as final. In other words, once declared, + the final status can't be "stripped". Such behaviour is typically desired + for larger libraries where constants are defined in a separate module and + then re-exported. + +Final methods +------------- + +Like with attributes, sometimes it is useful to protect a method from +overriding. In such situations one can use a ``typing.final`` decorator: + +.. code-block:: python + + from typing import final + + class Base: + @final + def common_name(self) -> None: # common signature + ... + + # 1000 lines later + + class Derived(Base): + def common_name(self) -> None: # Error, this overriding might break + # invariants in the base class. + ... + +This ``@final`` decorator can be used with instance methods, class methods, +static methods, and properties (this includes overloaded methods). + +Final classes +------------- + +As a bonus, applying a ``typing.final`` decorator to a class indicates to mypy +that it can't be subclassed. Mypy doesn't provide any additional features for +final classes, but some other tools may use this information for their benefits. +Plus it serves a verifiable documentation purpose: + +.. code-block:: python + + # file lib.pyi + from typing import final + + @final + class Leaf: + ... + + # file main.py + from lib import Leaf + + class MyLeaf(Leaf): # Error, library author believes this is unsafe + ... From b5dca3bcd79f37c06b142aefdf4874a4390534e2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 31 Aug 2018 13:28:12 +0100 Subject: [PATCH 30/52] Add section header --- docs/source/type_quals.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/type_quals.rst b/docs/source/type_quals.rst index 222c91e30cdb..4f61e28d1fa0 100644 --- a/docs/source/type_quals.rst +++ b/docs/source/type_quals.rst @@ -1,3 +1,9 @@ +Type qualifiers +=============== + +This section describes constructs that do not affect types of variables +and methods, but affect how they can be accessed, assigned, and overridden. + Class and instance variables **************************** From 1912fd85231a5252263813b4cc6dcb7a4e6efa75 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 31 Aug 2018 17:23:13 +0100 Subject: [PATCH 31/52] Fix the rest of comments about docs --- docs/source/type_quals.rst | 168 ++++++++++++++++++++++++++++--------- 1 file changed, 128 insertions(+), 40 deletions(-) diff --git a/docs/source/type_quals.rst b/docs/source/type_quals.rst index 4f61e28d1fa0..e765953e9a76 100644 --- a/docs/source/type_quals.rst +++ b/docs/source/type_quals.rst @@ -9,7 +9,7 @@ Class and instance variables By default mypy assumes that a variable declared in the class body is an instance variable. One can mark names intended to be used as class variables -with a special type qualifier ``ClassVar``. For example: +with a special type qualifier ``typing.ClassVar``. For example: .. code-block:: python @@ -36,6 +36,24 @@ other than assignment in class body. For example: class C: y: List[ClassVar[int]] # Error: can't use ClassVar as nested type +Instance variable can not override a class variable in a subclass +and vice-versa: + +.. code-block:: python + + class Base: + x: int + y: ClassVar[int] + + class Derived(Base): + x: ClassVar[int] # Error! + y: int # Error! + +.. note:: + + Assigning a value to a variable in the class body doesn't make it a class + variable, it just sets a default value for an instance variable, *only* + names explicitly declared with ``ClassVar`` are class variables. Final attributes of classes and modules *************************************** @@ -46,53 +64,52 @@ Final attributes of classes and modules versions of mypy. The final qualifiers are available in ``typing_extensions`` module. When the semantics is stable, they will be added to ``typing``. -There are several situations where static guarantees about non-redefinition -of certain names (or references in general) can be useful. One such example -is module or class level constants, user might want to guard them against -unintentional modifications: +You can declare a variable or attribute as final, which means that the variable +must not be assigned a new value after initialization. This is often useful for +module and class level constants as a way to prevent unintended modification. +Mypy will prevent further assignments to final names in type-checked code: .. code-block:: python - RATE = 3000 + from typing_extensions import Final + RATE: Final = 3000 class Base: - DEFAULT_ID = 0 + DEFAULT_ID: Final = 0 # 1000 lines later - class Derived(Base): - DEFAULT_ID = 1 # this may be unintentional - - RATE = 300 # this too + RATE = 300 # Error: can't assign to final attribute + Base.DEFAULT_ID = 1 # Error: can't override a final attribute -Another example is where a user might want to protect certain instance -attributes from overriding in a subclass: +Another use case for final attributes is where a user wants to protect certain +instance attributes from overriding in a subclass: .. code-block:: python import uuid + from typing_extensions import Final class Snowflake: """An absolutely unique object in the database""" def __init__(self) -> None: - self.id = uuid.uuid4() + self.id: Final = uuid.uuid4() # 1000 lines later class User(Snowflake): - id = uuid.uuid4() # This has valid type, but the meaning - # may be wrong + id = uuid.uuid4() # Error: can't override a final attribute Some other use cases might be solved by using ``@property``, but note that both above use cases can't be solved this way. For such situations, one might want -to use ``typing.Final``. +to use ``typing_extensions.Final``. Definition syntax ----------------- -The ``typing.Final`` type qualifier indicates that a given name or attribute -should never be re-assigned, re-defined, nor overridden. It can be used in -one of these forms: +The ``typing_extensions.Final`` type qualifier indicates that a given name or +attribute should never be re-assigned, re-defined, nor overridden. It can be +used in one of these forms: * The simplest one is ``ID: Final = 1``. Note that unlike gor generic classes this is *not* the same as ``Final[Any]``. Here mypy will infer type ``int``. @@ -116,13 +133,15 @@ The are two rules that should be always followed when defining a final name: .. code-block:: python + from typing_extensions import Final + ID: Final = 1 - ID: Final = 2 # Error! + ID: Final = 2 # Error: "ID" already declared as final class SomeCls: id: Final = 1 def __init__(self, x: int) -> None: - self.id: Final = x # Error! + self.id: Final = x # Error: "id" already declared in class body Note that mypy has a single namespace for a class. So there can't be two class-level and instance-level constants with the same name. @@ -138,6 +157,32 @@ The are two rules that should be always followed when defining a final name: ID = 1 ID: Final = 2 # Error! +* A final attribute declared in class body without r.h.s. must be initialized + in the ``__init__`` method (one can skip initializer in stub files): + + .. code-block:: python + + class SomeCls: + x: Final + y: Final # Error: final attribute without an initializer + def __init__(self) -> None: + self.x = 1 # Good + +* ``Final`` can be only used as an outermost type in assignments, using it in + any other position is an error. In particular, ``Final`` can't be used in + annotations for function arguments because this may cause confusions about + what are the guarantees in this case: + + .. code-block:: python + + x: List[Final[int]] = [] # Error! + def fun(x: Final[List[int]]) -> None: # Error! + ... + +* ``Final`` and ``ClassVar`` should not be used together, mypy will infer + the scope of a final declaration automatically depending on whether it was + initialized in class body or in ``__init__``. + .. note:: Conditional final declarations and final declarations within loops are not supported. @@ -154,23 +199,24 @@ two following guarantees: .. code-block:: python # file mod.py - from typing import Final + from typing_extensions import Final ID: Final = 1 - # file main.py - from typing import Final - - import mod - mod.ID = 2 # Error, can't assign to constant. - class SomeCls: ID: Final = 1 def meth(self) -> None: - self.ID = 2 # Error, can't assign to final attribute + self.ID = 2 # Error: can't assign to final attribute + + # file main.py + import mod + mod.ID = 2 # Error: can't assign to constant. + + from mod import ID + ID = 2 # Also an error, see note below. - class DerivedCls(SomeCls): + class DerivedCls(mod.SomeCls): ... DerivedCls.ID = 2 # Error! @@ -192,9 +238,22 @@ two following guarantees: class Other(Base): ID: Final = 2 # OK - class Combo(One, Other): # Error, cannot override final attribute. + class Combo(One, Other): # Error: cannot override final attribute. pass +* Declaring a name as final only guarantees that the name wll not be re-bound + to other value, it doesn't make the value immutable. One can use immutable ABCs + and containers to prevent mutating such values: + + .. code-block:: python + + x: Final = ['a', 'b'] + x.append('c') # OK + + y: Final[Sequance[str]] = ['a', 'b'] + y.append('x') # Error: Sequance is immutable + z: Final = ('a', 'b') # Also an option + .. note:: Mypy treats re-exported final names as final. In other words, once declared, @@ -206,31 +265,50 @@ Final methods ------------- Like with attributes, sometimes it is useful to protect a method from -overriding. In such situations one can use a ``typing.final`` decorator: +overriding. In such situations one can use a ``typing_extensions.final`` +decorator: .. code-block:: python - from typing import final + from typing_extensions import final class Base: @final - def common_name(self) -> None: # common signature + def common_name(self) -> None: ... # 1000 lines later class Derived(Base): - def common_name(self) -> None: # Error, this overriding might break + def common_name(self) -> None: # Error: this overriding might break # invariants in the base class. ... This ``@final`` decorator can be used with instance methods, class methods, -static methods, and properties (this includes overloaded methods). +static methods, and properties (this includes overloaded methods). For overloaded +methods it is enough to add ``@final`` on at leats one of overloads (or on +the implementation) to make it final: + +.. code-block:: python + from typing import Any, overload + + class Base: + @overload + def meth(self) -> None: ... + @overload + def meth(self, arg: int) -> int: ... + @final + def meth(self, x=None): + ... + + class Derived(Base): + def meth(self, x: Any = None) -> Any: # Error: can't override final method + ... Final classes ------------- -As a bonus, applying a ``typing.final`` decorator to a class indicates to mypy +As a bonus, applying a ``typing_extensions.final`` decorator to a class indicates to mypy that it can't be subclassed. Mypy doesn't provide any additional features for final classes, but some other tools may use this information for their benefits. Plus it serves a verifiable documentation purpose: @@ -238,7 +316,7 @@ Plus it serves a verifiable documentation purpose: .. code-block:: python # file lib.pyi - from typing import final + from typing_extensions import final @final class Leaf: @@ -247,5 +325,15 @@ Plus it serves a verifiable documentation purpose: # file main.py from lib import Leaf - class MyLeaf(Leaf): # Error, library author believes this is unsafe + class MyLeaf(Leaf): # Error: library author believes this is unsafe ... + +Some situations where this may be useful include: + +* A class wasn't designed to be subclassed. Perhaps subclassing does not + work as expected, or it's error-prone. +* You want to retain the freedom to arbitrarily change the class implementation + in the future, and these changes might break subclasses. +* You believe that subclassing would make code harder to understand or maintain. + For example, you may want to prevent unnecessarily tight coupling between + base classes and subclasses. From 235b28f7b65b502b1c654d6e2e9cf8c801c50573 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 31 Aug 2018 21:31:10 +0100 Subject: [PATCH 32/52] Prohibit overriding writeable with final --- docs/source/type_quals.rst | 5 +++-- mypy/checker.py | 21 +++++++++++++++++++-- mypy/messages.py | 3 +++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/source/type_quals.rst b/docs/source/type_quals.rst index e765953e9a76..6a349761d866 100644 --- a/docs/source/type_quals.rst +++ b/docs/source/type_quals.rst @@ -225,12 +225,13 @@ two following guarantees: * A final attribute can't be overridden by a subclass (even with another explicit final declaration). Note however, that final attributes can - override normal attributes. This also applies to multiple inheritance: + override read-only properties. This also applies to multiple inheritance: .. code-block:: python class Base: - ID = 0 + @property + def ID(self) -> int: ... class One(Base): ID: Final = 1 # OK diff --git a/mypy/checker.py b/mypy/checker.py index 905892e2f1b9..f4bbbea5d96c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1290,6 +1290,13 @@ def check_method_override_for_base_with_name( # First, check if we don't override a final (always an error, even with Any types). if isinstance(base_attr.node, (Var, FuncBase, Decorator)) and base_attr.node.is_final: self.msg.cant_override_final(name, base.name(), defn) + # Second, final can't override anything writeable independently of types. + if defn.is_final: + if isinstance(base_attr.node, (Var, Decorator)): + svar = (base_attr.node if isinstance(base_attr.node, Var) + else base_attr.node.var) + if not (svar.is_final or svar.is_property and not svar.is_settable_property): + self.msg.final_cant_override_writeable(name, defn) # Construct the type of the overriding method. if isinstance(defn, FuncBase): @@ -1578,10 +1585,15 @@ def check_compatibility(self, name: str, base1: TypeInfo, if second_type is None: self.msg.cannot_determine_type_in_base(name, base2.name(), ctx) ok = True - # Final attributes can never be overridden, but can override normal attributes, - # so we only check `second`. + # Final attributes can never be overridden, but can override + # non-final read-only attributes. if isinstance(second.node, (Var, FuncBase, Decorator)) and second.node.is_final: self.msg.cant_override_final(name, base2.name(), ctx) + if isinstance(first.node, (Var, FuncBase, Decorator)) and first.node.is_final: + if isinstance(second.node, (Var, Decorator)): + svar = second.node if isinstance(second.node, Var) else second.node.var + if not (svar.is_final or svar.is_property and not svar.is_settable_property): + self.msg.final_cant_override_writeable(name, ctx) # __slots__ is special and the type can vary across class hierarchy. if name == '__slots__': ok = True @@ -1910,6 +1922,11 @@ def check_compatibility_final_super(self, node: Var, if base_node.is_final: self.msg.cant_override_final(node.name(), base.name(), node) return False + if node.is_final: + if isinstance(base_node, (Var, Decorator)): + svar = base_node if isinstance(base_node, Var) else base_node.var + if not (svar.is_final or svar.is_property and not svar.is_settable_property): + self.msg.final_cant_override_writeable(node.name(), node) return True def get_final_context(self) -> bool: diff --git a/mypy/messages.py b/mypy/messages.py index f16e8fefae25..72844e8d0c55 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -951,6 +951,9 @@ def cant_assign_to_method(self, context: Context) -> None: def cant_assign_to_classvar(self, name: str, context: Context) -> None: self.fail('Cannot assign to class variable "%s" via instance' % name, context) + def final_cant_override_writeable(self, name: str, ctx: Context) -> None: + self.fail('Can\'t override writeable attribute "{}" with a final one'.format(name), ctx) + def cant_override_final(self, name: str, base_name: str, ctx: Context) -> None: self.fail('Cannot override final attribute "{}"'.format(name), ctx) self.note('(previously declared on base class "{}")'.format(base_name), ctx) From 86e883accfde8521b000c825378e58eaa7b9755b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 1 Sep 2018 01:30:51 +0100 Subject: [PATCH 33/52] Fix some tests --- mypy/checker.py | 36 ++--- mypy/semanal.py | 9 +- test-data/unit/check-final.test | 125 ++++++++++++++---- test-data/unit/fixtures/property.pyi | 1 + test-data/unit/lib-stub/typing_extensions.pyi | 3 + 5 files changed, 128 insertions(+), 46 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f4bbbea5d96c..89688c1fa042 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -21,7 +21,7 @@ ComparisonExpr, StarExpr, EllipsisExpr, RefExpr, PromoteExpr, Import, ImportFrom, ImportAll, ImportBase, TypeAlias, ARG_POS, ARG_STAR, LITERAL_TYPE, MDEF, GDEF, - CONTRAVARIANT, COVARIANT, INVARIANT, + CONTRAVARIANT, COVARIANT, INVARIANT ) from mypy import nodes from mypy.literals import literal, literal_hash @@ -1292,11 +1292,7 @@ def check_method_override_for_base_with_name( self.msg.cant_override_final(name, base.name(), defn) # Second, final can't override anything writeable independently of types. if defn.is_final: - if isinstance(base_attr.node, (Var, Decorator)): - svar = (base_attr.node if isinstance(base_attr.node, Var) - else base_attr.node.var) - if not (svar.is_final or svar.is_property and not svar.is_settable_property): - self.msg.final_cant_override_writeable(name, defn) + self.check_no_writeable(name, base_attr.node, defn) # Construct the type of the overriding method. if isinstance(defn, FuncBase): @@ -1590,10 +1586,7 @@ def check_compatibility(self, name: str, base1: TypeInfo, if isinstance(second.node, (Var, FuncBase, Decorator)) and second.node.is_final: self.msg.cant_override_final(name, base2.name(), ctx) if isinstance(first.node, (Var, FuncBase, Decorator)) and first.node.is_final: - if isinstance(second.node, (Var, Decorator)): - svar = second.node if isinstance(second.node, Var) else second.node.var - if not (svar.is_final or svar.is_property and not svar.is_settable_property): - self.msg.final_cant_override_writeable(name, ctx) + self.check_no_writeable(name, second.node, ctx) # __slots__ is special and the type can vary across class hierarchy. if name == '__slots__': ok = True @@ -1894,6 +1887,11 @@ def lvalue_type_from_base(self, expr_node: Var, # value, not the Callable if base_node.is_property: base_type = base_type.ret_type + if isinstance(base_type, FunctionLike) and isinstance(base_node, + OverloadedFuncDef): + # Same for properties with setter + if base_node.is_property: + base_type = base_type.items()[0].ret_type return base_type, base_node @@ -1923,12 +1921,20 @@ def check_compatibility_final_super(self, node: Var, self.msg.cant_override_final(node.name(), base.name(), node) return False if node.is_final: - if isinstance(base_node, (Var, Decorator)): - svar = base_node if isinstance(base_node, Var) else base_node.var - if not (svar.is_final or svar.is_property and not svar.is_settable_property): - self.msg.final_cant_override_writeable(node.name(), node) + self.check_no_writeable(node.name(), base_node, node) return True + def check_no_writeable(self, name: str, base_node: Optional[Node], ctx: Context) -> None: + if isinstance(base_node, Var): + ok = False + elif isinstance(base_node, OverloadedFuncDef) and base_node.is_property: + first_item = cast(Decorator, base_node.items[0]) + ok = not first_item.var.is_settable_property + else: + ok = True + if not ok: + self.msg.final_cant_override_writeable(name, ctx) + def get_final_context(self) -> bool: return self._is_final_def @@ -1944,7 +1950,7 @@ def set_final_context(self, is_final_def: bool) -> Iterator[None]: def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: """Check if this assignment does not assign to a final attribute. - This function perfoms the check only for name assignments at module + This function performs the check only for name assignments at module and class scope. The assignments to `obj.attr` and `Cls.attr` are checked in checkmember.py. """ diff --git a/mypy/semanal.py b/mypy/semanal.py index f2dfed79732e..8f2a83c7939a 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -864,7 +864,8 @@ def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None defn.info.runtime_protocol = True else: self.fail('@runtime can only be used with protocol classes', defn) - elif decorator.fullname == 'typing.final': + elif decorator.fullname in ('typing.final', + 'typing_extensions.final'): defn.info.is_final = True def calculate_abstract_status(self, typ: TypeInfo) -> None: @@ -2313,7 +2314,8 @@ def is_final_type(self, typ: Type) -> bool: sym = self.lookup_qualified(typ.name, typ) if not sym or not sym.node: return False - return sym.node.fullname() == 'typing.Final' + return sym.node.fullname() in ('typing.Final', + 'typing_extensions.Final') def fail_invalid_classvar(self, context: Context) -> None: self.fail('ClassVar can only be used for assignments in class body', context) @@ -2417,7 +2419,8 @@ def visit_decorator(self, dec: Decorator) -> None: elif refers_to_fullname(d, 'typing.no_type_check'): dec.var.type = AnyType(TypeOfAny.special_form) no_type_check = True - elif refers_to_fullname(d, 'typing.final'): + elif (refers_to_fullname(d, 'typing.final') or + refers_to_fullname(d, 'typing_extensions.final')): if self.is_class_scope(): assert self.type is not None, "No type set at class scope" if self.type.is_protocol: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 5d1a5729d72b..53d0b22f9fd9 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -451,10 +451,15 @@ D().b += B() # E: Can't assign to final attribute "b" [case testFinalOverridingVarClassBody] from typing import Final +# We use properties in this tests and below because we want to check +# that any existing variable before final doesn't affect logic of +# subsequent overrides but writable attributes can't be overridden by final. class A: - x = 0 - def __init__(self) -> None: - self.y = 0 + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... + class B(A): x: Final = 1 def __init__(self) -> None: @@ -472,15 +477,17 @@ class D(C): pass D.x = 4 # E: Can't assign to final attribute "x" D.y = 4 # E: Can't assign to final attribute "y" +[builtins fixtures/property.pyi] [out] [case testFinalOverridingVarClassBodyExplicit] from typing import Final class A: - x = 0 - def __init__(self) -> None: - self.y = 0 + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... class B(A): x: Final = 1 def __init__(self) -> None: @@ -490,15 +497,17 @@ class C(B): # N: (previously declared on base class "B") y: Final = 2 # E: Cannot override final attribute "y" \ # N: (previously declared on base class "B") +[builtins fixtures/property.pyi] [out] [case testFinalOverridingVarInit] from typing import Final class A: - x = 0 - def __init__(self) -> None: - self.y = 0 + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... class B(A): x: Final = 1 def __init__(self) -> None: @@ -510,15 +519,17 @@ class C(B): def meth(self) -> None: self.x = 3 # E: Can't assign to final attribute "x" self.y = 3 # E: Can't assign to final attribute "y" +[builtins fixtures/property.pyi] [out] [case testFinalOverridingVarInit2] from typing import Final class A: - x = 0 - def __init__(self) -> None: - self.y = 0 + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... class B(A): x: Final = 1 def __init__(self) -> None: @@ -529,15 +540,17 @@ class C(B): # N: (previously declared on base class "B") self.y: Final = 2 # E: Cannot override final attribute "y" \ # N: (previously declared on base class "B") +[builtins fixtures/property.pyi] [out] [case testFinalOverridingVarOtherMethod] from typing import Final class A: - x = 0 - def __init__(self) -> None: - self.y = 0 + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... class B(A): x: Final = 1 def __init__(self) -> None: @@ -552,41 +565,43 @@ class C(B): # N: (previously declared on base class "B") self.x = 3 # E: Can't assign to final attribute "x" self.y = 3 # E: Can't assign to final attribute "y" +[builtins fixtures/property.pyi] [out] [case testFinalOverridingVarMultipleInheritanceClass] -from typing import Final +from typing import Final, Any class A: - x: Final = 1 + x: Final[Any] = 1 class B: - x = 2 + @property + def x(self) -> int: ... class C(A, B): ... class D(B, A): ... # E: Cannot override final attribute "x" \ # N: (previously declared on base class "A") C.x = 3 # E: Can't assign to final attribute "x" -D.x = 3 # E: Can't assign to final attribute "x" C().x = 4 # E: Can't assign to final attribute "x" -D().x = 4 # E: Can't assign to final attribute "x" +D().x = 4 # E: Can't assign to final attribute "x" \ + # E: Property "x" defined in "B" is read-only +[builtins fixtures/property.pyi] [out] [case testFinalOverridingVarMultipleInheritanceInit] -from typing import Final +from typing import Final, Any class A: def __init__(self) -> None: - self.x: Final = 1 + self.x: Final[Any] = 1 class B: - def __init__(self) -> None: - self.x = 2 + @property + def x(self) -> int: ... class C(A, B): ... class D(B, A): ... # E: Cannot override final attribute "x" \ # N: (previously declared on base class "A") C.x = 3 # E: Can't access instance constant on class object \ # E: Can't assign to final attribute "x" -D.x = 3 # E: Can't assign to final attribute "x" C().x = 4 # E: Can't assign to final attribute "x" -D().x = 4 # E: Can't assign to final attribute "x" +[builtins fixtures/property.pyi] [out] [case testFinalOverridingVarMultipleInheritanceMixed] @@ -597,7 +612,7 @@ class A: class B: def __init__(self) -> None: self.x = 2 -class C(A, B): ... +class C(A, B): ... # E: Can't override writeable attribute "x" with a final one class D(B, A): ... # E: Cannot override final attribute "x" \ # N: (previously declared on base class "A") C.x = 3 # E: Can't assign to final attribute "x" @@ -716,6 +731,16 @@ class C(A): [builtins fixtures/classmethod.pyi] [out] +[case testFinalCanOverrideMethodWithFinal] +from typing import final + +class B: + def meth(self) -> None: ... +class C(B): + @final # OK + def meth(self) -> None: ... +[out] + [case testFinalOverridingMethodMultipleInheritance] from typing import final @@ -741,7 +766,7 @@ class B: class C(A, B): pass # E: Cannot override final attribute "m" \ # N: (previously declared on base class "B") -class D(B, A): pass +class D(B, A): pass # E: Can't override writeable attribute "m" with a final one [out] [case testFinalOverridingClassMethod] @@ -833,3 +858,47 @@ class C(B, A): # E: Can't inherit from final class "B" class D(A, B): # E: Can't inherit from final class "B" pass [out] + +[case testFinalCantOverrideWriteable] +from typing import Any, Final, final + +class B: + x: Any + @property + def y(self) -> Any: ... + @y.setter + def y(self, x: Any) -> None: ... + +class C(B): + x: Final = 1 # E: Can't override writeable attribute "x" with a final one + y: Final = 1 # E: Can't override writeable attribute "y" with a final one + +class D(B): + @final # E: Can't override writeable attribute "x" with a final one + def x(self) -> int: ... + @final # E: Can't override writeable attribute "y" with a final one + def y(self) -> int: ... +[builtins fixtures/property.pyi] +[out] + +[case testFinalCanUseTypingExtensions] +from typing_extensions import final, Final + +x: Final = 1 +x = 2 # E: Can't assign to constant "x" + +class S: + x: Final = 1 +S.x = 2 # E: Can't assign to final attribute "x" + +class B: + @final + def meth(self) -> None: ... +class C(B): + def meth(self) -> None: ... # E: Cannot override final attribute "meth" \ + # N: (previously declared on base class "B") + +@final +class F: ... +class E(F): ... # E: Can't inherit from final class "F" +[out] diff --git a/test-data/unit/fixtures/property.pyi b/test-data/unit/fixtures/property.pyi index 929317e2ef66..5a98f12bdda7 100644 --- a/test-data/unit/fixtures/property.pyi +++ b/test-data/unit/fixtures/property.pyi @@ -16,5 +16,6 @@ class int: pass class str: pass class bytes: pass class bool: pass +class ellipsis: pass class tuple(typing.Generic[_T]): pass diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 8c5be8f3637f..644a5a997562 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -4,3 +4,6 @@ _T = TypeVar('_T') class Protocol: pass def runtime(x: _T) -> _T: pass + +class Final: pass +def final(x: _T) -> _T: pass From aa8b43643efcc8cf613aa9057983258f78f53c61 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 1 Sep 2018 02:52:11 +0100 Subject: [PATCH 34/52] Allow deferred final definition in __init__ --- mypy/checker.py | 6 ++++++ mypy/nodes.py | 7 ++++++- mypy/semanal.py | 27 ++++++++++++++++++++++--- test-data/unit/check-final.test | 36 +++++++++++++++++++++++++++------ 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 89688c1fa042..f4e305349739 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1959,6 +1959,12 @@ def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: else: lvs = [s.lvalue] is_final_decl = s.is_final_def if isinstance(s, AssignmentStmt) else False + if is_final_decl and self.scope.active_class(): + lv = lvs[0] + assert isinstance(lv.node, Var) + if (lv.node.final_unset_in_class and not + lv.node.final_set_in_init and not self.is_stub): + self.fail('Final names outside stubs must have a value', s) for lv in lvs: if isinstance(lv, RefExpr) and isinstance(lv.node, Var): name = lv.node.name() diff --git a/mypy/nodes.py b/mypy/nodes.py index 4a0bf57aac2c..550dc5707540 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -706,7 +706,7 @@ def deserialize(cls, data: JsonDict) -> 'Decorator': VAR_FLAGS = [ 'is_self', 'is_initialized_in_class', 'is_staticmethod', 'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import', - 'is_classvar', 'is_abstract_var', 'is_final' + 'is_classvar', 'is_abstract_var', 'is_final', 'final_unset_in_class', 'final_set_in_init' ] @@ -732,6 +732,8 @@ class Var(SymbolNode): 'is_classvar', 'is_abstract_var', 'is_final', + 'final_unset_in_class', + 'final_set_in_init', 'is_suppressed_import', ) @@ -764,6 +766,9 @@ def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None: # store the literal value (unboxed) for the benefit of # tools like mypyc. self.final_value = None # type: Optional[Union[int, float, bool, str]] + # Where the value was set (only for class attributes) + self.final_unset_in_class = False + self.final_set_in_init = False def name(self) -> str: return self._name diff --git a/mypy/semanal.py b/mypy/semanal.py index 8f2a83c7939a..07f46531d7b4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1729,8 +1729,9 @@ def unwrap_final(self, s: AssignmentStmt) -> None: self.fail("Final declarations are prohibited within loops", s) if self.type and self.type.is_protocol: self.msg.protocol_members_cant_be_final(s) - if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file: - self.fail("Final declaration outside stubs must have right hand side", s) + if (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and + not self.is_stub_file and not self.is_class_scope()): + self.fail("Final names outside stubs must have a value", s) return def check_final_implicit_def(self, s: AssignmentStmt) -> None: @@ -1763,6 +1764,26 @@ def store_final_status(self, s: AssignmentStmt) -> None: if isinstance(node, Var): node.is_final = True node.final_value = self.unbox_literal(s.rvalue) + if (self.is_class_scope() and + (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs)): + node.final_unset_in_class = True + elif len(s.lvalues) == 1 and isinstance(s.lvalues[0], MemberExpr): + # Special case: deferred initialization of a final attribute in __init__. + # In this case we just pretend this is a valid final definition to suppress + # errors about assigning to final attribute. + lval = s.lvalues[0] + if self.is_self_member_ref(lval): + assert self.type, "Self member outside a class" + cur_node = self.type.names.get(lval.name, None) + if cur_node and isinstance(cur_node.node, Var) and cur_node.node.is_final: + assert self.function_stack + top_function = self.function_stack[-1] + if (top_function.name() == '__init__' and + cur_node.node.final_unset_in_class and + not cur_node.node.final_set_in_init and + not (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs)): + cur_node.node.final_set_in_init = True + s.is_final_def = True def unbox_literal(self, e: Expression) -> Optional[Union[int, float, bool, str]]: if isinstance(e, (IntExpr, FloatExpr, StrExpr)): @@ -2023,7 +2044,7 @@ def analyze_tuple_or_list_lvalue(self, lval: TupleExpr, star_exprs[0].valid = True for i in items: self.analyze_lvalue(i, nested=True, add_global=add_global, - explicit_type = explicit_type) + explicit_type=explicit_type) def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False, final_cb: Optional[Callable[[], None]] = None) -> None: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 53d0b22f9fd9..d43aab39248a 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -48,7 +48,7 @@ reveal_type(C((1, 2)).y) # E: Revealed type is 'builtins.float' [case testFinalBadDefinitionTooManyArgs] from typing import Final -x: Final[int, str] # E: Final declaration outside stubs must have right hand side \ +x: Final[int, str] # E: Final names outside stubs must have a value \ # E: Final[...] takes at most one type argument reveal_type(x) # E: Revealed type is 'builtins.int' @@ -166,13 +166,13 @@ def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost ty [case testFinalDefiningNoRhs] from typing import Final -x: Final # E: Final declaration outside stubs must have right hand side -y: Final[int] # E: Final declaration outside stubs must have right hand side +x: Final # E: Final names outside stubs must have a value +y: Final[int] # E: Final names outside stubs must have a value class C: - x: Final # E: Final declaration outside stubs must have right hand side - y: Final[int] # E: Final declaration outside stubs must have right hand side + x: Final # E: Final names outside stubs must have a value + y: Final[int] # E: Final names outside stubs must have a value def __init__(self) -> None: - self.z: Final # E: Final declaration outside stubs must have right hand side + self.z: Final # E: Final names outside stubs must have a value reveal_type(x) # E: Revealed type is 'Any' reveal_type(y) # E: Revealed type is 'builtins.int' reveal_type(C().x) # E: Revealed type is 'Any' @@ -257,6 +257,30 @@ while True: [builtins fixtures/list.pyi] [out] +[case testFinalDelayedDefinition] +from typing import Final + +class C: + x: Final[int] # OK, defined in __init__ + bad: Final[int] # E: Final names outside stubs must have a value + + def __init__(self, x: int) -> None: + self.x = x # OK, deferred definition + self.x = 2 # E: Can't assign to final attribute "x" + + def meth(self) -> None: + self.x = 2 # E: Can't assign to final attribute "x" + +c: C +c.x = 3 # E: Can't assign to final attribute "x" +class D(C): + x = 4 # E: Cannot override final attribute "x" \ + # N: (previously declared on base class "C") \ + # E: Can't assign to constant "x" +d: D +d.x = 5 # E: Can't assign to final attribute "x" +[out] + -- Reassignments [case testFinalReassignModuleVar] From 95da7ce12a6df61d0cb96fb665f91f5ac18ffcc6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 1 Sep 2018 02:59:27 +0100 Subject: [PATCH 35/52] Fix self-check --- mypy/checker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/checker.py b/mypy/checker.py index f4e305349739..9160209a5bf4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1961,6 +1961,7 @@ def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: is_final_decl = s.is_final_def if isinstance(s, AssignmentStmt) else False if is_final_decl and self.scope.active_class(): lv = lvs[0] + assert isinstance(lv, RefExpr) assert isinstance(lv.node, Var) if (lv.node.final_unset_in_class and not lv.node.final_set_in_init and not self.is_stub): From b50f3dea37967accc98aa4efce33e42c54c88c36 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 1 Sep 2018 03:21:45 +0100 Subject: [PATCH 36/52] Update new Var final flags --- mypy/semanal.py | 2 +- mypy/server/aststrip.py | 8 ++++++++ mypy/treetransform.py | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 07f46531d7b4..17d89e487209 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2060,7 +2060,7 @@ def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False, if ((node is None or isinstance(node.node, Var) and node.node.is_abstract_var) or # ... also an explicit declaration on self also creates a new Var. # Note that `explicit_type` might has been erased for bare `Final`, - # so we alse check if `final_cb` is passed. + # so we also check if `final_cb` is passed. (cur_node is None and (explicit_type or final_cb is not None))): if self.type.is_protocol and node is None: self.fail("Protocol members cannot be defined via assignment to self", lval) diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py index 31193c1b4a03..6bf590842504 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -259,6 +259,7 @@ def visit_name_expr(self, node: NameExpr) -> None: # [*] although we always strip type, thus returning the Var to the state after pass 1. if isinstance(node.node, Var): node.node.type = None + self._reset_var_final_flags(node.node) def visit_member_expr(self, node: MemberExpr) -> None: self.strip_ref_expr(node) @@ -273,8 +274,15 @@ def visit_member_expr(self, node: MemberExpr) -> None: # definition. self.strip_class_attr(node.name) node.def_var = None + if isinstance(node.node, Var): + self._reset_var_final_flags(node.node) super().visit_member_expr(node) + def _reset_var_final_flags(self, v: Var) -> None: + v.is_final = False + v.final_unset_in_class = False + v.final_set_in_init = False + def visit_index_expr(self, node: IndexExpr) -> None: node.analyzed = None # was a type alias super().visit_index_expr(node) diff --git a/mypy/treetransform.py b/mypy/treetransform.py index 1d6124cc68b1..981ea9bdf8a4 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -208,6 +208,8 @@ def visit_var(self, node: Var) -> Var: new.is_property = node.is_property new.is_final = node.is_final new.final_value = node.final_value + new.final_unset_in_class = node.final_unset_in_class + new.final_set_in_init = node.final_set_in_init new.set_line(node.line) self.var_map[node] = new return new From 2f74d8777e92b129125f54da2c6dd9d0a7704701 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 1 Sep 2018 16:02:42 +0100 Subject: [PATCH 37/52] Simplify/shorten some error messages --- mypy/checker.py | 14 ++++++++++---- mypy/checkmember.py | 2 +- mypy/messages.py | 6 +++--- test-data/unit/check-final.test | 21 ++++++--------------- test-data/unit/check-incremental.test | 4 ---- test-data/unit/fine-grained.test | 4 ---- 6 files changed, 20 insertions(+), 31 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9160209a5bf4..85c1f071aae2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1917,7 +1917,11 @@ def check_compatibility_final_super(self, node: Var, base: TypeInfo, base_node: Optional[Node]) -> bool: if not isinstance(base_node, (Var, FuncBase, Decorator)): return True - if base_node.is_final: + if base_node.is_final and (node.is_final or not isinstance(base_node, Var)): + # Give this error only for explicit override attempt with `Final`, or + # if we are overriding a final method with variable. + # Other override attempts will be flagged as assignment to constant + # in `check_final()`. self.msg.cant_override_final(node.name(), base.name(), node) return False if node.is_final: @@ -1973,15 +1977,17 @@ def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: if cls is not None: # Theses additional checks exist to give more errors messages # even if the final attribute was overridden with a new symbol - # (which is itself an error). + # (which is itself an error)... for base in cls.mro[1:]: sym = base.names.get(name) if sym and isinstance(sym.node, (Var, Decorator)): var = sym.node if isinstance(sym.node, Var) else sym.node.var if var.is_final and not is_final_decl: - self.msg.cant_assign_to_final(name, var.info is not None, s) + self.msg.cant_assign_to_final(name, var.info is None, s) + # ...but only once + break if lv.node.is_final and not is_final_decl: - self.msg.cant_assign_to_final(name, lv.node.info is not None, s) + self.msg.cant_assign_to_final(name, lv.node.info is None, s) def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression, context: Context, diff --git a/mypy/checkmember.py b/mypy/checkmember.py index ca660ea33a64..102428295ea7 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -315,7 +315,7 @@ def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Cont for base in info.mro: sym = base.names.get(name) if sym and isinstance(sym.node, (Var, FuncBase, Decorator)) and sym.node.is_final: - msg.cant_assign_to_final(name, module_level=False, ctx=ctx) + msg.cant_assign_to_final(name, attr_assign=True, ctx=ctx) def analyze_descriptor_access(instance_type: Type, descriptor_type: Type, diff --git a/mypy/messages.py b/mypy/messages.py index 72844e8d0c55..896ecf2a05a5 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -958,12 +958,12 @@ def cant_override_final(self, name: str, base_name: str, ctx: Context) -> None: self.fail('Cannot override final attribute "{}"'.format(name), ctx) self.note('(previously declared on base class "{}")'.format(base_name), ctx) - def cant_assign_to_final(self, name: str, module_level: bool, ctx: Context) -> None: + def cant_assign_to_final(self, name: str, attr_assign: bool, ctx: Context) -> None: """Warn about a prohibited assignment to a final attribute. - Pass `module_level=True` if the assignment assigns to a module attribute. + Pass `attr_assign=True` if the assignment assigns to an attribute. """ - kind = "constant" if module_level else "final attribute" + kind = "final attribute" if attr_assign else "constant" self.fail('Can\'t assign to {} "{}"'.format(kind, name), ctx) def protocol_members_cant_be_final(self, ctx: Context) -> None: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index d43aab39248a..d754ad8cff9c 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -274,9 +274,7 @@ class C: c: C c.x = 3 # E: Can't assign to final attribute "x" class D(C): - x = 4 # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "C") \ - # E: Can't assign to constant "x" + x = 4 # E: Can't assign to constant "x" d: D d.x = 5 # E: Can't assign to final attribute "x" [out] @@ -489,12 +487,8 @@ class B(A): def __init__(self) -> None: self.y: Final = 1 class C(B): - x: int = 2 # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "B") \ - # E: Can't assign to constant "x" - y: int = 2 # E: Cannot override final attribute "y" \ - # N: (previously declared on base class "B") \ - # E: Can't assign to constant "y" + x: int = 2 # E: Can't assign to constant "x" + y: int = 2 # E: Can't assign to constant "y" x = 3 # E: Can't assign to constant "x" y = 3 # E: Can't assign to constant "y" class D(C): @@ -581,12 +575,9 @@ class B(A): self.y: Final = 1 class C(B): def meth(self) -> None: - self.x: int = 2 # E: Can't assign to final attribute "x" \ - # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "B") - self.y: int = 2 # E: Can't assign to final attribute "y" \ - # E: Cannot override final attribute "y" \ - # N: (previously declared on base class "B") + self.x: int = 2 # E: Can't assign to final attribute "x" + self.y: int = 2 # E: Can't assign to final attribute "y" + self.x = 3 # E: Can't assign to final attribute "x" self.y = 3 # E: Can't assign to final attribute "y" [builtins fixtures/property.pyi] diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 5e5ddbf2b6e6..3aac52bbc894 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4953,12 +4953,8 @@ class C: self.y: Final = 1 [out] [out2] -main:4: error: Cannot override final attribute "x" -main:4: note: (previously declared on base class "C") main:4: error: Can't assign to constant "x" main:6: error: Can't assign to final attribute "y" -main:8: error: Cannot override final attribute "y" -main:8: note: (previously declared on base class "C") main:8: error: Can't assign to constant "y" main:10: error: Can't assign to final attribute "x" diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 5e5819ee7edf..82be9a2d109b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7601,12 +7601,8 @@ class C: self.y: Final = 1 [out] == -main:4: error: Cannot override final attribute "x" -main:4: note: (previously declared on base class "C") main:4: error: Can't assign to constant "x" main:6: error: Can't assign to final attribute "y" -main:8: error: Cannot override final attribute "y" -main:8: note: (previously declared on base class "C") main:8: error: Can't assign to constant "y" main:10: error: Can't assign to final attribute "x" From 03648689e7562e98a0e51935d1f52bd67dc0a89e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 1 Sep 2018 18:58:59 +0100 Subject: [PATCH 38/52] Further simplify error messages; use one-line errors instead of notes --- mypy/checker.py | 10 +-- mypy/messages.py | 4 +- test-data/unit/check-final.test | 94 +++++++++------------------ test-data/unit/check-incremental.test | 3 +- test-data/unit/fine-grained.test | 20 ++---- 5 files changed, 45 insertions(+), 86 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 85c1f071aae2..b88c496e5ea0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1980,10 +1980,12 @@ def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: # (which is itself an error)... for base in cls.mro[1:]: sym = base.names.get(name) - if sym and isinstance(sym.node, (Var, Decorator)): - var = sym.node if isinstance(sym.node, Var) else sym.node.var - if var.is_final and not is_final_decl: - self.msg.cant_assign_to_final(name, var.info is None, s) + # We only give this error if base node is variable, + # overriding final method will be caught in + # `check_compatibility_final_super()`. + if sym and isinstance(sym.node, Var): + if sym.node.is_final and not is_final_decl: + self.msg.cant_assign_to_final(name, sym.node.info is None, s) # ...but only once break if lv.node.is_final and not is_final_decl: diff --git a/mypy/messages.py b/mypy/messages.py index 896ecf2a05a5..9a699ebe75a2 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -955,8 +955,8 @@ def final_cant_override_writeable(self, name: str, ctx: Context) -> None: self.fail('Can\'t override writeable attribute "{}" with a final one'.format(name), ctx) def cant_override_final(self, name: str, base_name: str, ctx: Context) -> None: - self.fail('Cannot override final attribute "{}"'.format(name), ctx) - self.note('(previously declared on base class "{}")'.format(base_name), ctx) + self.fail('Cannot override final attribute "{}"' + ' (previously declared on base class "{}")'.format(name, base_name), ctx) def cant_assign_to_final(self, name: str, attr_assign: bool, ctx: Context) -> None: """Warn about a prohibited assignment to a final attribute. diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index d754ad8cff9c..fa9468223e3b 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -511,10 +511,8 @@ class B(A): def __init__(self) -> None: self.y: Final = 1 class C(B): - x: Final = 2 # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "B") - y: Final = 2 # E: Cannot override final attribute "y" \ - # N: (previously declared on base class "B") + x: Final = 2 # E: Cannot override final attribute "x" (previously declared on base class "B") + y: Final = 2 # E: Cannot override final attribute "y" (previously declared on base class "B") [builtins fixtures/property.pyi] [out] @@ -554,10 +552,8 @@ class B(A): self.y: Final = 1 class C(B): def __init__(self) -> None: - self.x: Final = 2 # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "B") - self.y: Final = 2 # E: Cannot override final attribute "y" \ - # N: (previously declared on base class "B") + self.x: Final = 2 # E: Cannot override final attribute "x" (previously declared on base class "B") + self.y: Final = 2 # E: Cannot override final attribute "y" (previously declared on base class "B") [builtins fixtures/property.pyi] [out] @@ -592,8 +588,7 @@ class B: @property def x(self) -> int: ... class C(A, B): ... -class D(B, A): ... # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "A") +class D(B, A): ... # E: Cannot override final attribute "x" (previously declared on base class "A") C.x = 3 # E: Can't assign to final attribute "x" C().x = 4 # E: Can't assign to final attribute "x" D().x = 4 # E: Can't assign to final attribute "x" \ @@ -611,8 +606,7 @@ class B: @property def x(self) -> int: ... class C(A, B): ... -class D(B, A): ... # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "A") +class D(B, A): ... # E: Cannot override final attribute "x" (previously declared on base class "A") C.x = 3 # E: Can't access instance constant on class object \ # E: Can't assign to final attribute "x" C().x = 4 # E: Can't assign to final attribute "x" @@ -628,8 +622,7 @@ class B: def __init__(self) -> None: self.x = 2 class C(A, B): ... # E: Can't override writeable attribute "x" with a final one -class D(B, A): ... # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "A") +class D(B, A): ... # E: Cannot override final attribute "x" (previously declared on base class "A") C.x = 3 # E: Can't assign to final attribute "x" D.x = 3 # E: Can't assign to final attribute "x" C().x = 4 # E: Can't assign to final attribute "x" @@ -645,17 +638,13 @@ class A: self.y: Final[Any] = 1 class B(A): - def x(self) -> None: pass # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "A") - def y(self) -> None: pass # E: Cannot override final attribute "y" \ - # N: (previously declared on base class "A") + def x(self) -> None: pass # E: Cannot override final attribute "x" (previously declared on base class "A") + def y(self) -> None: pass # E: Cannot override final attribute "y" (previously declared on base class "A") class C(A): - @property # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "A") + @property # E: Cannot override final attribute "x" (previously declared on base class "A") def x(self) -> None: pass - @property # E: Cannot override final attribute "y" \ - # N: (previously declared on base class "A") + @property # E: Cannot override final attribute "y" (previously declared on base class "A") def y(self) -> None: pass [builtins fixtures/property.pyi] [out] @@ -669,11 +658,9 @@ class A: self.y: Final[Any] = 1 class B(A): - @classmethod # E: Cannot override final attribute "x" \ - # N: (previously declared on base class "A") + @classmethod # E: Cannot override final attribute "x" (previously declared on base class "A") def x(self) -> None: pass - @classmethod # E: Cannot override final attribute "y" \ - # N: (previously declared on base class "A") + @classmethod # E: Cannot override final attribute "y" (previously declared on base class "A") def y(self) -> None: pass [builtins fixtures/classmethod.pyi] @@ -692,24 +679,14 @@ class A: def p(self) -> int: pass class B(A): - f = a # E: Cannot override final attribute "f" \ - # N: (previously declared on base class "A") \ - # E: Can't assign to constant "f" - p = a # E: Cannot override final attribute "p" \ - # N: (previously declared on base class "A") \ - # E: Can't assign to constant "p" + f = a # E: Cannot override final attribute "f" (previously declared on base class "A") + p = a # E: Cannot override final attribute "p" (previously declared on base class "A") class C(A): - f: Any # E: Cannot override final attribute "f" \ - # N: (previously declared on base class "A") \ - # E: Can't assign to constant "f" - p: Any # E: Cannot override final attribute "p" \ - # N: (previously declared on base class "A") \ - # E: Can't assign to constant "p" + f: Any # E: Cannot override final attribute "f" (previously declared on base class "A") + p: Any # E: Cannot override final attribute "p" (previously declared on base class "A") class D(A): - f: Final = a # E: Cannot override final attribute "f" \ - # N: (previously declared on base class "A") - p: Final = a # E: Cannot override final attribute "p" \ - # N: (previously declared on base class "A") + f: Final = a # E: Cannot override final attribute "f" (previously declared on base class "A") + p: Final = a # E: Cannot override final attribute "p" (previously declared on base class "A") [builtins fixtures/property.pyi] [out] @@ -728,21 +705,17 @@ class A: class B(A): def __init__(self) -> None: self.f: Any # E: Can't assign to final attribute "f" \ - # E: Cannot override final attribute "f" \ - # N: (previously declared on base class "A") + # E: Cannot override final attribute "f" (previously declared on base class "A") self.c: Any # E: Can't assign to final attribute "c" \ - # E: Cannot override final attribute "c" \ - # N: (previously declared on base class "A") + # E: Cannot override final attribute "c" (previously declared on base class "A") B().f = a # E: Can't assign to final attribute "f" B().c = a # E: Can't assign to final attribute "c" class C(A): def __init__(self) -> None: - self.f: Final = a # E: Cannot override final attribute "f" \ - # N: (previously declared on base class "A") - self.c: Final = a # E: Cannot override final attribute "c" \ - # N: (previously declared on base class "A") + self.f: Final = a # E: Cannot override final attribute "f" (previously declared on base class "A") + self.c: Final = a # E: Cannot override final attribute "c" (previously declared on base class "A") [builtins fixtures/classmethod.pyi] [out] @@ -765,8 +738,7 @@ class B: @final def m(self) -> int: pass -class C(A, B): pass # E: Cannot override final attribute "m" \ - # N: (previously declared on base class "B") +class C(A, B): pass # E: Cannot override final attribute "m" (previously declared on base class "B") class D(B, A): pass [out] @@ -779,8 +751,7 @@ class B: @final def m(self) -> int: pass -class C(A, B): pass # E: Cannot override final attribute "m" \ - # N: (previously declared on base class "B") +class C(A, B): pass # E: Cannot override final attribute "m" (previously declared on base class "B") class D(B, A): pass # E: Can't override writeable attribute "m" with a final one [out] @@ -793,8 +764,7 @@ class B: def f(cls) -> int: pass class C(B): - @classmethod # E: Cannot override final attribute "f" \ - # N: (previously declared on base class "B") + @classmethod # E: Cannot override final attribute "f" (previously declared on base class "B") def f(cls) -> int: pass [builtins fixtures/classmethod.pyi] [out] @@ -808,8 +778,7 @@ class B: def f() -> int: pass class C(B): - @staticmethod # E: Cannot override final attribute "f" \ - # N: (previously declared on base class "B") + @staticmethod # E: Cannot override final attribute "f" (previously declared on base class "B") def f() -> int: pass [builtins fixtures/staticmethod.pyi] [out] @@ -823,8 +792,7 @@ class B: def f(self) -> int: pass class C(B): - @property # E: Cannot override final attribute "f" \ - # N: (previously declared on base class "B") + @property # E: Cannot override final attribute "f" (previously declared on base class "B") def f(self) -> int: pass [builtins fixtures/property.pyi] [out] @@ -842,8 +810,7 @@ class B: pass class C(B): - @overload # E: Cannot override final attribute "f" \ - # N: (previously declared on base class "B") + @overload # E: Cannot override final attribute "f" (previously declared on base class "B") def f(self, x: int) -> int: ... @overload def f(self, x: str) -> str: ... @@ -910,8 +877,7 @@ class B: @final def meth(self) -> None: ... class C(B): - def meth(self) -> None: ... # E: Cannot override final attribute "meth" \ - # N: (previously declared on base class "B") + def meth(self) -> None: ... # E: Cannot override final attribute "meth" (previously declared on base class "B") @final class F: ... diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 3aac52bbc894..16ebc099003e 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4976,5 +4976,4 @@ class C: def meth(self) -> int: ... [out] [out2] -main:4: error: Cannot override final attribute "meth" -main:4: note: (previously declared on base class "C") +main:4: error: Cannot override final attribute "meth" (previously declared on base class "C") diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 82be9a2d109b..4f4b8a7b50b8 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7624,8 +7624,7 @@ class C: def meth(self) -> int: ... [out] == -main:4: error: Cannot override final attribute "meth" -main:4: note: (previously declared on base class "C") +main:4: error: Cannot override final attribute "meth" (previously declared on base class "C") [case testFinalAddFinalMethodOverrideWithVarFine] from mod import C @@ -7651,12 +7650,9 @@ class C: def other(self) -> int: ... [out] == -main:5: error: Cannot override final attribute "meth" -main:5: note: (previously declared on base class "C") -main:5: error: Can't assign to constant "meth" +main:5: error: Cannot override final attribute "meth" (previously declared on base class "C") main:7: error: Can't assign to final attribute "other" -main:7: error: Cannot override final attribute "other" -main:7: note: (previously declared on base class "C") +main:7: error: Cannot override final attribute "other" (previously declared on base class "C") [case testFinalAddFinalMethodOverrideOverloadFine] from typing import overload @@ -7690,8 +7686,7 @@ class C: def meth(self, x: str) -> str: ... [out] == -main:6: error: Cannot override final attribute "meth" -main:6: note: (previously declared on base class "C") +main:6: error: Cannot override final attribute "meth" (previously declared on base class "C") [case testFinalAddFinalPropertyWithVarFine] from mod import C @@ -7720,12 +7715,9 @@ class C: [builtins fixtures/property.pyi] [out] == -main:5: error: Cannot override final attribute "p" -main:5: note: (previously declared on base class "C") -main:5: error: Can't assign to constant "p" +main:5: error: Cannot override final attribute "p" (previously declared on base class "C") main:8: error: Can't assign to final attribute "p" -main:8: error: Cannot override final attribute "p" -main:8: note: (previously declared on base class "C") +main:8: error: Cannot override final attribute "p" (previously declared on base class "C") [case testIfMypyUnreachableClass] from a import x From 53720e3b3433ac44818cc3566e83bed9fc2de828 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 7 Sep 2018 00:18:13 +0100 Subject: [PATCH 39/52] Remove ClassVar discussion --- .../{type_quals.rst => final_attrs.rst} | 59 +------------------ docs/source/index.rst | 2 +- 2 files changed, 2 insertions(+), 59 deletions(-) rename docs/source/{type_quals.rst => final_attrs.rst} (82%) diff --git a/docs/source/type_quals.rst b/docs/source/final_attrs.rst similarity index 82% rename from docs/source/type_quals.rst rename to docs/source/final_attrs.rst index 6a349761d866..5773ffac1029 100644 --- a/docs/source/type_quals.rst +++ b/docs/source/final_attrs.rst @@ -1,62 +1,5 @@ -Type qualifiers -=============== - -This section describes constructs that do not affect types of variables -and methods, but affect how they can be accessed, assigned, and overridden. - -Class and instance variables -**************************** - -By default mypy assumes that a variable declared in the class body is -an instance variable. One can mark names intended to be used as class variables -with a special type qualifier ``typing.ClassVar``. For example: - -.. code-block:: python - - from typing import ClassVar - - class Base: - attr: int # This is an instance variable - num_subclasses: ClassVar[int] # This is a class variable - - def foo(self) -> None: - self.attr = 0 # OK - self.num_subclasses = 0 # Error: Cannot assign to class variable via instance - - Base.num_subclasses = 0 # OK - Base.attr = 0 # Also OK, sets default value for an instance variable - -Note that ``ClassVar`` is not valid as a nested type, and in any position -other than assignment in class body. For example: - -.. code-block:: python - - x: ClassVar[int] # Error: ClassVar can't be used at module scope - - class C: - y: List[ClassVar[int]] # Error: can't use ClassVar as nested type - -Instance variable can not override a class variable in a subclass -and vice-versa: - -.. code-block:: python - - class Base: - x: int - y: ClassVar[int] - - class Derived(Base): - x: ClassVar[int] # Error! - y: int # Error! - -.. note:: - - Assigning a value to a variable in the class body doesn't make it a class - variable, it just sets a default value for an instance variable, *only* - names explicitly declared with ``ClassVar`` are class variables. - Final attributes of classes and modules -*************************************** +======================================= .. note:: diff --git a/docs/source/index.rst b/docs/source/index.rst index 3a80e8f09fe4..173b197ac0f9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,7 +39,7 @@ Mypy is a static type checker for Python 3 and Python 2.7. stubs generics more_types - type_quals + final_attrs metaclasses .. toctree:: From 953119d1bf442f47b752df226a037828bb90a80c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 7 Sep 2018 00:42:35 +0100 Subject: [PATCH 40/52] Update docs --- docs/source/final_attrs.rst | 64 +++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/docs/source/final_attrs.rst b/docs/source/final_attrs.rst index 5773ffac1029..cde3b345d0fc 100644 --- a/docs/source/final_attrs.rst +++ b/docs/source/final_attrs.rst @@ -1,12 +1,6 @@ Final attributes of classes and modules ======================================= -.. note:: - - This is an experimental feature. Some details might change in later - versions of mypy. The final qualifiers are available in ``typing_extensions`` - module. When the semantics is stable, they will be added to ``typing``. - You can declare a variable or attribute as final, which means that the variable must not be assigned a new value after initialization. This is often useful for module and class level constants as a way to prevent unintended modification. @@ -43,22 +37,32 @@ instance attributes from overriding in a subclass: class User(Snowflake): id = uuid.uuid4() # Error: can't override a final attribute -Some other use cases might be solved by using ``@property``, but note that both -above use cases can't be solved this way. For such situations, one might want -to use ``typing_extensions.Final``. +Some other use cases might be solved by using ``@property``, but note that +neither of the above use cases can be solved with it. + +.. note:: + + This is an experimental feature. Some details might change in later + versions of mypy. The final qualifiers are available in ``typing_extensions`` + module. Definition syntax ------------------ +***************** The ``typing_extensions.Final`` type qualifier indicates that a given name or attribute should never be re-assigned, re-defined, nor overridden. It can be used in one of these forms: -* The simplest one is ``ID: Final = 1``. Note that unlike gor generic classes - this is *not* the same as ``Final[Any]``. Here mypy will infer type ``int``. -* An explicit type ``ID: Final[float] = 1`` can be used as in any - normal assignment. +* You can provide an explicit type using the syntax ``Final[]``. Example: + + .. code-block:: python + + ID: Final[float] = 1 + +* You can omit the type: ``ID: Final = 1``. Note that unlike for generic + classes this is *not* the same as ``Final[Any]``. Here mypy will infer + type ``int``. * In stub files one can omit the right hand side and just write ``ID: Final[float]``. @@ -67,7 +71,7 @@ used in one of these forms: but this is allowed *only* in ``__init__`` methods. Definition rules ----------------- +**************** The are two rules that should be always followed when defining a final name: @@ -122,16 +126,16 @@ The are two rules that should be always followed when defining a final name: def fun(x: Final[List[int]]) -> None: # Error! ... -* ``Final`` and ``ClassVar`` should not be used together, mypy will infer +* ``Final`` and ``ClassVar`` should not be used together. Mypy will infer the scope of a final declaration automatically depending on whether it was initialized in class body or in ``__init__``. .. note:: Conditional final declarations and final declarations within loops are - not supported. + rejected. Using final attributes ----------------------- +********************** As a result of a final declaration mypy strives to provide the two following guarantees: @@ -206,7 +210,7 @@ two following guarantees: then re-exported. Final methods -------------- +************* Like with attributes, sometimes it is useful to protect a method from overriding. In such situations one can use a ``typing_extensions.final`` @@ -229,9 +233,9 @@ decorator: ... This ``@final`` decorator can be used with instance methods, class methods, -static methods, and properties (this includes overloaded methods). For overloaded -methods it is enough to add ``@final`` on at leats one of overloads (or on -the implementation) to make it final: +static methods, and properties (this includes overloaded methods). For +overloaded methods one should add ``@final`` on the implementation to make +it final (or on the first overload in stubs): .. code-block:: python from typing import Any, overload @@ -250,29 +254,27 @@ the implementation) to make it final: ... Final classes -------------- +************* -As a bonus, applying a ``typing_extensions.final`` decorator to a class indicates to mypy -that it can't be subclassed. Mypy doesn't provide any additional features for -final classes, but some other tools may use this information for their benefits. -Plus it serves a verifiable documentation purpose: +You can apply a ``typing_extensions.final`` decorator to a class indicates +to mypy that it can't be subclassed. The decorator acts as a declaration +for mypy (and as documentation for humans), but it doesn't prevent subclassing +at runtime: .. code-block:: python - # file lib.pyi from typing_extensions import final @final class Leaf: ... - # file main.py from lib import Leaf - class MyLeaf(Leaf): # Error: library author believes this is unsafe + class MyLeaf(Leaf): # Error: Leaf can't be subclassed ... -Some situations where this may be useful include: +Here are some situations where using a final class may be useful: * A class wasn't designed to be subclassed. Perhaps subclassing does not work as expected, or it's error-prone. From ed009ee52436139e73dab625fe21581c926b56a7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 7 Sep 2018 01:26:14 +0100 Subject: [PATCH 41/52] Minor fixes in code --- docs/source/final_attrs.rst | 2 +- mypy/checker.py | 34 ++++++++++++++++++++++++------ mypy/checkexpr.py | 5 +---- mypy/messages.py | 2 +- mypy/nodes.py | 3 +++ mypy/semanal.py | 21 ++++++++++++------ mypy/server/aststrip.py | 1 + mypy/test/data.py | 10 +-------- test-data/unit/lib-stub/typing.pyi | 1 + 9 files changed, 52 insertions(+), 27 deletions(-) diff --git a/docs/source/final_attrs.rst b/docs/source/final_attrs.rst index cde3b345d0fc..7c390505e15d 100644 --- a/docs/source/final_attrs.rst +++ b/docs/source/final_attrs.rst @@ -49,7 +49,7 @@ neither of the above use cases can be solved with it. Definition syntax ***************** -The ``typing_extensions.Final`` type qualifier indicates that a given name or +The ``typing_extensions.Final`` qualifier indicates that a given name or attribute should never be re-assigned, re-defined, nor overridden. It can be used in one of these forms: diff --git a/mypy/checker.py b/mypy/checker.py index b88c496e5ea0..21c7664c6c4d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -219,7 +219,7 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option self.recurse_into_functions = True # This internal flag is used to track whether we a currently type-checking # a final declaration (assignment), so that some errors should be suppressed. - # Should not be set manually, use get_final_context/set_final_context instead. + # Should not be set manually, use get_final_context/enter_final_context instead. # NOTE: we use the context manager to avoid "threading" an additional `is_final_def` # argument through various `checker` and `checkmember` functions. self._is_final_def = False @@ -1287,7 +1287,7 @@ def check_method_override_for_base_with_name( else: context = defn.func - # First, check if we don't override a final (always an error, even with Any types). + # First, check if we override a final (always an error, even with Any types). if isinstance(base_attr.node, (Var, FuncBase, Decorator)) and base_attr.node.is_final: self.msg.cant_override_final(name, base.name(), defn) # Second, final can't override anything writeable independently of types. @@ -1634,7 +1634,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: Handle all kinds of assignment statements (simple, indexed, multiple). """ - with self.set_final_context(s.is_final_def): + with self.enter_final_context(s.is_final_def): self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax) if (s.type is not None and @@ -1915,6 +1915,15 @@ def check_compatibility_classvar_super(self, node: Var, def check_compatibility_final_super(self, node: Var, base: TypeInfo, base_node: Optional[Node]) -> bool: + """Check if an assignment overrides a final attribute in a base class. + + This only check situations where either a node in base class is not a variable + but a final method, or where override is explicitly declared as final. + In these cases we give a more detailed error message. In addition, we check that + a final variable doesn't override writeable attribute, which is not safe. + + Other situations are checked in `check_final()`. + """ if not isinstance(base_node, (Var, FuncBase, Decorator)): return True if base_node.is_final and (node.is_final or not isinstance(base_node, Var)): @@ -1929,6 +1938,17 @@ def check_compatibility_final_super(self, node: Var, return True def check_no_writeable(self, name: str, base_node: Optional[Node], ctx: Context) -> None: + """Check that a final variable doesn't override writeable attribute. + + This is done to prevent situations like this: + class C: + attr = 1 + class D(C): + attr: Final = 2 + + x: C = D() + x.attr = 3 # Oops! + """ if isinstance(base_node, Var): ok = False elif isinstance(base_node, OverloadedFuncDef) and base_node.is_property: @@ -1940,10 +1960,12 @@ def check_no_writeable(self, name: str, base_node: Optional[Node], ctx: Context) self.msg.final_cant_override_writeable(name, ctx) def get_final_context(self) -> bool: + """Check whether we a currently checking a final declaration.""" return self._is_final_def @contextmanager - def set_final_context(self, is_final_def: bool) -> Iterator[None]: + def enter_final_context(self, is_final_def: bool) -> Iterator[None]: + """Store whether the current checked assignment is a final declaration.""" old_ctx = self._is_final_def self._is_final_def = is_final_def try: @@ -1975,7 +1997,7 @@ def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: name = lv.node.name() cls = self.scope.active_class() if cls is not None: - # Theses additional checks exist to give more errors messages + # Theses additional checks exist to give more error messages # even if the final attribute was overridden with a new symbol # (which is itself an error)... for base in cls.mro[1:]: @@ -2641,7 +2663,7 @@ def visit_operator_assignment_stmt(self, if isinstance(s.lvalue, MemberExpr): # Special case, some additional errors may be given for # assignments to read-only or final attributes. - lvalue_type = self.expr_checker._visit_member_expr(s.lvalue, True) + lvalue_type = self.expr_checker.visit_member_expr(s.lvalue, True) else: lvalue_type = self.expr_checker.accept(s.lvalue) inplace, method = infer_operator_assignment_method(lvalue_type, s.op) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 141bc92ac933..03307881f954 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1609,11 +1609,8 @@ def apply_generic_arguments(self, callable: CallableType, types: Sequence[Option """Simple wrapper around mypy.applytype.apply_generic_arguments.""" return applytype.apply_generic_arguments(callable, types, self.msg, context) - def visit_member_expr(self, e: MemberExpr) -> Type: + def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: """Visit member expression (of form e.id).""" - return self._visit_member_expr(e, False) - - def _visit_member_expr(self, e: MemberExpr, is_lvalue: bool) -> Type: self.chk.module_refs.update(extract_refexpr_names(e)) result = self.analyze_ordinary_member_access(e, is_lvalue) return self.narrow_type_from_binder(e, result) diff --git a/mypy/messages.py b/mypy/messages.py index 7c7929edb221..d3c778e1965b 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -956,7 +956,7 @@ def final_cant_override_writeable(self, name: str, ctx: Context) -> None: def cant_override_final(self, name: str, base_name: str, ctx: Context) -> None: self.fail('Cannot override final attribute "{}"' - ' (previously declared on base class "{}")'.format(name, base_name), ctx) + ' (previously declared in base class "{}")'.format(name, base_name), ctx) def cant_assign_to_final(self, name: str, attr_assign: bool, ctx: Context) -> None: """Warn about a prohibited assignment to a final attribute. diff --git a/mypy/nodes.py b/mypy/nodes.py index 550dc5707540..da2f0781aaaa 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -788,6 +788,8 @@ def serialize(self) -> JsonDict: 'type': None if self.type is None else self.type.serialize(), 'flags': get_flags(self, VAR_FLAGS), } # type: JsonDict + if self.final_value is not None: + data['final_value'] = self.final_value return data @classmethod @@ -798,6 +800,7 @@ def deserialize(cls, data: JsonDict) -> 'Var': v = Var(name, type) v._fullname = data['fullname'] set_flags(v, data['flags']) + v.final_value = data.get('final_value') return v diff --git a/mypy/semanal.py b/mypy/semanal.py index cd44ec24ff3a..7b09853119d8 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1683,7 +1683,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.unwrap_final(s) def final_cb() -> None: - self.fail("Final can't redefine an existing name, ignoring", s) + self.fail("Final can't redefine an existing name", s) s.is_final_def = False for lval in s.lvalues: @@ -1743,18 +1743,18 @@ def unwrap_final(self, s: AssignmentStmt) -> None: else: s.type = s.type.args[0] if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], RefExpr): - self.fail("Invalid final declaration, ignoring", s) + self.fail("Invalid final declaration", s) return lval = s.lvalues[0] assert isinstance(lval, RefExpr) s.is_final_def = True if self.loop_depth > 0: - self.fail("Final declarations are prohibited within loops", s) + self.fail("Cannot use Final inside a loop", s) if self.type and self.type.is_protocol: self.msg.protocol_members_cant_be_final(s) if (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file and not self.is_class_scope()): - self.fail("Final names outside stubs must have a value", s) + self.fail("Final name must be initialized with a value", s) return def check_final_implicit_def(self, s: AssignmentStmt) -> None: @@ -1774,8 +1774,7 @@ def check_final_implicit_def(self, s: AssignmentStmt) -> None: else: assert self.function_stack if self.function_stack[-1].name() != '__init__': - self.fail("Can only declare final attributes in class body" - " or __init__ method", s) + self.fail("Can only declare final attributes in class body or __init__", s) s.is_final_def = False return @@ -1950,6 +1949,8 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False, nested: If true, the lvalue is within a tuple or list lvalue expression add_global: Add name to globals table only if this is true (used in first pass) explicit_type: Assignment has type annotation + final_cb: A callback to call in situation where a final declaration on `self` + overrides an existing name (only used by `analyze_member_lvalue`). """ if isinstance(lval, NameExpr): # Top-level definitions within some statements (at least while) are @@ -2071,6 +2072,14 @@ def analyze_tuple_or_list_lvalue(self, lval: TupleExpr, def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False, final_cb: Optional[Callable[[], None]] = None) -> None: + """Analyze lvalue that is a member expression. + + Arguments: + lval: The target lvalue + explicit_type: Assignment has type annotation + final_cb: A callback to call in situation where a final declaration on `self` + overrides an existing name. + """ lval.accept(self) if self.is_self_member_ref(lval): assert self.type, "Self member outside a class" diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py index 6bf590842504..5314ca7f3f5b 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -282,6 +282,7 @@ def _reset_var_final_flags(self, v: Var) -> None: v.is_final = False v.final_unset_in_class = False v.final_set_in_init = False + v.final_value = None def visit_index_expr(self, node: IndexExpr) -> None: node.analyzed = None # was a type alias diff --git a/mypy/test/data.py b/mypy/test/data.py index f785b2e3948e..217d3489d89c 100644 --- a/mypy/test/data.py +++ b/mypy/test/data.py @@ -188,14 +188,6 @@ def parse_test_cases(parent: 'DataSuiteCollector', suite: 'DataSuite', path, p[i0].line)) -MYPY = False -if MYPY: - if sys.version_info >= (3, 5): - TmpDir = tempfile.TemporaryDirectory[str] - else: - TmpDir = tempfile.TemporaryDirectory - - class DataDrivenTestCase(pytest.Item): # type: ignore # inheriting from Any """Holds parsed data-driven test cases, and handles directory setup and teardown.""" @@ -238,7 +230,7 @@ def __init__(self, super().__init__(name, parent) self.skip = skip self.old_cwd = None # type: Optional[str] - self.tmpdir = None # type: Optional[TmpDir] + self.tmpdir = None # type: Optional[tempfile.TemporaryDirectory[str]] self.input = input self.output = output self.output2 = output2 diff --git a/test-data/unit/lib-stub/typing.pyi b/test-data/unit/lib-stub/typing.pyi index 6bc05958c41f..e57f71491a61 100644 --- a/test-data/unit/lib-stub/typing.pyi +++ b/test-data/unit/lib-stub/typing.pyi @@ -56,6 +56,7 @@ class Mapping(Generic[T_contra, T_co]): def runtime(cls: type) -> type: pass +# This is an unofficial extension. def final(meth: T) -> T: pass TYPE_CHECKING = 1 From b8c67aac1e80f9e48e3d52992e81ce92ab1700d0 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 7 Sep 2018 02:42:43 +0100 Subject: [PATCH 42/52] Stricter rules for final in overloads; update tests --- mypy/checker.py | 2 +- mypy/messages.py | 3 + mypy/semanal.py | 17 +++- test-data/unit/check-final.test | 116 +++++++++++++++----------- test-data/unit/check-incremental.test | 2 +- test-data/unit/fine-grained.test | 12 +-- 6 files changed, 91 insertions(+), 61 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 21c7664c6c4d..20d145d793d3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1991,7 +1991,7 @@ def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: assert isinstance(lv.node, Var) if (lv.node.final_unset_in_class and not lv.node.final_set_in_init and not self.is_stub): - self.fail('Final names outside stubs must have a value', s) + self.msg.final_without_value(s) for lv in lvs: if isinstance(lv, RefExpr) and isinstance(lv.node, Var): name = lv.node.name() diff --git a/mypy/messages.py b/mypy/messages.py index d3c778e1965b..edbb99f3dcc5 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -969,6 +969,9 @@ def cant_assign_to_final(self, name: str, attr_assign: bool, ctx: Context) -> No def protocol_members_cant_be_final(self, ctx: Context) -> None: self.fail("Protocol members can't be final", ctx) + def final_without_value(self, ctx: Context) -> None: + self.fail("Final name must be initialized with a value", ctx) + def read_only_property(self, name: str, type: TypeInfo, context: Context) -> None: self.fail('Property "{}" defined in "{}" is read-only'.format( diff --git a/mypy/semanal.py b/mypy/semanal.py index 7b09853119d8..378d2e9c64ad 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -614,10 +614,21 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: # redefinitions already. return - # Check final status, if at least one item or the implementation is marked - # as @final, then the whole overloaded definition if @final. + # Check final status, if the implementation is marked + # as @final (or the first overload in stubs), then the whole overloaded + # definition if @final. if any(item.is_final for item in defn.items): + # We anyway mark it as final because it was probably the intention. defn.is_final = True + # Only show the error once per overload + bad_final = next(ov for ov in defn.items if ov.is_final) + if not self.is_stub_file: + self.fail("@final should be applied only to overload implementation", + bad_final) + elif any(item.is_final for item in defn.items[1:]): + bad_final = next(ov for ov in defn.items[1:] if ov.is_final) + self.fail("In stub files @final should be applied only to the first overload", + bad_final) if defn.impl is not None and defn.impl.is_final: defn.is_final = True @@ -1754,7 +1765,7 @@ def unwrap_final(self, s: AssignmentStmt) -> None: self.msg.protocol_members_cant_be_final(s) if (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file and not self.is_class_scope()): - self.fail("Final name must be initialized with a value", s) + self.msg.final_without_value(s) return def check_final_implicit_def(self, s: AssignmentStmt) -> None: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index fa9468223e3b..075dd438c1db 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -48,7 +48,7 @@ reveal_type(C((1, 2)).y) # E: Revealed type is 'builtins.float' [case testFinalBadDefinitionTooManyArgs] from typing import Final -x: Final[int, str] # E: Final names outside stubs must have a value \ +x: Final[int, str] # E: Final name must be initialized with a value \ # E: Final[...] takes at most one type argument reveal_type(x) # E: Revealed type is 'builtins.int' @@ -61,10 +61,10 @@ reveal_type(C().x) # E: Revealed type is 'builtins.float' [case testFinalInvalidDefinitions] from typing import Final, Any -x = y = 1 # type: Final[float] # E: Invalid final declaration, ignoring +x = y = 1 # type: Final[float] # E: Invalid final declaration z: Any z[0]: Final # E: Unexpected type declaration \ - # E: Invalid final declaration, ignoring + # E: Invalid final declaration [out] [case testFinalDefiningInstanceVarStubs] @@ -122,13 +122,21 @@ class C: def f(self, x): pass + @overload + def bad(self, x: int) -> int: ... + @final # E: @final should be applied only to overload implementation + @overload + def bad(self, x: str) -> str: ... + def bad(self, x): + pass + reveal_type(C().f) # E: Revealed type is 'Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)' [out] [case testFinalDefiningMethOverloadedStubs] from mod import C -reveal_type(C().f) # E: Revealed type is 'Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)' +reveal_type(C().f) [file mod.pyi] from typing import final, overload @@ -138,7 +146,15 @@ class C: def f(self, x: int) -> int: ... @overload def f(self, x: str) -> str: ... + + @overload + def bad(self, x: int) -> int: ... + @final # Error! + @overload + def bad(self, x: str) -> str: ... [out] +tmp/mod.pyi:12: error: In stub files @final should be applied only to the first overload +main:3: error: Revealed type is 'Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)' [case testFinalDefiningProperty] from typing import final @@ -166,13 +182,13 @@ def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost ty [case testFinalDefiningNoRhs] from typing import Final -x: Final # E: Final names outside stubs must have a value -y: Final[int] # E: Final names outside stubs must have a value +x: Final # E: Final name must be initialized with a value +y: Final[int] # E: Final name must be initialized with a value class C: - x: Final # E: Final names outside stubs must have a value - y: Final[int] # E: Final names outside stubs must have a value + x: Final # E: Final name must be initialized with a value + y: Final[int] # E: Final name must be initialized with a value def __init__(self) -> None: - self.z: Final # E: Final names outside stubs must have a value + self.z: Final # E: Final name must be initialized with a value reveal_type(x) # E: Revealed type is 'Any' reveal_type(y) # E: Revealed type is 'builtins.int' reveal_type(C().x) # E: Revealed type is 'Any' @@ -211,8 +227,8 @@ from typing import Final, Any, Tuple class C: def meth(self, x: Tuple[int, Any]) -> None: - self.x: Final = x # E: Can only declare final attributes in class body or __init__ method - self.y: Final[float] = 1 # E: Can only declare final attributes in class body or __init__ method + self.x: Final = x # E: Can only declare final attributes in class body or __init__ + self.y: Final[float] = 1 # E: Can only declare final attributes in class body or __init__ [out] [case testFinalDefiningOnlyOnSelf] @@ -250,10 +266,10 @@ class P(Protocol): from typing import Final for i in [1, 2, 3]: - x: Final = i # E: Final declarations are prohibited within loops + x: Final = i # E: Cannot use Final inside a loop while True: - y: Final = True # E: Final declarations are prohibited within loops + y: Final = True # E: Cannot use Final inside a loop [builtins fixtures/list.pyi] [out] @@ -262,7 +278,7 @@ from typing import Final class C: x: Final[int] # OK, defined in __init__ - bad: Final[int] # E: Final names outside stubs must have a value + bad: Final[int] # E: Final name must be initialized with a value def __init__(self, x: int) -> None: self.x = x # OK, deferred definition @@ -293,12 +309,12 @@ def f() -> int: y = 1 y: Final = 2 # E: Name 'y' already defined on line 10 \ - # E: Final can't redefine an existing name, ignoring + # E: Final can't redefine an existing name y = 3 # No error here, first definition wins z: Final = 1 z: Final = 2 # E: Name 'z' already defined on line 14 \ - # E: Final can't redefine an existing name, ignoring \ + # E: Final can't redefine an existing name \ # E: Can't assign to constant "z" z = 3 # E: Can't assign to constant "z" [out] @@ -311,7 +327,7 @@ from lib.mod import ID X = 1 # E: Can't assign to constant "X" ID: Final = 1 # E: Name 'ID' already defined (possibly by an import) \ - # E: Final can't redefine an existing name, ignoring \ + # E: Final can't redefine an existing name \ # E: Can't assign to constant "ID" ID = 1 # E: Can't assign to constant "ID" [file lib/__init__.pyi] @@ -336,7 +352,7 @@ def f() -> None: x = 1 # E: Can't assign to constant "x" y: Final = 1 - y: Final = 2 # E: Final can't redefine an existing name, ignoring \ + y: Final = 2 # E: Final can't redefine an existing name \ # E: Can't assign to constant "y" def nested() -> None: nonlocal nl @@ -359,7 +375,7 @@ class C: x = 2 # E: Can't assign to constant "x" y = 1 - y: Final = 2 # E: Final can't redefine an existing name, ignoring + y: Final = 2 # E: Final can't redefine an existing name [out] [case testFinalReassignInstanceVarInit] @@ -369,7 +385,7 @@ class C: def __init__(self) -> None: self.x: Final = 1 self.y = 1 - self.y: Final = 2 # E: Final can't redefine an existing name, ignoring + self.y: Final = 2 # E: Final can't redefine an existing name def meth(self) -> None: self.x = 2 # E: Can't assign to final attribute "x" [out] @@ -511,8 +527,8 @@ class B(A): def __init__(self) -> None: self.y: Final = 1 class C(B): - x: Final = 2 # E: Cannot override final attribute "x" (previously declared on base class "B") - y: Final = 2 # E: Cannot override final attribute "y" (previously declared on base class "B") + x: Final = 2 # E: Cannot override final attribute "x" (previously declared in base class "B") + y: Final = 2 # E: Cannot override final attribute "y" (previously declared in base class "B") [builtins fixtures/property.pyi] [out] @@ -552,8 +568,8 @@ class B(A): self.y: Final = 1 class C(B): def __init__(self) -> None: - self.x: Final = 2 # E: Cannot override final attribute "x" (previously declared on base class "B") - self.y: Final = 2 # E: Cannot override final attribute "y" (previously declared on base class "B") + self.x: Final = 2 # E: Cannot override final attribute "x" (previously declared in base class "B") + self.y: Final = 2 # E: Cannot override final attribute "y" (previously declared in base class "B") [builtins fixtures/property.pyi] [out] @@ -588,7 +604,7 @@ class B: @property def x(self) -> int: ... class C(A, B): ... -class D(B, A): ... # E: Cannot override final attribute "x" (previously declared on base class "A") +class D(B, A): ... # E: Cannot override final attribute "x" (previously declared in base class "A") C.x = 3 # E: Can't assign to final attribute "x" C().x = 4 # E: Can't assign to final attribute "x" D().x = 4 # E: Can't assign to final attribute "x" \ @@ -606,7 +622,7 @@ class B: @property def x(self) -> int: ... class C(A, B): ... -class D(B, A): ... # E: Cannot override final attribute "x" (previously declared on base class "A") +class D(B, A): ... # E: Cannot override final attribute "x" (previously declared in base class "A") C.x = 3 # E: Can't access instance constant on class object \ # E: Can't assign to final attribute "x" C().x = 4 # E: Can't assign to final attribute "x" @@ -622,7 +638,7 @@ class B: def __init__(self) -> None: self.x = 2 class C(A, B): ... # E: Can't override writeable attribute "x" with a final one -class D(B, A): ... # E: Cannot override final attribute "x" (previously declared on base class "A") +class D(B, A): ... # E: Cannot override final attribute "x" (previously declared in base class "A") C.x = 3 # E: Can't assign to final attribute "x" D.x = 3 # E: Can't assign to final attribute "x" C().x = 4 # E: Can't assign to final attribute "x" @@ -638,13 +654,13 @@ class A: self.y: Final[Any] = 1 class B(A): - def x(self) -> None: pass # E: Cannot override final attribute "x" (previously declared on base class "A") - def y(self) -> None: pass # E: Cannot override final attribute "y" (previously declared on base class "A") + def x(self) -> None: pass # E: Cannot override final attribute "x" (previously declared in base class "A") + def y(self) -> None: pass # E: Cannot override final attribute "y" (previously declared in base class "A") class C(A): - @property # E: Cannot override final attribute "x" (previously declared on base class "A") + @property # E: Cannot override final attribute "x" (previously declared in base class "A") def x(self) -> None: pass - @property # E: Cannot override final attribute "y" (previously declared on base class "A") + @property # E: Cannot override final attribute "y" (previously declared in base class "A") def y(self) -> None: pass [builtins fixtures/property.pyi] [out] @@ -658,9 +674,9 @@ class A: self.y: Final[Any] = 1 class B(A): - @classmethod # E: Cannot override final attribute "x" (previously declared on base class "A") + @classmethod # E: Cannot override final attribute "x" (previously declared in base class "A") def x(self) -> None: pass - @classmethod # E: Cannot override final attribute "y" (previously declared on base class "A") + @classmethod # E: Cannot override final attribute "y" (previously declared in base class "A") def y(self) -> None: pass [builtins fixtures/classmethod.pyi] @@ -679,14 +695,14 @@ class A: def p(self) -> int: pass class B(A): - f = a # E: Cannot override final attribute "f" (previously declared on base class "A") - p = a # E: Cannot override final attribute "p" (previously declared on base class "A") + f = a # E: Cannot override final attribute "f" (previously declared in base class "A") + p = a # E: Cannot override final attribute "p" (previously declared in base class "A") class C(A): - f: Any # E: Cannot override final attribute "f" (previously declared on base class "A") - p: Any # E: Cannot override final attribute "p" (previously declared on base class "A") + f: Any # E: Cannot override final attribute "f" (previously declared in base class "A") + p: Any # E: Cannot override final attribute "p" (previously declared in base class "A") class D(A): - f: Final = a # E: Cannot override final attribute "f" (previously declared on base class "A") - p: Final = a # E: Cannot override final attribute "p" (previously declared on base class "A") + f: Final = a # E: Cannot override final attribute "f" (previously declared in base class "A") + p: Final = a # E: Cannot override final attribute "p" (previously declared in base class "A") [builtins fixtures/property.pyi] [out] @@ -705,17 +721,17 @@ class A: class B(A): def __init__(self) -> None: self.f: Any # E: Can't assign to final attribute "f" \ - # E: Cannot override final attribute "f" (previously declared on base class "A") + # E: Cannot override final attribute "f" (previously declared in base class "A") self.c: Any # E: Can't assign to final attribute "c" \ - # E: Cannot override final attribute "c" (previously declared on base class "A") + # E: Cannot override final attribute "c" (previously declared in base class "A") B().f = a # E: Can't assign to final attribute "f" B().c = a # E: Can't assign to final attribute "c" class C(A): def __init__(self) -> None: - self.f: Final = a # E: Cannot override final attribute "f" (previously declared on base class "A") - self.c: Final = a # E: Cannot override final attribute "c" (previously declared on base class "A") + self.f: Final = a # E: Cannot override final attribute "f" (previously declared in base class "A") + self.c: Final = a # E: Cannot override final attribute "c" (previously declared in base class "A") [builtins fixtures/classmethod.pyi] [out] @@ -738,7 +754,7 @@ class B: @final def m(self) -> int: pass -class C(A, B): pass # E: Cannot override final attribute "m" (previously declared on base class "B") +class C(A, B): pass # E: Cannot override final attribute "m" (previously declared in base class "B") class D(B, A): pass [out] @@ -751,7 +767,7 @@ class B: @final def m(self) -> int: pass -class C(A, B): pass # E: Cannot override final attribute "m" (previously declared on base class "B") +class C(A, B): pass # E: Cannot override final attribute "m" (previously declared in base class "B") class D(B, A): pass # E: Can't override writeable attribute "m" with a final one [out] @@ -764,7 +780,7 @@ class B: def f(cls) -> int: pass class C(B): - @classmethod # E: Cannot override final attribute "f" (previously declared on base class "B") + @classmethod # E: Cannot override final attribute "f" (previously declared in base class "B") def f(cls) -> int: pass [builtins fixtures/classmethod.pyi] [out] @@ -778,7 +794,7 @@ class B: def f() -> int: pass class C(B): - @staticmethod # E: Cannot override final attribute "f" (previously declared on base class "B") + @staticmethod # E: Cannot override final attribute "f" (previously declared in base class "B") def f() -> int: pass [builtins fixtures/staticmethod.pyi] [out] @@ -792,7 +808,7 @@ class B: def f(self) -> int: pass class C(B): - @property # E: Cannot override final attribute "f" (previously declared on base class "B") + @property # E: Cannot override final attribute "f" (previously declared in base class "B") def f(self) -> int: pass [builtins fixtures/property.pyi] [out] @@ -810,7 +826,7 @@ class B: pass class C(B): - @overload # E: Cannot override final attribute "f" (previously declared on base class "B") + @overload # E: Cannot override final attribute "f" (previously declared in base class "B") def f(self, x: int) -> int: ... @overload def f(self, x: str) -> str: ... @@ -877,7 +893,7 @@ class B: @final def meth(self) -> None: ... class C(B): - def meth(self) -> None: ... # E: Cannot override final attribute "meth" (previously declared on base class "B") + def meth(self) -> None: ... # E: Cannot override final attribute "meth" (previously declared in base class "B") @final class F: ... diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index d299d32eee22..564b14fe4ec6 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4978,7 +4978,7 @@ class C: def meth(self) -> int: ... [out] [out2] -main:4: error: Cannot override final attribute "meth" (previously declared on base class "C") +main:4: error: Cannot override final attribute "meth" (previously declared in base class "C") -- These tests should just not crash [case testOverrideByBadVar] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 4f4b8a7b50b8..ae02abc0e4bd 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7624,7 +7624,7 @@ class C: def meth(self) -> int: ... [out] == -main:4: error: Cannot override final attribute "meth" (previously declared on base class "C") +main:4: error: Cannot override final attribute "meth" (previously declared in base class "C") [case testFinalAddFinalMethodOverrideWithVarFine] from mod import C @@ -7650,9 +7650,9 @@ class C: def other(self) -> int: ... [out] == -main:5: error: Cannot override final attribute "meth" (previously declared on base class "C") +main:5: error: Cannot override final attribute "meth" (previously declared in base class "C") main:7: error: Can't assign to final attribute "other" -main:7: error: Cannot override final attribute "other" (previously declared on base class "C") +main:7: error: Cannot override final attribute "other" (previously declared in base class "C") [case testFinalAddFinalMethodOverrideOverloadFine] from typing import overload @@ -7686,7 +7686,7 @@ class C: def meth(self, x: str) -> str: ... [out] == -main:6: error: Cannot override final attribute "meth" (previously declared on base class "C") +main:6: error: Cannot override final attribute "meth" (previously declared in base class "C") [case testFinalAddFinalPropertyWithVarFine] from mod import C @@ -7715,9 +7715,9 @@ class C: [builtins fixtures/property.pyi] [out] == -main:5: error: Cannot override final attribute "p" (previously declared on base class "C") +main:5: error: Cannot override final attribute "p" (previously declared in base class "C") main:8: error: Can't assign to final attribute "p" -main:8: error: Cannot override final attribute "p" (previously declared on base class "C") +main:8: error: Cannot override final attribute "p" (previously declared in base class "C") [case testIfMypyUnreachableClass] from a import x From c9abd9db835240962467a9ee164f4bbb50d56545 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 7 Sep 2018 02:54:18 +0100 Subject: [PATCH 43/52] Minor fixes --- docs/source/final_attrs.rst | 4 ++-- mypy/checker.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/final_attrs.rst b/docs/source/final_attrs.rst index 7c390505e15d..49ef17fa4fef 100644 --- a/docs/source/final_attrs.rst +++ b/docs/source/final_attrs.rst @@ -1,5 +1,5 @@ -Final attributes of classes and modules -======================================= +Final names, methods and classes +================================ You can declare a variable or attribute as final, which means that the variable must not be assigned a new value after initialization. This is often useful for diff --git a/mypy/checker.py b/mypy/checker.py index 20d145d793d3..e9ccd5e5769c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -21,7 +21,7 @@ ComparisonExpr, StarExpr, EllipsisExpr, RefExpr, PromoteExpr, Import, ImportFrom, ImportAll, ImportBase, TypeAlias, ARG_POS, ARG_STAR, LITERAL_TYPE, MDEF, GDEF, - CONTRAVARIANT, COVARIANT, INVARIANT + CONTRAVARIANT, COVARIANT, INVARIANT, ) from mypy import nodes from mypy.literals import literal, literal_hash From 41b996faa298c3eb29db339cecf38e75a851dbb7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 11:14:22 +0100 Subject: [PATCH 44/52] Address some CR --- mypy/checker.py | 18 +++++---- mypy/semanal.py | 39 +++++++++++-------- mypy/typeanal.py | 2 +- test-data/unit/check-final.test | 66 ++++++++++++++++++++++++++++----- 4 files changed, 92 insertions(+), 33 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e9ccd5e5769c..23270bf14669 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1260,6 +1260,17 @@ def check_method_or_accessor_override_for_base(self, defn: Union[FuncBase, Decor """Check if method definition is compatible with a base class.""" if base: name = defn.name() + base_attr = base.names.get(name) + if base_attr: + # First, check if we override a final (always an error, even with Any types). + if (isinstance(base_attr.node, (Var, FuncBase, Decorator)) + and base_attr.node.is_final): + self.msg.cant_override_final(name, base.name(), defn) + # Second, final can't override anything writeable independently of types. + if defn.is_final: + self.check_no_writeable(name, base_attr.node, defn) + + # Check the type of override. if name not in ('__init__', '__new__', '__init_subclass__'): # Check method override # (__init__, __new__, __init_subclass__ are special). @@ -1287,13 +1298,6 @@ def check_method_override_for_base_with_name( else: context = defn.func - # First, check if we override a final (always an error, even with Any types). - if isinstance(base_attr.node, (Var, FuncBase, Decorator)) and base_attr.node.is_final: - self.msg.cant_override_final(name, base.name(), defn) - # Second, final can't override anything writeable independently of types. - if defn.is_final: - self.check_no_writeable(name, base_attr.node, defn) - # Construct the type of the overriding method. if isinstance(defn, FuncBase): typ = self.function_type(defn) # type: Type diff --git a/mypy/semanal.py b/mypy/semanal.py index 378d2e9c64ad..dfdaa2e1c849 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1693,9 +1693,10 @@ def add_type_alias_deps(self, aliases_used: Iterable[str], def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.unwrap_final(s) - def final_cb() -> None: - self.fail("Final can't redefine an existing name", s) - s.is_final_def = False + def final_cb(keep_final: bool) -> None: + self.fail("Cannot redefine an existing name as final", s) + if not keep_final: + s.is_final_def = False for lval in s.lvalues: self.analyze_lvalue(lval, explicit_type=s.type is not None, @@ -1952,7 +1953,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: def analyze_lvalue(self, lval: Lvalue, nested: bool = False, add_global: bool = False, explicit_type: bool = False, - final_cb: Optional[Callable[[], None]] = None) -> None: + final_cb: Optional[Callable[[bool], None]] = None) -> None: """Analyze an lvalue or assignment target. Args: @@ -1961,7 +1962,7 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False, add_global: Add name to globals table only if this is true (used in first pass) explicit_type: Assignment has type annotation final_cb: A callback to call in situation where a final declaration on `self` - overrides an existing name (only used by `analyze_member_lvalue`). + overrides an existing name. """ if isinstance(lval, NameExpr): # Top-level definitions within some statements (at least while) are @@ -2018,8 +2019,8 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False, lval.kind = MDEF lval.fullname = lval.name self.type.names[lval.name] = SymbolTableNode(MDEF, v) - elif explicit_type: - # Don't re-bind types + else: + # An existing name, try to find the original definition. global_def = self.globals.get(lval.name) if self.locals: locals_last = self.locals[-1] @@ -2032,15 +2033,21 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False, type_def = self.type.names.get(lval.name) if self.type else None original_def = global_def or local_def or type_def - self.name_already_defined(lval.name, lval, original_def) - if final_cb is not None: - final_cb() - else: - # Bind to an existing name. + + # Redefining an existing name with final is always an error. if final_cb is not None: - final_cb() - lval.accept(self) - self.check_lvalue_validity(lval.node, lval) + # We avoid extra errors if the original definition is also final + # by keeping the final status of this assignment. + keep_final = (original_def and isinstance(original_def.node, Var) and + original_def.node.is_final) + final_cb(keep_final) + if explicit_type: + # Don't re-bind types + self.name_already_defined(lval.name, lval, original_def) + else: + # Bind to an existing name. + lval.accept(self) + self.check_lvalue_validity(lval.node, lval) elif isinstance(lval, MemberExpr): if not add_global: self.analyze_member_lvalue(lval, explicit_type, final_cb=final_cb) @@ -2098,7 +2105,7 @@ def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False, node = self.type.get(lval.name) if cur_node and final_cb is not None: # Overrides will be checked in type checker. - final_cb() + final_cb(False) # If the attribute of self is not defined in superclasses, create a new Var, ... if ((node is None or isinstance(node.node, Var) and node.node.is_abstract_var) or # ... also an explicit declaration on self also creates a new Var. diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 789af524ca4f..bda69d54555b 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -228,7 +228,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType) -> Type: return NoneTyp() elif fullname == 'typing.Any' or fullname == 'builtins.Any': return AnyType(TypeOfAny.explicit) - elif fullname == 'typing.Final': + elif fullname in ('typing.Final', 'typing_extensions.Final'): self.fail("Final can be only used as an outermost type qualifier" " in assignments", t) return AnyType(TypeOfAny.from_error) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 075dd438c1db..80df5bb249f4 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -180,6 +180,13 @@ def f(x: Final[int]) -> int: ... # E: Final can be only used as an outermost ty def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost type qualifier in assignments [out] +[case testFinalDefiningNotInMethodExtensions] +from typing_extensions import Final + +def f(x: Final[int]) -> int: ... # E: Final can be only used as an outermost type qualifier in assignments +def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost type qualifier in assignments +[out] + [case testFinalDefiningNoRhs] from typing import Final x: Final # E: Final name must be initialized with a value @@ -309,13 +316,12 @@ def f() -> int: y = 1 y: Final = 2 # E: Name 'y' already defined on line 10 \ - # E: Final can't redefine an existing name + # E: Cannot redefine an existing name as final y = 3 # No error here, first definition wins z: Final = 1 z: Final = 2 # E: Name 'z' already defined on line 14 \ - # E: Final can't redefine an existing name \ - # E: Can't assign to constant "z" + # E: Cannot redefine an existing name as final z = 3 # E: Can't assign to constant "z" [out] @@ -327,8 +333,7 @@ from lib.mod import ID X = 1 # E: Can't assign to constant "X" ID: Final = 1 # E: Name 'ID' already defined (possibly by an import) \ - # E: Final can't redefine an existing name \ - # E: Can't assign to constant "ID" + # E: Cannot redefine an existing name as final ID = 1 # E: Can't assign to constant "ID" [file lib/__init__.pyi] from lib.const import X as X @@ -352,8 +357,7 @@ def f() -> None: x = 1 # E: Can't assign to constant "x" y: Final = 1 - y: Final = 2 # E: Final can't redefine an existing name \ - # E: Can't assign to constant "y" + y: Final = 2 # E: Cannot redefine an existing name as final def nested() -> None: nonlocal nl nl = 1 # E: Can't assign to constant "nl" @@ -375,7 +379,7 @@ class C: x = 2 # E: Can't assign to constant "x" y = 1 - y: Final = 2 # E: Final can't redefine an existing name + y: Final = 2 # E: Cannot redefine an existing name as final [out] [case testFinalReassignInstanceVarInit] @@ -385,7 +389,7 @@ class C: def __init__(self) -> None: self.x: Final = 1 self.y = 1 - self.y: Final = 2 # E: Final can't redefine an existing name + self.y: Final = 2 # E: Cannot redefine an existing name as final def meth(self) -> None: self.x = 2 # E: Can't assign to final attribute "x" [out] @@ -682,6 +686,29 @@ class B(A): [builtins fixtures/classmethod.pyi] [out] +[case testFinalOverridingMethodRegular] +from typing import final + +class B: + @final + def meth(self) -> None: ... +class C(B): + def meth(self) -> None: ... # E: Cannot override final attribute "meth" (previously declared in base class "B") +[out] + +[case testFinalOverridingMethodInitNew] +from typing import final + +class B: + @final + def __init__(self) -> None: ... + @final + def __new__(cls) -> B: ... +class C(B): + def __init__(self) -> None: ... # E: Cannot override final attribute "__init__" (previously declared in base class "B") + def __new__(cls) -> B: ... # E: Cannot override final attribute "__new__" (previously declared in base class "B") +[out] + [case testFinalOverridingMethodWithVar] from typing import final, Final, Any @@ -899,3 +926,24 @@ class C(B): class F: ... class E(F): ... # E: Can't inherit from final class "F" [out] + +[case testFinalCanUseTypingExtensionsAliased] +from typing_extensions import final as f, Final as F + +x: F = 1 +x = 2 # E: Can't assign to constant "x" + +class S: + x: F = 1 +S.x = 2 # E: Can't assign to final attribute "x" + +class B: + @f + def meth(self) -> None: ... +class C(B): + def meth(self) -> None: ... # E: Cannot override final attribute "meth" (previously declared in base class "B") + +@f +class D(C): ... +class E(D): ... # E: Can't inherit from final class "D" +[out] From 7a852851e9a1d30dc4a179f8a7f2d9f8949e18a3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 13:04:52 +0100 Subject: [PATCH 45/52] More CR; update tests --- docs/source/final_attrs.rst | 33 +--- mypy/checker.py | 17 +- mypy/checkmember.py | 5 +- mypy/messages.py | 10 +- mypy/semanal.py | 43 +++-- mypy/typeanal.py | 4 +- test-data/unit/check-final.test | 233 +++++++++++++++----------- test-data/unit/check-incremental.test | 18 +- test-data/unit/fine-grained.test | 22 +-- 9 files changed, 206 insertions(+), 179 deletions(-) diff --git a/docs/source/final_attrs.rst b/docs/source/final_attrs.rst index 49ef17fa4fef..100e81eb78a5 100644 --- a/docs/source/final_attrs.rst +++ b/docs/source/final_attrs.rst @@ -115,10 +115,9 @@ The are two rules that should be always followed when defining a final name: def __init__(self) -> None: self.x = 1 # Good -* ``Final`` can be only used as an outermost type in assignments, using it in - any other position is an error. In particular, ``Final`` can't be used in - annotations for function arguments because this may cause confusions about - what are the guarantees in this case: +* ``Final`` can be only used as an outermost type in assignments or variable + annotations. using it in any other position is an error. In particular, + ``Final`` can't be used in annotations for function arguments: .. code-block:: python @@ -128,11 +127,7 @@ The are two rules that should be always followed when defining a final name: * ``Final`` and ``ClassVar`` should not be used together. Mypy will infer the scope of a final declaration automatically depending on whether it was - initialized in class body or in ``__init__``. - -.. note:: - Conditional final declarations and final declarations within loops are - rejected. + initialized in the class body or in ``__init__``. Using final attributes ********************** @@ -202,18 +197,11 @@ two following guarantees: y.append('x') # Error: Sequance is immutable z: Final = ('a', 'b') # Also an option -.. note:: - - Mypy treats re-exported final names as final. In other words, once declared, - the final status can't be "stripped". Such behaviour is typically desired - for larger libraries where constants are defined in a separate module and - then re-exported. - Final methods ************* Like with attributes, sometimes it is useful to protect a method from -overriding. In such situations one can use a ``typing_extensions.final`` +overriding. In such situations one can use the ``typing_extensions.final`` decorator: .. code-block:: python @@ -228,8 +216,7 @@ decorator: # 1000 lines later class Derived(Base): - def common_name(self) -> None: # Error: this overriding might break - # invariants in the base class. + def common_name(self) -> None: # Error: cannot override a final method ... This ``@final`` decorator can be used with instance methods, class methods, @@ -249,14 +236,10 @@ it final (or on the first overload in stubs): def meth(self, x=None): ... - class Derived(Base): - def meth(self, x: Any = None) -> Any: # Error: can't override final method - ... - Final classes ************* -You can apply a ``typing_extensions.final`` decorator to a class indicates +You can apply a ``typing_extensions.final`` decorator to a class to indicate to mypy that it can't be subclassed. The decorator acts as a declaration for mypy (and as documentation for humans), but it doesn't prevent subclassing at runtime: @@ -269,8 +252,6 @@ at runtime: class Leaf: ... - from lib import Leaf - class MyLeaf(Leaf): # Error: Leaf can't be subclassed ... diff --git a/mypy/checker.py b/mypy/checker.py index 23270bf14669..7eb8ced3607c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1268,7 +1268,7 @@ def check_method_or_accessor_override_for_base(self, defn: Union[FuncBase, Decor self.msg.cant_override_final(name, base.name(), defn) # Second, final can't override anything writeable independently of types. if defn.is_final: - self.check_no_writeable(name, base_attr.node, defn) + self.check_no_writable(name, base_attr.node, defn) # Check the type of override. if name not in ('__init__', '__new__', '__init_subclass__'): @@ -1473,7 +1473,7 @@ def visit_class_def(self, defn: ClassDef) -> None: self.check_protocol_variance(defn) for base in typ.mro[1:]: if base.is_final: - self.fail('Can\'t inherit from final class "{}"'.format(base.name()), defn) + self.fail('Cannot inherit from final class "{}"'.format(base.name()), defn) with self.tscope.class_scope(defn.info), self.enter_partial_types(is_class=True): old_binder = self.binder self.binder = ConditionalTypeBinder() @@ -1590,7 +1590,7 @@ def check_compatibility(self, name: str, base1: TypeInfo, if isinstance(second.node, (Var, FuncBase, Decorator)) and second.node.is_final: self.msg.cant_override_final(name, base2.name(), ctx) if isinstance(first.node, (Var, FuncBase, Decorator)) and first.node.is_final: - self.check_no_writeable(name, second.node, ctx) + self.check_no_writable(name, second.node, ctx) # __slots__ is special and the type can vary across class hierarchy. if name == '__slots__': ok = True @@ -1660,12 +1660,13 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.expr_checker.accept(s.rvalue) rvalue = self.temp_node(self.type_map[s.rvalue], s) for lv in s.lvalues[:-1]: - self.check_assignment(lv, rvalue, s.type is None) + with self.enter_final_context(s.is_final_def): + self.check_assignment(lv, rvalue, s.type is None) self.check_final(s) if (s.is_final_def and s.type and not has_no_typevars(s.type) and self.scope.active_class() is not None): - self.fail("Constant declared in class body can't depend on type variables", s) + self.fail("Final name declared in class body cannot depend on type variables", s) def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type: bool = True, new_syntax: bool = False) -> None: @@ -1938,10 +1939,10 @@ def check_compatibility_final_super(self, node: Var, self.msg.cant_override_final(node.name(), base.name(), node) return False if node.is_final: - self.check_no_writeable(node.name(), base_node, node) + self.check_no_writable(node.name(), base_node, node) return True - def check_no_writeable(self, name: str, base_node: Optional[Node], ctx: Context) -> None: + def check_no_writable(self, name: str, base_node: Optional[Node], ctx: Context) -> None: """Check that a final variable doesn't override writeable attribute. This is done to prevent situations like this: @@ -1961,7 +1962,7 @@ class D(C): else: ok = True if not ok: - self.msg.final_cant_override_writeable(name, ctx) + self.msg.final_cant_override_writable(name, ctx) def get_final_context(self) -> bool: """Check whether we a currently checking a final declaration.""" diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 102428295ea7..a08ba75fddc7 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -549,10 +549,11 @@ def analyze_class_attribute_access(itype: Instance, if isinstance(node.node, TypeInfo): msg.fail(messages.CANNOT_ASSIGN_TO_TYPE, context) - # If a constant was declared on `self` in `__init__`, then it + # If a final attribute was declared on `self` in `__init__`, then it # can't be accessed on the class object. if node.implicit and isinstance(node.node, Var) and node.node.is_final: - msg.fail("Can't access instance constant on class object", context) + msg.fail('Cannot access final instance ' + 'attribute "{}" on class object'.format(node.node.name()), context) # An assignment to final attribute on class object is also always an error, # independently of types. diff --git a/mypy/messages.py b/mypy/messages.py index edbb99f3dcc5..6242699453ad 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -951,8 +951,8 @@ def cant_assign_to_method(self, context: Context) -> None: def cant_assign_to_classvar(self, name: str, context: Context) -> None: self.fail('Cannot assign to class variable "%s" via instance' % name, context) - def final_cant_override_writeable(self, name: str, ctx: Context) -> None: - self.fail('Can\'t override writeable attribute "{}" with a final one'.format(name), ctx) + def final_cant_override_writable(self, name: str, ctx: Context) -> None: + self.fail('Cannot override writable attribute "{}" with a final one'.format(name), ctx) def cant_override_final(self, name: str, base_name: str, ctx: Context) -> None: self.fail('Cannot override final attribute "{}"' @@ -963,11 +963,11 @@ def cant_assign_to_final(self, name: str, attr_assign: bool, ctx: Context) -> No Pass `attr_assign=True` if the assignment assigns to an attribute. """ - kind = "final attribute" if attr_assign else "constant" - self.fail('Can\'t assign to {} "{}"'.format(kind, name), ctx) + kind = "attribute" if attr_assign else "name" + self.fail('Cannot assign to final {} "{}"'.format(kind, name), ctx) def protocol_members_cant_be_final(self, ctx: Context) -> None: - self.fail("Protocol members can't be final", ctx) + self.fail("Protocol member cannot be final", ctx) def final_without_value(self, ctx: Context) -> None: self.fail("Final name must be initialized with a value", ctx) diff --git a/mypy/semanal.py b/mypy/semanal.py index dfdaa2e1c849..108e861e7e0e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -627,7 +627,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: bad_final) elif any(item.is_final for item in defn.items[1:]): bad_final = next(ov for ov in defn.items[1:] if ov.is_final) - self.fail("In stub files @final should be applied only to the first overload", + self.fail("In a stub file @final must be applied only to the first overload", bad_final) if defn.impl is not None and defn.impl.is_final: defn.is_final = True @@ -1786,7 +1786,7 @@ def check_final_implicit_def(self, s: AssignmentStmt) -> None: else: assert self.function_stack if self.function_stack[-1].name() != '__init__': - self.fail("Can only declare final attributes in class body or __init__", s) + self.fail("Can only declare a final attribute in class body or __init__", s) s.is_final_def = False return @@ -1801,23 +1801,32 @@ def store_final_status(self, s: AssignmentStmt) -> None: if (self.is_class_scope() and (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs)): node.final_unset_in_class = True - elif len(s.lvalues) == 1 and isinstance(s.lvalues[0], MemberExpr): + else: # Special case: deferred initialization of a final attribute in __init__. # In this case we just pretend this is a valid final definition to suppress # errors about assigning to final attribute. - lval = s.lvalues[0] - if self.is_self_member_ref(lval): - assert self.type, "Self member outside a class" - cur_node = self.type.names.get(lval.name, None) - if cur_node and isinstance(cur_node.node, Var) and cur_node.node.is_final: - assert self.function_stack - top_function = self.function_stack[-1] - if (top_function.name() == '__init__' and - cur_node.node.final_unset_in_class and - not cur_node.node.final_set_in_init and - not (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs)): - cur_node.node.final_set_in_init = True - s.is_final_def = True + for lval in self.flatten_lvalues(s.lvalues): + if isinstance(lval, MemberExpr) and self.is_self_member_ref(lval): + assert self.type, "Self member outside a class" + cur_node = self.type.names.get(lval.name, None) + if cur_node and isinstance(cur_node.node, Var) and cur_node.node.is_final: + assert self.function_stack + top_function = self.function_stack[-1] + if (top_function.name() == '__init__' and + cur_node.node.final_unset_in_class and + not cur_node.node.final_set_in_init and + not (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs)): + cur_node.node.final_set_in_init = True + s.is_final_def = True + + def flatten_lvalues(self, lvalues: List[Expression]) -> List[Expression]: + res = [] # type: List[Expression] + for lv in lvalues: + if isinstance(lv, (TupleExpr, ListExpr)): + res.extend(self.flatten_lvalues(lv.items)) + else: + res.append(lv) + return res def unbox_literal(self, e: Expression) -> Optional[Union[int, float, bool, str]]: if isinstance(e, (IntExpr, FloatExpr, StrExpr)): @@ -2501,7 +2510,7 @@ def visit_decorator(self, dec: Decorator) -> None: dec.var.is_final = True removed.append(i) else: - self.fail("@final can't be used with non-method functions", d) + self.fail("@final cannot be used with non-method functions", d) for i in reversed(removed): del dec.decorators[i] if not dec.is_overload or dec.var.is_property: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index bda69d54555b..39307331353d 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -229,8 +229,8 @@ def visit_unbound_type_nonoptional(self, t: UnboundType) -> Type: elif fullname == 'typing.Any' or fullname == 'builtins.Any': return AnyType(TypeOfAny.explicit) elif fullname in ('typing.Final', 'typing_extensions.Final'): - self.fail("Final can be only used as an outermost type qualifier" - " in assignments", t) + self.fail("Final can be only used as an outermost qualifier" + " in a variable annotation", t) return AnyType(TypeOfAny.from_error) elif fullname == 'typing.Tuple': if len(t.args) == 0 and not t.empty_tuple_index: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 80df5bb249f4..f35b6c8bb084 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -9,7 +9,7 @@ from typing import Final x: Final = int() y: Final[float] = int() z: Final[int] = int() -bad: Final[str] = str() +bad: Final[str] = int() # E: Incompatible types in assignment (expression has type "int", variable has type "str") reveal_type(x) # E: Revealed type is 'builtins.int' reveal_type(y) # E: Revealed type is 'builtins.float' @@ -80,12 +80,17 @@ class C: y: Final[int] def __init__(self) -> None: self.z: Final + +reveal_type(x) # E: Revealed type is 'Any' +reveal_type(C.x) # E: Revealed type is 'Any' +v: C +reveal_type(v.z) # E: Revealed type is 'Any' [out] [case testFinalDefiningFunc] from typing import final -@final # E: @final can't be used with non-method functions +@final # E: @final cannot be used with non-method functions def f(x: int) -> None: ... [out] @@ -96,7 +101,7 @@ from typing import final, overload def f(x: int) -> int: ... @overload def f(x: str) -> str: ... -@final # E: @final can't be used with non-method functions +@final # E: @final cannot be used with non-method functions def f(x): pass [out] @@ -153,7 +158,7 @@ class C: @overload def bad(self, x: str) -> str: ... [out] -tmp/mod.pyi:12: error: In stub files @final should be applied only to the first overload +tmp/mod.pyi:12: error: In a stub file @final must be applied only to the first overload main:3: error: Revealed type is 'Overload(def (x: builtins.int) -> builtins.int, def (x: builtins.str) -> builtins.str)' [case testFinalDefiningProperty] @@ -163,28 +168,32 @@ class C: @final @property def f(self) -> int: pass + @property + @final + def g(self) -> int: pass reveal_type(C().f) # E: Revealed type is 'builtins.int' +reveal_type(C().g) # E: Revealed type is 'builtins.int' [builtins fixtures/property.pyi] [out] [case testFinalDefiningOuterOnly] from typing import Final, Callable, Tuple, Any -x: Tuple[Final] # E: Final can be only used as an outermost type qualifier in assignments -y: Callable[[], Tuple[Final[int]]] # E: Final can be only used as an outermost type qualifier in assignments +x: Tuple[Final] # E: Final can be only used as an outermost qualifier in a variable annotation +y: Callable[[], Tuple[Final[int]]] # E: Final can be only used as an outermost qualifier in a variable annotation [out] [case testFinalDefiningNotInMethod] from typing import Final -def f(x: Final[int]) -> int: ... # E: Final can be only used as an outermost type qualifier in assignments -def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost type qualifier in assignments +def f(x: Final[int]) -> int: ... # E: Final can be only used as an outermost qualifier in a variable annotation +def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost qualifier in a variable annotation [out] [case testFinalDefiningNotInMethodExtensions] from typing_extensions import Final -def f(x: Final[int]) -> int: ... # E: Final can be only used as an outermost type qualifier in assignments -def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost type qualifier in assignments +def f(x: Final[int]) -> int: ... # E: Final can be only used as an outermost qualifier in a variable annotation +def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost qualifier in a variable annotation [out] [case testFinalDefiningNoRhs] @@ -210,7 +219,7 @@ T = TypeVar('T') d: Any class C(Generic[T]): - x: Final[Tuple[T, T]] = d # E: Constant declared in class body can't depend on type variables + x: Final[Tuple[T, T]] = d # E: Final name declared in class body cannot depend on type variables [out] [case testFinalDefiningTypevarsImplicit] @@ -224,9 +233,9 @@ class C(Generic[T]): self.y: Final = 1 reveal_type(C((1, 2)).x) # E: Revealed type is 'Tuple[builtins.int*, builtins.int*]' -C.x # E: Can't access instance constant on class object \ +C.x # E: Cannot access final instance attribute "x" on class object \ # E: Access to generic instance variables via class is ambiguous -C.y # E: Can't access instance constant on class object +C.y # E: Cannot access final instance attribute "y" on class object [out] [case testFinalDefiningNotInOtherMethod] @@ -234,8 +243,8 @@ from typing import Final, Any, Tuple class C: def meth(self, x: Tuple[int, Any]) -> None: - self.x: Final = x # E: Can only declare final attributes in class body or __init__ - self.y: Final[float] = 1 # E: Can only declare final attributes in class body or __init__ + self.x: Final = x # E: Can only declare a final attribute in class body or __init__ + self.y: Final[float] = 1 # E: Can only declare a final attribute in class body or __init__ [out] [case testFinalDefiningOnlyOnSelf] @@ -256,15 +265,15 @@ class C: from typing import Final, final, Protocol, overload class P(Protocol): - x: Final[float] = 1 # E: Protocol members can't be final - @final # E: Protocol members can't be final + x: Final[float] = 1 # E: Protocol member cannot be final + @final # E: Protocol member cannot be final def meth(self, x) -> int: pass @overload def other(self, x: int) -> int: ... @overload def other(self, x: str) -> str: ... - @final # E: Protocol members can't be final + @final # E: Protocol member cannot be final def other(self, x): pass [out] @@ -289,17 +298,17 @@ class C: def __init__(self, x: int) -> None: self.x = x # OK, deferred definition - self.x = 2 # E: Can't assign to final attribute "x" + self.x = 2 # E: Cannot assign to final attribute "x" def meth(self) -> None: - self.x = 2 # E: Can't assign to final attribute "x" + self.x = 2 # E: Cannot assign to final attribute "x" c: C -c.x = 3 # E: Can't assign to final attribute "x" +c.x = 3 # E: Cannot assign to final attribute "x" class D(C): - x = 4 # E: Can't assign to constant "x" + x = 4 # E: Cannot assign to final name "x" d: D -d.x = 5 # E: Can't assign to final attribute "x" +d.x = 5 # E: Cannot assign to final attribute "x" [out] -- Reassignments @@ -308,10 +317,10 @@ d.x = 5 # E: Can't assign to final attribute "x" from typing import Final x: Final = 1 -x = 2 # E: Can't assign to constant "x" +x = 2 # E: Cannot assign to final name "x" def f() -> int: global x - x = 3 # E: Can't assign to constant "x" + x = 3 # E: Cannot assign to final name "x" return x y = 1 @@ -322,7 +331,7 @@ y = 3 # No error here, first definition wins z: Final = 1 z: Final = 2 # E: Name 'z' already defined on line 14 \ # E: Cannot redefine an existing name as final -z = 3 # E: Can't assign to constant "z" +z = 3 # E: Cannot assign to final name "z" [out] [case testFinalReassignModuleReexport] @@ -331,10 +340,10 @@ from typing import Final from lib import X from lib.mod import ID -X = 1 # E: Can't assign to constant "X" +X = 1 # E: Cannot assign to final name "X" ID: Final = 1 # E: Name 'ID' already defined (possibly by an import) \ # E: Cannot redefine an existing name as final -ID = 1 # E: Can't assign to constant "ID" +ID = 1 # E: Cannot assign to final name "ID" [file lib/__init__.pyi] from lib.const import X as X @@ -354,18 +363,18 @@ from typing import Final def f() -> None: nl: Final = 0 x: Final = 1 - x = 1 # E: Can't assign to constant "x" + x = 1 # E: Cannot assign to final name "x" y: Final = 1 y: Final = 2 # E: Cannot redefine an existing name as final def nested() -> None: nonlocal nl - nl = 1 # E: Can't assign to constant "nl" + nl = 1 # E: Cannot assign to final name "nl" [out] [case testFinalReassignModuleVarExternal] import mod -mod.x = 2 # E: Can't assign to constant "x" +mod.x = 2 # E: Cannot assign to final name "x" [file mod.pyi] from typing import Final x: Final[int] @@ -376,7 +385,7 @@ from typing import Final class C: x: Final = 1 - x = 2 # E: Can't assign to constant "x" + x = 2 # E: Cannot assign to final name "x" y = 1 y: Final = 2 # E: Cannot redefine an existing name as final @@ -391,7 +400,7 @@ class C: self.y = 1 self.y: Final = 2 # E: Cannot redefine an existing name as final def meth(self) -> None: - self.x = 2 # E: Can't assign to final attribute "x" + self.x = 2 # E: Cannot assign to final attribute "x" [out] [case testFinalReassignInstanceVarClassVsInit] @@ -401,8 +410,8 @@ class C: y: Final = 1 def __init__(self) -> None: self.x: Final = 1 - self.y = 2 # E: Can't assign to final attribute "y" - x = 2 # E: Can't assign to constant "x" + self.y = 2 # E: Cannot assign to final attribute "y" + x = 2 # E: Cannot assign to final name "x" [out] [case testFinalReassignInstanceVarMethod] @@ -413,15 +422,15 @@ class C: def __init__(self) -> None: self.y: Final = 1 def meth(self) -> None: - self.x = 2 # E: Can't assign to final attribute "x" - self.y = 2 # E: Can't assign to final attribute "y" + self.x = 2 # E: Cannot assign to final attribute "x" + self.y = 2 # E: Cannot assign to final attribute "y" def other(self) -> None: - self.x = 2 # E: Can't assign to final attribute "x" - self.y = 2 # E: Can't assign to final attribute "y" + self.x = 2 # E: Cannot assign to final attribute "x" + self.y = 2 # E: Cannot assign to final attribute "y" @classmethod def cm(cls) -> None: - cls.x = 2 # E: Can't assign to final attribute "x" - cls.y # E: Can't access instance constant on class object + cls.x = 2 # E: Cannot assign to final attribute "x" + cls.y # E: Cannot access final instance attribute "y" on class object [builtins fixtures/classmethod.pyi] [out] @@ -435,10 +444,10 @@ class C: class D(C): pass -C.x = 2 # E: Can't assign to final attribute "x" -D.x = 2 # E: Can't assign to final attribute "x" -D.y = 2 # E: Can't access instance constant on class object \ - # E: Can't assign to final attribute "y" +C.x = 2 # E: Cannot assign to final attribute "x" +D.x = 2 # E: Cannot assign to final attribute "x" +D.y = 2 # E: Cannot access final instance attribute "y" on class object \ + # E: Cannot assign to final attribute "y" [out] [case testFinalReassignInstanceVarExternalInstance] @@ -451,17 +460,17 @@ class C: class D(C): pass -C().x = 2 # E: Can't assign to final attribute "x" -D().x = 2 # E: Can't assign to final attribute "x" -D().y = 2 # E: Can't assign to final attribute "y" +C().x = 2 # E: Cannot assign to final attribute "x" +D().x = 2 # E: Cannot assign to final attribute "x" +D().y = 2 # E: Cannot assign to final attribute "y" [out] [case testFinalWorksWithComplexTargets] from typing import Final, Any y: Final[Any] = 1 -x = a, (b, y), c = 2, (2, 2), 2 # E: Can't assign to constant "y" -t, *y, s = u = [2, 2, 2] # E: Can't assign to constant "y" +x = a, (b, y), c = 2, (2, 2), 2 # E: Cannot assign to final name "y" +t, *y, s = u = [2, 2, 2] # E: Cannot assign to final name "y" [builtins fixtures/list.pyi] [out] @@ -482,10 +491,10 @@ class C: class D(C): pass -a += A() # E: Can't assign to constant "a" -b += B() # E: Can't assign to constant "b" -D().a += A() # E: Can't assign to final attribute "a" -D().b += B() # E: Can't assign to final attribute "b" +a += A() # E: Cannot assign to final name "a" +b += B() # E: Cannot assign to final name "b" +D().a += A() # E: Cannot assign to final attribute "a" +D().b += B() # E: Cannot assign to final attribute "b" [out] -- Overriding @@ -495,7 +504,7 @@ from typing import Final # We use properties in this tests and below because we want to check # that any existing variable before final doesn't affect logic of -# subsequent overrides but writable attributes can't be overridden by final. +# subsequent overrides but writable attributes cannot be overridden by final. class A: @property def x(self) -> int: ... @@ -507,14 +516,14 @@ class B(A): def __init__(self) -> None: self.y: Final = 1 class C(B): - x: int = 2 # E: Can't assign to constant "x" - y: int = 2 # E: Can't assign to constant "y" - x = 3 # E: Can't assign to constant "x" - y = 3 # E: Can't assign to constant "y" + x: int = 2 # E: Cannot assign to final name "x" + y: int = 2 # E: Cannot assign to final name "y" + x = 3 # E: Cannot assign to final name "x" + y = 3 # E: Cannot assign to final name "y" class D(C): pass -D.x = 4 # E: Can't assign to final attribute "x" -D.y = 4 # E: Can't assign to final attribute "y" +D.x = 4 # E: Cannot assign to final attribute "x" +D.y = 4 # E: Cannot assign to final attribute "y" [builtins fixtures/property.pyi] [out] @@ -550,11 +559,11 @@ class B(A): self.y: Final = 1 class C(B): def __init__(self) -> None: - self.x = 2 # E: Can't assign to final attribute "x" - self.y = 2 # E: Can't assign to final attribute "y" + self.x = 2 # E: Cannot assign to final attribute "x" + self.y = 2 # E: Cannot assign to final attribute "y" def meth(self) -> None: - self.x = 3 # E: Can't assign to final attribute "x" - self.y = 3 # E: Can't assign to final attribute "y" + self.x = 3 # E: Cannot assign to final attribute "x" + self.y = 3 # E: Cannot assign to final attribute "y" [builtins fixtures/property.pyi] [out] @@ -591,11 +600,11 @@ class B(A): self.y: Final = 1 class C(B): def meth(self) -> None: - self.x: int = 2 # E: Can't assign to final attribute "x" - self.y: int = 2 # E: Can't assign to final attribute "y" + self.x: int = 2 # E: Cannot assign to final attribute "x" + self.y: int = 2 # E: Cannot assign to final attribute "y" - self.x = 3 # E: Can't assign to final attribute "x" - self.y = 3 # E: Can't assign to final attribute "y" + self.x = 3 # E: Cannot assign to final attribute "x" + self.y = 3 # E: Cannot assign to final attribute "y" [builtins fixtures/property.pyi] [out] @@ -609,9 +618,9 @@ class B: def x(self) -> int: ... class C(A, B): ... class D(B, A): ... # E: Cannot override final attribute "x" (previously declared in base class "A") -C.x = 3 # E: Can't assign to final attribute "x" -C().x = 4 # E: Can't assign to final attribute "x" -D().x = 4 # E: Can't assign to final attribute "x" \ +C.x = 3 # E: Cannot assign to final attribute "x" +C().x = 4 # E: Cannot assign to final attribute "x" +D().x = 4 # E: Cannot assign to final attribute "x" \ # E: Property "x" defined in "B" is read-only [builtins fixtures/property.pyi] [out] @@ -627,9 +636,9 @@ class B: def x(self) -> int: ... class C(A, B): ... class D(B, A): ... # E: Cannot override final attribute "x" (previously declared in base class "A") -C.x = 3 # E: Can't access instance constant on class object \ - # E: Can't assign to final attribute "x" -C().x = 4 # E: Can't assign to final attribute "x" +C.x = 3 # E: Cannot access final instance attribute "x" on class object \ + # E: Cannot assign to final attribute "x" +C().x = 4 # E: Cannot assign to final attribute "x" [builtins fixtures/property.pyi] [out] @@ -641,12 +650,12 @@ class A: class B: def __init__(self) -> None: self.x = 2 -class C(A, B): ... # E: Can't override writeable attribute "x" with a final one +class C(A, B): ... # E: Cannot override writable attribute "x" with a final one class D(B, A): ... # E: Cannot override final attribute "x" (previously declared in base class "A") -C.x = 3 # E: Can't assign to final attribute "x" -D.x = 3 # E: Can't assign to final attribute "x" -C().x = 4 # E: Can't assign to final attribute "x" -D().x = 4 # E: Can't assign to final attribute "x" +C.x = 3 # E: Cannot assign to final attribute "x" +D.x = 3 # E: Cannot assign to final attribute "x" +C().x = 4 # E: Cannot assign to final attribute "x" +D().x = 4 # E: Cannot assign to final attribute "x" [out] [case testFinalOverridingVarWithMethod] @@ -747,13 +756,13 @@ class A: class B(A): def __init__(self) -> None: - self.f: Any # E: Can't assign to final attribute "f" \ + self.f: Any # E: Cannot assign to final attribute "f" \ # E: Cannot override final attribute "f" (previously declared in base class "A") - self.c: Any # E: Can't assign to final attribute "c" \ + self.c: Any # E: Cannot assign to final attribute "c" \ # E: Cannot override final attribute "c" (previously declared in base class "A") -B().f = a # E: Can't assign to final attribute "f" -B().c = a # E: Can't assign to final attribute "c" +B().f = a # E: Cannot assign to final attribute "f" +B().c = a # E: Cannot assign to final attribute "c" class C(A): def __init__(self) -> None: @@ -795,7 +804,7 @@ class B: def m(self) -> int: pass class C(A, B): pass # E: Cannot override final attribute "m" (previously declared in base class "B") -class D(B, A): pass # E: Can't override writeable attribute "m" with a final one +class D(B, A): pass # E: Cannot override writable attribute "m" with a final one [out] [case testFinalOverridingClassMethod] @@ -819,10 +828,15 @@ class B: @staticmethod @final def f() -> int: pass + @final + @staticmethod + def g() -> int: pass class C(B): @staticmethod # E: Cannot override final attribute "f" (previously declared in base class "B") def f() -> int: pass + @staticmethod # E: Cannot override final attribute "g" (previously declared in base class "B") + def g() -> int: pass [builtins fixtures/staticmethod.pyi] [out] @@ -833,10 +847,15 @@ class B: @final @property def f(self) -> int: pass + @property + @final + def g(self) -> int: pass class C(B): @property # E: Cannot override final attribute "f" (previously declared in base class "B") def f(self) -> int: pass + @property # E: Cannot override final attribute "g" (previously declared in base class "B") + def g(self) -> int: pass [builtins fixtures/property.pyi] [out] @@ -866,9 +885,9 @@ from typing import final @final class B: ... -class C(B): # E: Can't inherit from final class "B" +class C(B): # E: Cannot inherit from final class "B" pass -class D(C): # E: Can't inherit from final class "B" +class D(C): # E: Cannot inherit from final class "B" pass [out] @@ -878,9 +897,9 @@ from typing import final class A: ... @final class B: ... -class C(B, A): # E: Can't inherit from final class "B" +class C(B, A): # E: Cannot inherit from final class "B" pass -class D(A, B): # E: Can't inherit from final class "B" +class D(A, B): # E: Cannot inherit from final class "B" pass [out] @@ -895,13 +914,13 @@ class B: def y(self, x: Any) -> None: ... class C(B): - x: Final = 1 # E: Can't override writeable attribute "x" with a final one - y: Final = 1 # E: Can't override writeable attribute "y" with a final one + x: Final = 1 # E: Cannot override writable attribute "x" with a final one + y: Final = 1 # E: Cannot override writable attribute "y" with a final one class D(B): - @final # E: Can't override writeable attribute "x" with a final one + @final # E: Cannot override writable attribute "x" with a final one def x(self) -> int: ... - @final # E: Can't override writeable attribute "y" with a final one + @final # E: Cannot override writable attribute "y" with a final one def y(self) -> int: ... [builtins fixtures/property.pyi] [out] @@ -910,11 +929,11 @@ class D(B): from typing_extensions import final, Final x: Final = 1 -x = 2 # E: Can't assign to constant "x" +x = 2 # E: Cannot assign to final name "x" class S: x: Final = 1 -S.x = 2 # E: Can't assign to final attribute "x" +S.x = 2 # E: Cannot assign to final attribute "x" class B: @final @@ -924,18 +943,18 @@ class C(B): @final class F: ... -class E(F): ... # E: Can't inherit from final class "F" +class E(F): ... # E: Cannot inherit from final class "F" [out] [case testFinalCanUseTypingExtensionsAliased] from typing_extensions import final as f, Final as F x: F = 1 -x = 2 # E: Can't assign to constant "x" +x = 2 # E: Cannot assign to final name "x" class S: x: F = 1 -S.x = 2 # E: Can't assign to final attribute "x" +S.x = 2 # E: Cannot assign to final attribute "x" class B: @f @@ -945,5 +964,21 @@ class C(B): @f class D(C): ... -class E(D): ... # E: Can't inherit from final class "D" +class E(D): ... # E: Cannot inherit from final class "D" +[out] + +[case testFinalMultiassignAllowed] +from typing import Final + +class A: + x: Final[int] + y: Final[int] + def __init__(self) -> None: + self.x, self.y = 1, 2 + +class B: + x: Final[int] + y: Final[int] + def __init__(self) -> None: + self.x = self.y = 1 [out] diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 564b14fe4ec6..a902a25d5ed3 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4922,11 +4922,11 @@ class C: self.z: Final = 1 [out] [out2] -main:5: error: Can't assign to constant "x" -main:6: error: Can't assign to constant "x" -main:8: error: Can't assign to final attribute "y" -main:9: error: Can't assign to final attribute "z" -main:10: error: Can't assign to final attribute "y" +main:5: error: Cannot assign to final name "x" +main:6: error: Cannot assign to final name "x" +main:8: error: Cannot assign to final attribute "y" +main:9: error: Cannot assign to final attribute "z" +main:10: error: Cannot assign to final attribute "y" [case testFinalAddFinalVarOverride] from mod import C @@ -4955,10 +4955,10 @@ class C: self.y: Final = 1 [out] [out2] -main:4: error: Can't assign to constant "x" -main:6: error: Can't assign to final attribute "y" -main:8: error: Can't assign to constant "y" -main:10: error: Can't assign to final attribute "x" +main:4: error: Cannot assign to final name "x" +main:6: error: Cannot assign to final attribute "y" +main:8: error: Cannot assign to final name "y" +main:10: error: Cannot assign to final attribute "x" [case testFinalAddFinalMethodOverride] from mod import C diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index ae02abc0e4bd..ba64993fedde 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7568,11 +7568,11 @@ class C: self.z: Final = 1 [out] == -main:5: error: Can't assign to constant "x" -main:7: error: Can't assign to constant "x" -main:10: error: Can't assign to final attribute "y" -main:11: error: Can't assign to final attribute "z" -main:12: error: Can't assign to final attribute "y" +main:5: error: Cannot assign to final name "x" +main:7: error: Cannot assign to final name "x" +main:10: error: Cannot assign to final attribute "y" +main:11: error: Cannot assign to final attribute "z" +main:12: error: Cannot assign to final attribute "y" [case testFinalAddFinalVarOverrideFine] from mod import C @@ -7601,10 +7601,10 @@ class C: self.y: Final = 1 [out] == -main:4: error: Can't assign to constant "x" -main:6: error: Can't assign to final attribute "y" -main:8: error: Can't assign to constant "y" -main:10: error: Can't assign to final attribute "x" +main:4: error: Cannot assign to final name "x" +main:6: error: Cannot assign to final attribute "y" +main:8: error: Cannot assign to final name "y" +main:10: error: Cannot assign to final attribute "x" [case testFinalAddFinalMethodOverrideFine] from mod import C @@ -7651,7 +7651,7 @@ class C: [out] == main:5: error: Cannot override final attribute "meth" (previously declared in base class "C") -main:7: error: Can't assign to final attribute "other" +main:7: error: Cannot assign to final attribute "other" main:7: error: Cannot override final attribute "other" (previously declared in base class "C") [case testFinalAddFinalMethodOverrideOverloadFine] @@ -7716,7 +7716,7 @@ class C: [out] == main:5: error: Cannot override final attribute "p" (previously declared in base class "C") -main:8: error: Can't assign to final attribute "p" +main:8: error: Cannot assign to final attribute "p" main:8: error: Cannot override final attribute "p" (previously declared in base class "C") [case testIfMypyUnreachableClass] From b37f35ffe4895b1a38ad5cd05b6bd0884bd5df42 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 13:20:06 +0100 Subject: [PATCH 46/52] Prohibit bare final without r.h.s. --- mypy/semanal.py | 2 ++ test-data/unit/check-final.test | 34 ++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 108e861e7e0e..4681f071a8f6 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1752,6 +1752,8 @@ def unwrap_final(self, s: AssignmentStmt) -> None: self.fail("Final[...] takes at most one type argument", s.type) if not s.type.args: s.type = None + if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs: + self.fail("Type in Final[...] can only be omitted if there is an initializer", s) else: s.type = s.type.args[0] if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], RefExpr): diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index f35b6c8bb084..4ffe8fb682f1 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -63,8 +63,8 @@ from typing import Final, Any x = y = 1 # type: Final[float] # E: Invalid final declaration z: Any -z[0]: Final # E: Unexpected type declaration \ - # E: Invalid final declaration +z[0]: Final[int] # E: Unexpected type declaration \ + # E: Invalid final declaration [out] [case testFinalDefiningInstanceVarStubs] @@ -73,13 +73,13 @@ import mod [file mod.pyi] from typing import Final -x: Final +x: Final # E: Type in Final[...] can only be omitted if there is an initializer y: Final[int] class C: - x: Final + x: Final # E: Type in Final[...] can only be omitted if there is an initializer y: Final[int] def __init__(self) -> None: - self.z: Final + self.z: Final # E: Type in Final[...] can only be omitted if there is an initializer reveal_type(x) # E: Revealed type is 'Any' reveal_type(C.x) # E: Revealed type is 'Any' @@ -198,13 +198,16 @@ def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost qu [case testFinalDefiningNoRhs] from typing import Final -x: Final # E: Final name must be initialized with a value +x: Final # E: Type in Final[...] can only be omitted if there is an initializer \ + # E: Final name must be initialized with a value y: Final[int] # E: Final name must be initialized with a value class C: - x: Final # E: Final name must be initialized with a value + x: Final # E: Type in Final[...] can only be omitted if there is an initializer \ + # E: Final name must be initialized with a value y: Final[int] # E: Final name must be initialized with a value def __init__(self) -> None: - self.z: Final # E: Final name must be initialized with a value + self.z: Final # E: Type in Final[...] can only be omitted if there is an initializer \ + # E: Final name must be initialized with a value reveal_type(x) # E: Revealed type is 'Any' reveal_type(y) # E: Revealed type is 'builtins.int' reveal_type(C().x) # E: Revealed type is 'Any' @@ -340,10 +343,9 @@ from typing import Final from lib import X from lib.mod import ID -X = 1 # E: Cannot assign to final name "X" -ID: Final = 1 # E: Name 'ID' already defined (possibly by an import) \ - # E: Cannot redefine an existing name as final -ID = 1 # E: Cannot assign to final name "ID" +X = 1 # Error! +ID: Final = 1 # Two errors! +ID = 1 # Error! [file lib/__init__.pyi] from lib.const import X as X @@ -353,9 +355,15 @@ from lib.const import * [file lib/const.pyi] from typing import Final -ID: Final +ID: Final # Error! X: Final[int] [out] +tmp/lib/const.pyi:3: error: Type in Final[...] can only be omitted if there is an initializer +main:6: error: Cannot assign to final name "X" +main:7: error: Name 'ID' already defined (possibly by an import) +main:7: error: Cannot redefine an existing name as final +main:8: error: Cannot assign to final name "ID" + [case testFinalReassignFuncScope] from typing import Final From 66fdbc835c3890201fa21281c24a580c841a1fdc Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 13:34:11 +0100 Subject: [PATCH 47/52] Minor fixes --- docs/source/final_attrs.rst | 21 +++++++++++---------- mypy/semanal.py | 6 +++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/source/final_attrs.rst b/docs/source/final_attrs.rst index 100e81eb78a5..ef4c2fc3813a 100644 --- a/docs/source/final_attrs.rst +++ b/docs/source/final_attrs.rst @@ -43,11 +43,11 @@ neither of the above use cases can be solved with it. .. note:: This is an experimental feature. Some details might change in later - versions of mypy. The final qualifiers are available in ``typing_extensions`` - module. + versions of mypy. The final qualifiers are available in the + ``typing_extensions`` package available on PyPI. -Definition syntax -***************** +Syntax variants +*************** The ``typing_extensions.Final`` qualifier indicates that a given name or attribute should never be re-assigned, re-defined, nor overridden. It can be @@ -64,10 +64,10 @@ used in one of these forms: classes this is *not* the same as ``Final[Any]``. Here mypy will infer type ``int``. -* In stub files one can omit the right hand side and just write +* In stub files you can omit the right hand side and just write ``ID: Final[float]``. -* Finally, one can define ``self.id: Final = 1`` (also with a type argument), +* Finally, you can define ``self.id: Final = 1`` (also with a type argument), but this is allowed *only* in ``__init__`` methods. Definition rules @@ -104,14 +104,15 @@ The are two rules that should be always followed when defining a final name: ID = 1 ID: Final = 2 # Error! -* A final attribute declared in class body without r.h.s. must be initialized - in the ``__init__`` method (one can skip initializer in stub files): +* A final attribute declared in class body without an initializer must + be initialized in the ``__init__`` method (you can skip the initializer + in stub files): .. code-block:: python class SomeCls: - x: Final - y: Final # Error: final attribute without an initializer + x: Final[int] + y: Final[int] # Error: final attribute without an initializer def __init__(self) -> None: self.x = 1 # Good diff --git a/mypy/semanal.py b/mypy/semanal.py index 9adadd1cbd38..2d4a3d7efb62 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2049,8 +2049,8 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False, if final_cb is not None: # We avoid extra errors if the original definition is also final # by keeping the final status of this assignment. - keep_final = (original_def and isinstance(original_def.node, Var) and - original_def.node.is_final) + keep_final = bool(original_def and isinstance(original_def.node, Var) and + original_def.node.is_final) final_cb(keep_final) if explicit_type: # Don't re-bind types @@ -2100,7 +2100,7 @@ def analyze_tuple_or_list_lvalue(self, lval: TupleExpr, explicit_type=explicit_type) def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False, - final_cb: Optional[Callable[[], None]] = None) -> None: + final_cb: Optional[Callable[[bool], None]] = None) -> None: """Analyze lvalue that is a member expression. Arguments: From a76da46e15aa404f29574e36380b12b762ad9bc8 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 13:39:06 +0100 Subject: [PATCH 48/52] Kill the doc part --- docs/source/final_attrs.rst | 267 ------------------------------------ docs/source/index.rst | 1 - 2 files changed, 268 deletions(-) delete mode 100644 docs/source/final_attrs.rst diff --git a/docs/source/final_attrs.rst b/docs/source/final_attrs.rst deleted file mode 100644 index ef4c2fc3813a..000000000000 --- a/docs/source/final_attrs.rst +++ /dev/null @@ -1,267 +0,0 @@ -Final names, methods and classes -================================ - -You can declare a variable or attribute as final, which means that the variable -must not be assigned a new value after initialization. This is often useful for -module and class level constants as a way to prevent unintended modification. -Mypy will prevent further assignments to final names in type-checked code: - -.. code-block:: python - - from typing_extensions import Final - - RATE: Final = 3000 - class Base: - DEFAULT_ID: Final = 0 - - # 1000 lines later - - RATE = 300 # Error: can't assign to final attribute - Base.DEFAULT_ID = 1 # Error: can't override a final attribute - -Another use case for final attributes is where a user wants to protect certain -instance attributes from overriding in a subclass: - -.. code-block:: python - - import uuid - from typing_extensions import Final - - class Snowflake: - """An absolutely unique object in the database""" - def __init__(self) -> None: - self.id: Final = uuid.uuid4() - - # 1000 lines later - - class User(Snowflake): - id = uuid.uuid4() # Error: can't override a final attribute - -Some other use cases might be solved by using ``@property``, but note that -neither of the above use cases can be solved with it. - -.. note:: - - This is an experimental feature. Some details might change in later - versions of mypy. The final qualifiers are available in the - ``typing_extensions`` package available on PyPI. - -Syntax variants -*************** - -The ``typing_extensions.Final`` qualifier indicates that a given name or -attribute should never be re-assigned, re-defined, nor overridden. It can be -used in one of these forms: - - -* You can provide an explicit type using the syntax ``Final[]``. Example: - - .. code-block:: python - - ID: Final[float] = 1 - -* You can omit the type: ``ID: Final = 1``. Note that unlike for generic - classes this is *not* the same as ``Final[Any]``. Here mypy will infer - type ``int``. - -* In stub files you can omit the right hand side and just write - ``ID: Final[float]``. - -* Finally, you can define ``self.id: Final = 1`` (also with a type argument), - but this is allowed *only* in ``__init__`` methods. - -Definition rules -**************** - -The are two rules that should be always followed when defining a final name: - -* There can be *at most one* final declaration per module or class for - a given attribute: - - .. code-block:: python - - from typing_extensions import Final - - ID: Final = 1 - ID: Final = 2 # Error: "ID" already declared as final - - class SomeCls: - id: Final = 1 - def __init__(self, x: int) -> None: - self.id: Final = x # Error: "id" already declared in class body - - Note that mypy has a single namespace for a class. So there can't be two - class-level and instance-level constants with the same name. - -* There must be *exactly one* assignment to a final attribute: - - .. code-block:: python - - ID = 1 - ID: Final = 2 # Error! - - class SomeCls: - ID = 1 - ID: Final = 2 # Error! - -* A final attribute declared in class body without an initializer must - be initialized in the ``__init__`` method (you can skip the initializer - in stub files): - - .. code-block:: python - - class SomeCls: - x: Final[int] - y: Final[int] # Error: final attribute without an initializer - def __init__(self) -> None: - self.x = 1 # Good - -* ``Final`` can be only used as an outermost type in assignments or variable - annotations. using it in any other position is an error. In particular, - ``Final`` can't be used in annotations for function arguments: - - .. code-block:: python - - x: List[Final[int]] = [] # Error! - def fun(x: Final[List[int]]) -> None: # Error! - ... - -* ``Final`` and ``ClassVar`` should not be used together. Mypy will infer - the scope of a final declaration automatically depending on whether it was - initialized in the class body or in ``__init__``. - -Using final attributes -********************** - -As a result of a final declaration mypy strives to provide the -two following guarantees: - -* A final attribute can't be re-assigned (or otherwise re-defined), both - internally and externally: - - .. code-block:: python - - # file mod.py - from typing_extensions import Final - - ID: Final = 1 - - class SomeCls: - ID: Final = 1 - - def meth(self) -> None: - self.ID = 2 # Error: can't assign to final attribute - - # file main.py - import mod - mod.ID = 2 # Error: can't assign to constant. - - from mod import ID - ID = 2 # Also an error, see note below. - - class DerivedCls(mod.SomeCls): - ... - - DerivedCls.ID = 2 # Error! - obj: DerivedCls - obj.ID = 2 # Error! - -* A final attribute can't be overridden by a subclass (even with another - explicit final declaration). Note however, that final attributes can - override read-only properties. This also applies to multiple inheritance: - - .. code-block:: python - - class Base: - @property - def ID(self) -> int: ... - - class One(Base): - ID: Final = 1 # OK - - class Other(Base): - ID: Final = 2 # OK - - class Combo(One, Other): # Error: cannot override final attribute. - pass - -* Declaring a name as final only guarantees that the name wll not be re-bound - to other value, it doesn't make the value immutable. One can use immutable ABCs - and containers to prevent mutating such values: - - .. code-block:: python - - x: Final = ['a', 'b'] - x.append('c') # OK - - y: Final[Sequance[str]] = ['a', 'b'] - y.append('x') # Error: Sequance is immutable - z: Final = ('a', 'b') # Also an option - -Final methods -************* - -Like with attributes, sometimes it is useful to protect a method from -overriding. In such situations one can use the ``typing_extensions.final`` -decorator: - -.. code-block:: python - - from typing_extensions import final - - class Base: - @final - def common_name(self) -> None: - ... - - # 1000 lines later - - class Derived(Base): - def common_name(self) -> None: # Error: cannot override a final method - ... - -This ``@final`` decorator can be used with instance methods, class methods, -static methods, and properties (this includes overloaded methods). For -overloaded methods one should add ``@final`` on the implementation to make -it final (or on the first overload in stubs): - -.. code-block:: python - from typing import Any, overload - - class Base: - @overload - def meth(self) -> None: ... - @overload - def meth(self, arg: int) -> int: ... - @final - def meth(self, x=None): - ... - -Final classes -************* - -You can apply a ``typing_extensions.final`` decorator to a class to indicate -to mypy that it can't be subclassed. The decorator acts as a declaration -for mypy (and as documentation for humans), but it doesn't prevent subclassing -at runtime: - -.. code-block:: python - - from typing_extensions import final - - @final - class Leaf: - ... - - class MyLeaf(Leaf): # Error: Leaf can't be subclassed - ... - -Here are some situations where using a final class may be useful: - -* A class wasn't designed to be subclassed. Perhaps subclassing does not - work as expected, or it's error-prone. -* You want to retain the freedom to arbitrarily change the class implementation - in the future, and these changes might break subclasses. -* You believe that subclassing would make code harder to understand or maintain. - For example, you may want to prevent unnecessarily tight coupling between - base classes and subclasses. diff --git a/docs/source/index.rst b/docs/source/index.rst index 173b197ac0f9..9c7e043f2822 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,7 +39,6 @@ Mypy is a static type checker for Python 3 and Python 2.7. stubs generics more_types - final_attrs metaclasses .. toctree:: From aefd17c6a7037289cd03dbfa5bcca55151bd858b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 15:57:44 +0100 Subject: [PATCH 49/52] Add test case for delayed definition in a subclass --- test-data/unit/check-final.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 4ffe8fb682f1..c87749118c80 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -215,6 +215,18 @@ reveal_type(C().y) # E: Revealed type is 'builtins.int' reveal_type(C().z) # E: Revealed type is 'Any' [out] +[case testFinalDefiningNoRhsSubclass] +from typing import Final + +class A: + x: Final[int] # E: Final name must be initialized with a value + +class B(A): + x = 1 # E: Cannot assign to final name "x" + def __init__(self) -> None: + self.x = 1 # E: Cannot assign to final attribute "x" +[out] + [case testFinalDefiningNoTypevarsExplicit] from typing import Final, TypeVar, Generic, Tuple, Any From 309e1a5fda4b888bc483b4ce4993ce3315796235 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 16:14:50 +0100 Subject: [PATCH 50/52] Show less errors; add a test --- mypy/checker.py | 7 +++++-- mypy/semanal.py | 5 ++++- test-data/unit/check-final.test | 19 +++++++++++++------ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7eb8ced3607c..467b9dd13137 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1994,8 +1994,11 @@ def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: lv = lvs[0] assert isinstance(lv, RefExpr) assert isinstance(lv.node, Var) - if (lv.node.final_unset_in_class and not - lv.node.final_set_in_init and not self.is_stub): + if (lv.node.final_unset_in_class and not lv.node.final_set_in_init and + not self.is_stub and # It is OK to skip initializer in stub files. + # Avoid extra error messages, if there is no type in Final[...], + # then we already reported the error about missing r.h.s. + s.type is not None): self.msg.final_without_value(s) for lv in lvs: if isinstance(lv, RefExpr) and isinstance(lv.node, Var): diff --git a/mypy/semanal.py b/mypy/semanal.py index 2d4a3d7efb62..212bfe698c67 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1750,9 +1750,11 @@ def unwrap_final(self, s: AssignmentStmt) -> None: assert isinstance(s.type, UnboundType) if len(s.type.args) > 1: self.fail("Final[...] takes at most one type argument", s.type) + invalid_bare_final = False if not s.type.args: s.type = None if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs: + invalid_bare_final = True self.fail("Type in Final[...] can only be omitted if there is an initializer", s) else: s.type = s.type.args[0] @@ -1768,7 +1770,8 @@ def unwrap_final(self, s: AssignmentStmt) -> None: self.msg.protocol_members_cant_be_final(s) if (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs and not self.is_stub_file and not self.is_class_scope()): - self.msg.final_without_value(s) + if not invalid_bare_final: # Skip extra error messages. + self.msg.final_without_value(s) return def check_final_implicit_def(self, s: AssignmentStmt) -> None: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index c87749118c80..b4dc45a429da 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -198,16 +198,13 @@ def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost qu [case testFinalDefiningNoRhs] from typing import Final -x: Final # E: Type in Final[...] can only be omitted if there is an initializer \ - # E: Final name must be initialized with a value +x: Final # E: Type in Final[...] can only be omitted if there is an initializer y: Final[int] # E: Final name must be initialized with a value class C: - x: Final # E: Type in Final[...] can only be omitted if there is an initializer \ - # E: Final name must be initialized with a value + x: Final # E: Type in Final[...] can only be omitted if there is an initializer y: Final[int] # E: Final name must be initialized with a value def __init__(self) -> None: - self.z: Final # E: Type in Final[...] can only be omitted if there is an initializer \ - # E: Final name must be initialized with a value + self.z: Final # E: Type in Final[...] can only be omitted if there is an initializer reveal_type(x) # E: Revealed type is 'Any' reveal_type(y) # E: Revealed type is 'builtins.int' reveal_type(C().x) # E: Revealed type is 'Any' @@ -326,6 +323,16 @@ d: D d.x = 5 # E: Cannot assign to final attribute "x" [out] +[case testFinalDelayedDefinitionOtherMethod] +from typing import Final + +class C: + x: Final[int] # E: Final name must be initialized with a value + + def meth(self) -> None: + self.x = 2 # E: Cannot assign to final attribute "x" +[out] + -- Reassignments [case testFinalReassignModuleVar] From 98270fc5a046d07e8bf2d3f035e0ee038062f1ef Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 16:18:04 +0100 Subject: [PATCH 51/52] Fix self-check --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 467b9dd13137..db4dbfbb8502 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1998,7 +1998,7 @@ def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: not self.is_stub and # It is OK to skip initializer in stub files. # Avoid extra error messages, if there is no type in Final[...], # then we already reported the error about missing r.h.s. - s.type is not None): + isinstance(s, AssignmentStmt) and s.type is not None): self.msg.final_without_value(s) for lv in lvs: if isinstance(lv, RefExpr) and isinstance(lv.node, Var): From 8b52fdabc4fd392215a2dabddc1d7ff955ea4c63 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 16:34:06 +0100 Subject: [PATCH 52/52] Fix grammar --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index db4dbfbb8502..0ede0cb5a2bb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1922,7 +1922,7 @@ def check_compatibility_final_super(self, node: Var, base: TypeInfo, base_node: Optional[Node]) -> bool: """Check if an assignment overrides a final attribute in a base class. - This only check situations where either a node in base class is not a variable + This only checks situations where either a node in base class is not a variable but a final method, or where override is explicitly declared as final. In these cases we give a more detailed error message. In addition, we check that a final variable doesn't override writeable attribute, which is not safe.