From 62e6f513d9648e3cff72703bb51e0312d6123249 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 11 Sep 2018 17:34:36 +0100 Subject: [PATCH] Final names and attributes (#5522) Fixes https://github.com/python/mypy/issues/1214 Fixes https://github.com/python/typing/issues/286 Fixes https://github.com/python/typing/issues/242 (partially, other part is out of scope) This is a working implementation of final access qualifier briefly discussed at PyCon typing meeting. Final names/attributes can be used to have more static guarantees about semantics of some code and can be used by other tools like mypyc for optimizations. We can play with this implementation before starting to write an actual PEP. The basic idea is simple: once declared as final, a name/attribute can't be re-assigned, overridden, or redefined in any other way. For example: ```python from typing import Final NO: Final = 0 YES: Final = 255 class BaseEngine: RATE: Final[float] = 3000 YES = 1 # Error! class Engine(BaseEngine): RATE = 9000 # Also an error! ``` For more use cases, examples, and specification, see the docs patch. Here are some comments on decisions made: * __What can be final?__ It is hard to say what semantic nodes are important, I started from just module and class constants, but quickly realized it is hard to draw the line without missing some use cases (in particular for mypyc). So I went ahead and implemented all of them, everything can be final: module constants, class-level and instance-level attributes, method, and also classes. * __Two names or one name?__ I currently use two names `Final` for assignments and `@final` for decorators. My PEP8-formatted mind just can't accept `@Final` :-) * __Should re-exported names keep they const-ness?__ I think yes, this is a very common pattern, so it looks like this is a sane default. * __What to do with instance-level vs class-level attributes?__ The point here is that mypy has a common namespace for class attributes. I didn't want to complicate things (including the mental model), so I just decided that one can't have, e.g., a name that is constant on class but assignable on instances, etc. Such use cases are relatively rare, and we can implement this later if there will be high demand for this. ...deferred features: * I didn't implement any constant propagation in mypy _yet_. This can be done later on per use-case basis. For example: ```python fields: Final = [('x', int), ('y', int)] NT = NamedTuple('NT', fields) ``` * __Should final classes be like sealed in Scala?__ I think probably no. On one hand it could be be a nice feature, on other hand it complicates the mental model and is less useful for things like mypyc. * I don't allow `Final` in function argument types. One argument is simplicity, another is I didn't see many bugs related to shadowing an argument in function bodies, finally people might have quite different expectations for this. If people will ask, this would be easy to implement. ...and implementation internals: * There are two additional safety nets that I don't mention in the docs: (a) there can be no `TypeVar`s in the type of class-level constant, (b) instance-level constant can't be accessed on the class object. * I generate errors for re-definitions in all subclasses, not only in immediate children. I think this is what most people would want: turning something into a constant will flag most re-assignment points. * We store the `final_value` for constants initialized with a simple literal, but we never use it. This exists only for tools like mypyc that may use it for optimizations. cc @ambv @rchen152 @vlasovskikh --- mypy/checker.py | 160 ++- mypy/checkexpr.py | 4 +- mypy/checkmember.py | 25 + mypy/messages.py | 21 + mypy/nodes.py | 40 +- mypy/semanal.py | 221 +++- mypy/server/astdiff.py | 9 +- mypy/server/aststrip.py | 16 + mypy/strconv.py | 2 + mypy/test/data.py | 1 + mypy/test/testcheck.py | 1 + mypy/treetransform.py | 7 + mypy/typeanal.py | 4 + test-data/unit/check-final.test | 1011 +++++++++++++++++ test-data/unit/check-incremental.test | 93 ++ test-data/unit/diff.test | 126 ++ test-data/unit/fine-grained.test | 188 +++ test-data/unit/fixtures/property.pyi | 1 + test-data/unit/lib-stub/typing.pyi | 4 + test-data/unit/lib-stub/typing_extensions.pyi | 3 + test-data/unit/semanal-basic.test | 36 + 21 files changed, 1943 insertions(+), 30 deletions(-) create mode 100644 test-data/unit/check-final.test 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())))