diff --git a/mypy/checker.py b/mypy/checker.py index 096ec7abade1..0ede0cb5a2bb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -217,6 +217,12 @@ 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 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/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 def reset(self) -> None: """Cleanup stale state that might be left over from a typechecking run. @@ -1254,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_writable(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). @@ -1280,6 +1297,7 @@ def check_method_override_for_base_with_name( context = defn else: context = defn.func + # Construct the type of the overriding method. if isinstance(defn, FuncBase): typ = self.function_type(defn) # type: Type @@ -1453,6 +1471,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('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() @@ -1564,6 +1585,12 @@ 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 + # 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: + self.check_no_writable(name, second.node, ctx) # __slots__ is special and the type can vary across class hierarchy. if name == '__slots__': ok = True @@ -1611,7 +1638,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.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 self.options.disallow_any_unimported and @@ -1632,7 +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("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: @@ -1742,6 +1776,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 @@ -1852,6 +1892,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 @@ -1873,6 +1918,109 @@ 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: + """Check if an assignment overrides a final attribute in a base class. + + 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. + + 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)): + # 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: + self.check_no_writable(node.name(), base_node, node) + return True + + 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: + 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: + 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_writable(name, ctx) + + def get_final_context(self) -> bool: + """Check whether we a currently checking a final declaration.""" + return self._is_final_def + + @contextmanager + 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: + yield + finally: + self._is_final_def = old_ctx + + def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None: + """Check if this assignment does not assign to a final attribute. + + 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. + """ + 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 + 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 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. + 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): + name = lv.node.name() + cls = self.scope.active_class() + if cls is not None: + # 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:]: + sym = base.names.get(name) + # 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: + 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, infer_lvalue_type: bool = True) -> None: @@ -2520,7 +2668,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) @@ -2534,6 +2687,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 211f183a4733..03307881f954 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1609,10 +1609,10 @@ 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).""" 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/checkmember.py b/mypy/checkmember.py index ce978e4b92a3..a08ba75fddc7 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -262,6 +262,12 @@ 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(): + 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, chk=chk, implicit=implicit) @@ -304,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, attr_assign=True, ctx=ctx) + + def analyze_descriptor_access(instance_type: Type, descriptor_type: Type, builtin_type: Callable[[str], Instance], msg: MessageBuilder, @@ -535,6 +549,17 @@ def analyze_class_attribute_access(itype: Instance, if isinstance(node.node, TypeInfo): msg.fail(messages.CANNOT_ASSIGN_TO_TYPE, context) + # 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('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. + 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/messages.py b/mypy/messages.py index f121f6e4b3d9..6242699453ad 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -951,6 +951,27 @@ 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_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 "{}"' + ' (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. + + Pass `attr_assign=True` if the assignment assigns to an attribute. + """ + 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 member cannot 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/nodes.py b/mypy/nodes.py index a508b4256bcb..da2f0781aaaa 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] @@ -667,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 @@ -698,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_classvar', 'is_abstract_var', 'is_final', 'final_unset_in_class', 'final_set_in_init' ] @@ -712,6 +720,7 @@ class Var(SymbolNode): '_fullname', 'info', 'type', + 'final_value', 'is_self', 'is_ready', 'is_inferred', @@ -722,6 +731,9 @@ class Var(SymbolNode): 'is_settable_property', 'is_classvar', 'is_abstract_var', + 'is_final', + 'final_unset_in_class', + 'final_set_in_init', 'is_suppressed_import', ) @@ -748,6 +760,15 @@ 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 "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]] + # 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 @@ -767,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 @@ -777,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 @@ -919,6 +943,13 @@ class AssignmentStmt(Statement): new_syntax = False # type: bool # Does this assignment define a type alias? is_alias_def = False + # 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, type: 'Optional[mypy.types.Type]' = None, new_syntax: bool = False) -> None: @@ -2191,7 +2222,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: @@ -2211,6 +2242,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 7f346ac036ea..212bfe698c67 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -614,6 +614,24 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None: # redefinitions already. return + # 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 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 + # We know this is an overload def -- let's handle classmethod and staticmethod class_status = [] static_status = [] @@ -855,12 +873,15 @@ 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 in ('typing.final', + 'typing_extensions.final'): + defn.info.is_final = True def calculate_abstract_status(self, typ: TypeInfo) -> None: """Calculate abstract status of a class. @@ -1469,6 +1490,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) @@ -1668,8 +1691,17 @@ def add_type_alias_deps(self, aliases_used: Iterable[str], self.cur_mod_node.alias_deps[target].update(aliases_used) def visit_assignment_stmt(self, s: AssignmentStmt) -> None: + self.unwrap_final(s) + + 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) + 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: @@ -1697,6 +1729,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) @@ -1705,6 +1738,108 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> 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) + 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] + if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], RefExpr): + 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("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()): + 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: + """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 a final attribute in class body or __init__", 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 + 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 + 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. + 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)): + 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.""" if self.options.semantic_analysis_only or self.function_stack: @@ -1831,7 +1966,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[[bool], None]] = None) -> None: """Analyze an lvalue or assignment target. Args: @@ -1839,6 +1975,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. """ if isinstance(lval, NameExpr): # Top-level definitions within some statements (at least while) are @@ -1895,8 +2033,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] @@ -1909,14 +2047,24 @@ 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) - else: - # Bind to an existing name. - lval.accept(self) - self.check_lvalue_validity(lval.node, lval) + + # Redefining an existing name with final is always an error. + 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 = 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 + 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) + 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) @@ -1952,18 +2100,32 @@ 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[[bool], None]] = None) -> None: + """Analyze lvalue that is a member expression. - def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool = False) -> None: + 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" 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(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)): + # Note that `explicit_type` might has been erased for bare `Final`, + # 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) else: @@ -2231,6 +2393,15 @@ 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() 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) @@ -2333,6 +2504,18 @@ 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') 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: + 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 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/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 893ab624ed18..5314ca7f3f5b 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -116,6 +116,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. @@ -130,6 +132,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: @@ -137,6 +142,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 @@ -165,6 +171,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) @@ -252,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) @@ -266,8 +274,16 @@ 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 + v.final_value = None + def visit_index_expr(self, node: IndexExpr) -> None: node.analyzed = None # was a type alias super().visit_index_expr(node) 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/test/data.py b/mypy/test/data.py index 873ded377b6e..217d3489d89c 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 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/treetransform.py b/mypy/treetransform.py index 41616e85621c..981ea9bdf8a4 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,10 @@ 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.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 @@ -219,6 +225,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/mypy/typeanal.py b/mypy/typeanal.py index 16b41f9f36b3..a8c58e6ed379 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -228,6 +228,10 @@ 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 in ('typing.Final', 'typing_extensions.Final'): + 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: # Bare 'Tuple' is same as 'tuple' diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test new file mode 100644 index 000000000000..b4dc45a429da --- /dev/null +++ b/test-data/unit/check-final.test @@ -0,0 +1,1011 @@ +-- Test cases for final qualifier +-- + +-- Definitions + +[case testFinalDefiningModuleVar] +from typing import Final + +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(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] +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") +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] +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) # 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 name must be initialized with a value \ + # 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 +z: Any +z[0]: Final[int] # E: Unexpected type declaration \ + # E: Invalid final declaration +[out] + +[case testFinalDefiningInstanceVarStubs] +# Allow skipping r.h.s. +import mod +[file mod.pyi] +from typing import Final + +x: Final # E: Type in Final[...] can only be omitted if there is an initializer +y: Final[int] +class C: + 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 # 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' +v: C +reveal_type(v.z) # E: Revealed type is 'Any' +[out] + +[case testFinalDefiningFunc] +from typing import final + +@final # E: @final cannot 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 cannot 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 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 + + @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) +[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: ... + + @overload + def bad(self, x: int) -> int: ... + @final # Error! + @overload + def bad(self, x: str) -> str: ... +[out] +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] +from typing import final + +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 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 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 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] +from typing import Final +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 + 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 +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 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 + +T = TypeVar('T') +d: Any + +class C(Generic[T]): + x: Final[Tuple[T, T]] = d # E: Final name declared in class body cannot depend on type variables +[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 + self.y: Final = 1 + +reveal_type(C((1, 2)).x) # E: Revealed type is 'Tuple[builtins.int*, builtins.int*]' +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: Cannot access final instance attribute "y" on class object +[out] + +[case testFinalDefiningNotInOtherMethod] +from typing import Final, Any, Tuple + +class C: + def meth(self, x: Tuple[int, Any]) -> None: + 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] +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] + +[case testFinalNotInProtocol] +from typing import Final, final, Protocol, overload + +class P(Protocol): + 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 member cannot be final + def other(self, x): + pass +[out] + +[case testFinalNotInLoops] +from typing import Final + +for i in [1, 2, 3]: + x: Final = i # E: Cannot use Final inside a loop + +while True: + y: Final = True # E: Cannot use Final inside a loop +[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 name must be initialized with a value + + def __init__(self, x: int) -> None: + self.x = x # OK, deferred definition + self.x = 2 # E: Cannot assign to final attribute "x" + + def meth(self) -> None: + self.x = 2 # E: Cannot assign to final attribute "x" + +c: C +c.x = 3 # E: Cannot assign to final attribute "x" +class D(C): + x = 4 # E: Cannot assign to final name "x" +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 typing import Final + +x: Final = 1 +x = 2 # E: Cannot assign to final name "x" +def f() -> int: + global x + x = 3 # E: Cannot assign to final name "x" + return x + +y = 1 +y: Final = 2 # E: Name 'y' already defined on line 10 \ + # 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: Cannot redefine an existing name as final +z = 3 # E: Cannot assign to final name "z" +[out] + +[case testFinalReassignModuleReexport] +from typing import Final + +from lib import X +from lib.mod import ID + +X = 1 # Error! +ID: Final = 1 # Two errors! +ID = 1 # Error! +[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 # 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 + +def f() -> None: + nl: Final = 0 + x: Final = 1 + 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: Cannot assign to final name "nl" +[out] + +[case testFinalReassignModuleVarExternal] +import mod +mod.x = 2 # E: Cannot assign to final name "x" +[file mod.pyi] +from typing import Final +x: Final[int] +[out] + +[case testFinalReassignInstanceVarClassBody] +from typing import Final + +class C: + x: Final = 1 + x = 2 # E: Cannot assign to final name "x" + + y = 1 + y: Final = 2 # E: Cannot redefine an existing name as final +[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: Cannot redefine an existing name as final + def meth(self) -> None: + self.x = 2 # E: Cannot assign to final attribute "x" +[out] + +[case testFinalReassignInstanceVarClassVsInit] +from typing import Final + +class C: + y: Final = 1 + def __init__(self) -> None: + self.x: Final = 1 + self.y = 2 # E: Cannot assign to final attribute "y" + x = 2 # E: Cannot assign to final name "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: Cannot assign to final attribute "x" + self.y = 2 # E: Cannot assign to final attribute "y" + def other(self) -> None: + 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: Cannot assign to final attribute "x" + cls.y # E: Cannot access final instance attribute "y" on class object +[builtins fixtures/classmethod.pyi] +[out] + +[case testFinalReassignInstanceVarExternalClass] +from typing import Final + +class C: + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 + +class D(C): pass + +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] +from typing import Final + +class C: + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 + +class D(C): pass + +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: 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] + +[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: 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 + +[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 cannot be overridden by final. +class A: + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... + +class B(A): + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 +class C(B): + 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: Cannot assign to final attribute "x" +D.y = 4 # E: Cannot assign to final attribute "y" +[builtins fixtures/property.pyi] +[out] + +[case testFinalOverridingVarClassBodyExplicit] +from typing import Final + +class A: + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... +class B(A): + x: Final = 1 + def __init__(self) -> None: + self.y: Final = 1 +class C(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] + +[case testFinalOverridingVarInit] +from typing import Final + +class A: + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... +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: Cannot assign to final attribute "x" + self.y = 2 # E: Cannot assign to final attribute "y" + def meth(self) -> None: + 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] + +[case testFinalOverridingVarInit2] +from typing import Final + +class A: + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... +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 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] + +[case testFinalOverridingVarOtherMethod] +from typing import Final + +class A: + @property + def x(self) -> int: ... + @property + def y(self) -> int: ... +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: Cannot assign to final attribute "x" + self.y: int = 2 # E: Cannot 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] + +[case testFinalOverridingVarMultipleInheritanceClass] +from typing import Final, Any + +class A: + x: Final[Any] = 1 +class B: + @property + 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: 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] + +[case testFinalOverridingVarMultipleInheritanceInit] +from typing import Final, Any + +class A: + def __init__(self) -> None: + self.x: Final[Any] = 1 +class B: + @property + 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: 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] + +[case testFinalOverridingVarMultipleInheritanceMixed] +from typing import Final + +class A: + x: Final = 1 +class B: + def __init__(self) -> None: + self.x = 2 +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: 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] +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 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 in base class "A") + def x(self) -> None: pass + @property # E: Cannot override final attribute "y" (previously declared in 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 final attribute "x" (previously declared in base class "A") + def x(self) -> None: pass + @classmethod # E: Cannot override final attribute "y" (previously declared in base class "A") + def y(self) -> None: pass + +[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 + +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 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 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 in base class "A") + p: Final = a # E: Cannot override final attribute "p" (previously declared in base class "A") +[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: Cannot assign to final attribute "f" \ + # E: Cannot override final attribute "f" (previously declared in base class "A") + 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: Cannot assign to final attribute "f" +B().c = a # E: Cannot assign to final attribute "c" + +class C(A): + def __init__(self) -> None: + 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] + +[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 + +class A: + def m(self) -> int: pass +class B: + @final + 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 +[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 final attribute "m" (previously declared in base class "B") +class D(B, A): pass # E: Cannot override writable attribute "m" with a final one +[out] + +[case testFinalOverridingClassMethod] +from typing import final + +class B: + @classmethod + @final + def f(cls) -> int: pass + +class C(B): + @classmethod # E: Cannot override final attribute "f" (previously declared in 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 + @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] + +[case testFinalOverridingProperty] +from typing import final + +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] + +[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 final attribute "f" (previously declared in 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 typing import final + +@final +class B: ... +class C(B): # E: Cannot inherit from final class "B" + pass +class D(C): # E: Cannot inherit from final class "B" + pass +[out] + +[case testFinalClassNoInheritanceMulti] +from typing import final + +class A: ... +@final +class B: ... +class C(B, A): # E: Cannot inherit from final class "B" + pass +class D(A, B): # E: Cannot 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: 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: Cannot override writable attribute "x" with a final one + def x(self) -> int: ... + @final # E: Cannot override writable 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: Cannot assign to final name "x" + +class S: + x: Final = 1 +S.x = 2 # E: Cannot 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" (previously declared in base class "B") + +@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: Cannot assign to final name "x" + +class S: + x: F = 1 +S.x = 2 # E: Cannot 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: 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 bd1bb3d563ba..a902a25d5ed3 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4887,6 +4887,99 @@ def f(x: str) -> None: pass [out2] 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 = 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] +[out2] +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 + +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 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 + +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" (previously declared in base class "C") + -- These tests should just not crash [case testOverrideByBadVar] import a diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index d110f8efdb4c..0c618d1ed07e 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -944,3 +944,129 @@ class A: [out] __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 + +[file next.py] +from typing import Final + +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 + +[case testFinalFlagsTriggerMethod] +from typing import final + +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 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 diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 056c38aace91..ba64993fedde 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -7531,6 +7531,194 @@ Func = Callable[..., Any] [out] == +-- Test cases for final qualifier + +[case testFinalAddFinalVarAssignFine] +import mod +from a import D +from mod import x + +x = 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 + +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: 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 + +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 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 + +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" (previously declared in 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" (previously declared in base class "C") +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] +from typing import overload +from mod import C + +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 +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:6: error: Cannot override final attribute "meth" (previously declared in base class "C") + +[case testFinalAddFinalPropertyWithVarFine] +from mod import C + +def outer() -> None: + 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:5: error: Cannot override final attribute "p" (previously declared in base class "C") +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 a import x 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.pyi b/test-data/unit/lib-stub/typing.pyi index 9c227ec385ea..e57f71491a61 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,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 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 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())))