Skip to content

Commit 62e6f51

Browse files
authored
Final names and attributes (#5522)
Fixes #1214 Fixes python/typing#286 Fixes python/typing#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
1 parent 614090b commit 62e6f51

21 files changed

+1943
-30
lines changed

mypy/checker.py

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option
217217
# If True, process function definitions. If False, don't. This is used
218218
# for processing module top levels in fine-grained incremental mode.
219219
self.recurse_into_functions = True
220+
# This internal flag is used to track whether we a currently type-checking
221+
# a final declaration (assignment), so that some errors should be suppressed.
222+
# Should not be set manually, use get_final_context/enter_final_context instead.
223+
# NOTE: we use the context manager to avoid "threading" an additional `is_final_def`
224+
# argument through various `checker` and `checkmember` functions.
225+
self._is_final_def = False
220226

221227
def reset(self) -> None:
222228
"""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
12541260
"""Check if method definition is compatible with a base class."""
12551261
if base:
12561262
name = defn.name()
1263+
base_attr = base.names.get(name)
1264+
if base_attr:
1265+
# First, check if we override a final (always an error, even with Any types).
1266+
if (isinstance(base_attr.node, (Var, FuncBase, Decorator))
1267+
and base_attr.node.is_final):
1268+
self.msg.cant_override_final(name, base.name(), defn)
1269+
# Second, final can't override anything writeable independently of types.
1270+
if defn.is_final:
1271+
self.check_no_writable(name, base_attr.node, defn)
1272+
1273+
# Check the type of override.
12571274
if name not in ('__init__', '__new__', '__init_subclass__'):
12581275
# Check method override
12591276
# (__init__, __new__, __init_subclass__ are special).
@@ -1280,6 +1297,7 @@ def check_method_override_for_base_with_name(
12801297
context = defn
12811298
else:
12821299
context = defn.func
1300+
12831301
# Construct the type of the overriding method.
12841302
if isinstance(defn, FuncBase):
12851303
typ = self.function_type(defn) # type: Type
@@ -1453,6 +1471,9 @@ def visit_class_def(self, defn: ClassDef) -> None:
14531471
typ = defn.info
14541472
if typ.is_protocol and typ.defn.type_vars:
14551473
self.check_protocol_variance(defn)
1474+
for base in typ.mro[1:]:
1475+
if base.is_final:
1476+
self.fail('Cannot inherit from final class "{}"'.format(base.name()), defn)
14561477
with self.tscope.class_scope(defn.info), self.enter_partial_types(is_class=True):
14571478
old_binder = self.binder
14581479
self.binder = ConditionalTypeBinder()
@@ -1564,6 +1585,12 @@ def check_compatibility(self, name: str, base1: TypeInfo,
15641585
if second_type is None:
15651586
self.msg.cannot_determine_type_in_base(name, base2.name(), ctx)
15661587
ok = True
1588+
# Final attributes can never be overridden, but can override
1589+
# non-final read-only attributes.
1590+
if isinstance(second.node, (Var, FuncBase, Decorator)) and second.node.is_final:
1591+
self.msg.cant_override_final(name, base2.name(), ctx)
1592+
if isinstance(first.node, (Var, FuncBase, Decorator)) and first.node.is_final:
1593+
self.check_no_writable(name, second.node, ctx)
15671594
# __slots__ is special and the type can vary across class hierarchy.
15681595
if name == '__slots__':
15691596
ok = True
@@ -1611,7 +1638,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
16111638
16121639
Handle all kinds of assignment statements (simple, indexed, multiple).
16131640
"""
1614-
self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)
1641+
with self.enter_final_context(s.is_final_def):
1642+
self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)
16151643

16161644
if (s.type is not None and
16171645
self.options.disallow_any_unimported and
@@ -1632,7 +1660,13 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
16321660
self.expr_checker.accept(s.rvalue)
16331661
rvalue = self.temp_node(self.type_map[s.rvalue], s)
16341662
for lv in s.lvalues[:-1]:
1635-
self.check_assignment(lv, rvalue, s.type is None)
1663+
with self.enter_final_context(s.is_final_def):
1664+
self.check_assignment(lv, rvalue, s.type is None)
1665+
1666+
self.check_final(s)
1667+
if (s.is_final_def and s.type and not has_no_typevars(s.type)
1668+
and self.scope.active_class() is not None):
1669+
self.fail("Final name declared in class body cannot depend on type variables", s)
16361670

16371671
def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type: bool = True,
16381672
new_syntax: bool = False) -> None:
@@ -1742,6 +1776,12 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[
17421776
# Show only one error per variable
17431777
break
17441778

1779+
if not self.check_compatibility_final_super(lvalue_node,
1780+
base,
1781+
tnode.node):
1782+
# Show only one error per variable
1783+
break
1784+
17451785
for base in lvalue_node.info.mro[1:]:
17461786
# Only check __slots__ against the 'object'
17471787
# 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,
18521892
# value, not the Callable
18531893
if base_node.is_property:
18541894
base_type = base_type.ret_type
1895+
if isinstance(base_type, FunctionLike) and isinstance(base_node,
1896+
OverloadedFuncDef):
1897+
# Same for properties with setter
1898+
if base_node.is_property:
1899+
base_type = base_type.items()[0].ret_type
18551900

18561901
return base_type, base_node
18571902

@@ -1873,6 +1918,109 @@ def check_compatibility_classvar_super(self, node: Var,
18731918
return False
18741919
return True
18751920

1921+
def check_compatibility_final_super(self, node: Var,
1922+
base: TypeInfo, base_node: Optional[Node]) -> bool:
1923+
"""Check if an assignment overrides a final attribute in a base class.
1924+
1925+
This only checks situations where either a node in base class is not a variable
1926+
but a final method, or where override is explicitly declared as final.
1927+
In these cases we give a more detailed error message. In addition, we check that
1928+
a final variable doesn't override writeable attribute, which is not safe.
1929+
1930+
Other situations are checked in `check_final()`.
1931+
"""
1932+
if not isinstance(base_node, (Var, FuncBase, Decorator)):
1933+
return True
1934+
if base_node.is_final and (node.is_final or not isinstance(base_node, Var)):
1935+
# Give this error only for explicit override attempt with `Final`, or
1936+
# if we are overriding a final method with variable.
1937+
# Other override attempts will be flagged as assignment to constant
1938+
# in `check_final()`.
1939+
self.msg.cant_override_final(node.name(), base.name(), node)
1940+
return False
1941+
if node.is_final:
1942+
self.check_no_writable(node.name(), base_node, node)
1943+
return True
1944+
1945+
def check_no_writable(self, name: str, base_node: Optional[Node], ctx: Context) -> None:
1946+
"""Check that a final variable doesn't override writeable attribute.
1947+
1948+
This is done to prevent situations like this:
1949+
class C:
1950+
attr = 1
1951+
class D(C):
1952+
attr: Final = 2
1953+
1954+
x: C = D()
1955+
x.attr = 3 # Oops!
1956+
"""
1957+
if isinstance(base_node, Var):
1958+
ok = False
1959+
elif isinstance(base_node, OverloadedFuncDef) and base_node.is_property:
1960+
first_item = cast(Decorator, base_node.items[0])
1961+
ok = not first_item.var.is_settable_property
1962+
else:
1963+
ok = True
1964+
if not ok:
1965+
self.msg.final_cant_override_writable(name, ctx)
1966+
1967+
def get_final_context(self) -> bool:
1968+
"""Check whether we a currently checking a final declaration."""
1969+
return self._is_final_def
1970+
1971+
@contextmanager
1972+
def enter_final_context(self, is_final_def: bool) -> Iterator[None]:
1973+
"""Store whether the current checked assignment is a final declaration."""
1974+
old_ctx = self._is_final_def
1975+
self._is_final_def = is_final_def
1976+
try:
1977+
yield
1978+
finally:
1979+
self._is_final_def = old_ctx
1980+
1981+
def check_final(self, s: Union[AssignmentStmt, OperatorAssignmentStmt]) -> None:
1982+
"""Check if this assignment does not assign to a final attribute.
1983+
1984+
This function performs the check only for name assignments at module
1985+
and class scope. The assignments to `obj.attr` and `Cls.attr` are checked
1986+
in checkmember.py.
1987+
"""
1988+
if isinstance(s, AssignmentStmt):
1989+
lvs = self.flatten_lvalues(s.lvalues)
1990+
else:
1991+
lvs = [s.lvalue]
1992+
is_final_decl = s.is_final_def if isinstance(s, AssignmentStmt) else False
1993+
if is_final_decl and self.scope.active_class():
1994+
lv = lvs[0]
1995+
assert isinstance(lv, RefExpr)
1996+
assert isinstance(lv.node, Var)
1997+
if (lv.node.final_unset_in_class and not lv.node.final_set_in_init and
1998+
not self.is_stub and # It is OK to skip initializer in stub files.
1999+
# Avoid extra error messages, if there is no type in Final[...],
2000+
# then we already reported the error about missing r.h.s.
2001+
isinstance(s, AssignmentStmt) and s.type is not None):
2002+
self.msg.final_without_value(s)
2003+
for lv in lvs:
2004+
if isinstance(lv, RefExpr) and isinstance(lv.node, Var):
2005+
name = lv.node.name()
2006+
cls = self.scope.active_class()
2007+
if cls is not None:
2008+
# Theses additional checks exist to give more error messages
2009+
# even if the final attribute was overridden with a new symbol
2010+
# (which is itself an error)...
2011+
for base in cls.mro[1:]:
2012+
sym = base.names.get(name)
2013+
# We only give this error if base node is variable,
2014+
# overriding final method will be caught in
2015+
# `check_compatibility_final_super()`.
2016+
if sym and isinstance(sym.node, Var):
2017+
if sym.node.is_final and not is_final_decl:
2018+
self.msg.cant_assign_to_final(name, sym.node.info is None, s)
2019+
# ...but only once
2020+
break
2021+
if lv.node.is_final and not is_final_decl:
2022+
self.msg.cant_assign_to_final(name, lv.node.info is None, s)
2023+
18762024
def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression,
18772025
context: Context,
18782026
infer_lvalue_type: bool = True) -> None:
@@ -2520,7 +2668,12 @@ def visit_while_stmt(self, s: WhileStmt) -> None:
25202668
def visit_operator_assignment_stmt(self,
25212669
s: OperatorAssignmentStmt) -> None:
25222670
"""Type check an operator assignment statement, e.g. x += 1."""
2523-
lvalue_type = self.expr_checker.accept(s.lvalue)
2671+
if isinstance(s.lvalue, MemberExpr):
2672+
# Special case, some additional errors may be given for
2673+
# assignments to read-only or final attributes.
2674+
lvalue_type = self.expr_checker.visit_member_expr(s.lvalue, True)
2675+
else:
2676+
lvalue_type = self.expr_checker.accept(s.lvalue)
25242677
inplace, method = infer_operator_assignment_method(lvalue_type, s.op)
25252678
if inplace:
25262679
# There is __ifoo__, treat as x = x.__ifoo__(y)
@@ -2534,6 +2687,7 @@ def visit_operator_assignment_stmt(self,
25342687
expr.set_line(s)
25352688
self.check_assignment(lvalue=s.lvalue, rvalue=expr,
25362689
infer_lvalue_type=True, new_syntax=False)
2690+
self.check_final(s)
25372691

25382692
def visit_assert_stmt(self, s: AssertStmt) -> None:
25392693
self.expr_checker.accept(s.expr)

mypy/checkexpr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,10 +1609,10 @@ def apply_generic_arguments(self, callable: CallableType, types: Sequence[Option
16091609
"""Simple wrapper around mypy.applytype.apply_generic_arguments."""
16101610
return applytype.apply_generic_arguments(callable, types, self.msg, context)
16111611

1612-
def visit_member_expr(self, e: MemberExpr) -> Type:
1612+
def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type:
16131613
"""Visit member expression (of form e.id)."""
16141614
self.chk.module_refs.update(extract_refexpr_names(e))
1615-
result = self.analyze_ordinary_member_access(e, False)
1615+
result = self.analyze_ordinary_member_access(e, is_lvalue)
16161616
return self.narrow_type_from_binder(e, result)
16171617

16181618
def analyze_ordinary_member_access(self, e: MemberExpr,

mypy/checkmember.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,12 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo,
262262

263263
if isinstance(v, Var):
264264
implicit = info[name].implicit
265+
266+
# An assignment to final attribute is always an error,
267+
# independently of types.
268+
if is_lvalue and not chk.get_final_context():
269+
check_final_member(name, info, msg, node)
270+
265271
return analyze_var(name, v, itype, info, node, is_lvalue, msg,
266272
original_type, builtin_type, not_ready_callback,
267273
chk=chk, implicit=implicit)
@@ -304,6 +310,14 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo,
304310
return msg.has_no_attr(original_type, itype, name, node)
305311

306312

313+
def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Context) -> None:
314+
"""Give an error if the name being assigned was declared as final."""
315+
for base in info.mro:
316+
sym = base.names.get(name)
317+
if sym and isinstance(sym.node, (Var, FuncBase, Decorator)) and sym.node.is_final:
318+
msg.cant_assign_to_final(name, attr_assign=True, ctx=ctx)
319+
320+
307321
def analyze_descriptor_access(instance_type: Type, descriptor_type: Type,
308322
builtin_type: Callable[[str], Instance],
309323
msg: MessageBuilder,
@@ -535,6 +549,17 @@ def analyze_class_attribute_access(itype: Instance,
535549
if isinstance(node.node, TypeInfo):
536550
msg.fail(messages.CANNOT_ASSIGN_TO_TYPE, context)
537551

552+
# If a final attribute was declared on `self` in `__init__`, then it
553+
# can't be accessed on the class object.
554+
if node.implicit and isinstance(node.node, Var) and node.node.is_final:
555+
msg.fail('Cannot access final instance '
556+
'attribute "{}" on class object'.format(node.node.name()), context)
557+
558+
# An assignment to final attribute on class object is also always an error,
559+
# independently of types.
560+
if is_lvalue and not chk.get_final_context():
561+
check_final_member(name, itype.type, msg, context)
562+
538563
if itype.type.is_enum and not (is_lvalue or is_decorated or is_method):
539564
return itype
540565

mypy/messages.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,27 @@ def cant_assign_to_method(self, context: Context) -> None:
951951
def cant_assign_to_classvar(self, name: str, context: Context) -> None:
952952
self.fail('Cannot assign to class variable "%s" via instance' % name, context)
953953

954+
def final_cant_override_writable(self, name: str, ctx: Context) -> None:
955+
self.fail('Cannot override writable attribute "{}" with a final one'.format(name), ctx)
956+
957+
def cant_override_final(self, name: str, base_name: str, ctx: Context) -> None:
958+
self.fail('Cannot override final attribute "{}"'
959+
' (previously declared in base class "{}")'.format(name, base_name), ctx)
960+
961+
def cant_assign_to_final(self, name: str, attr_assign: bool, ctx: Context) -> None:
962+
"""Warn about a prohibited assignment to a final attribute.
963+
964+
Pass `attr_assign=True` if the assignment assigns to an attribute.
965+
"""
966+
kind = "attribute" if attr_assign else "name"
967+
self.fail('Cannot assign to final {} "{}"'.format(kind, name), ctx)
968+
969+
def protocol_members_cant_be_final(self, ctx: Context) -> None:
970+
self.fail("Protocol member cannot be final", ctx)
971+
972+
def final_without_value(self, ctx: Context) -> None:
973+
self.fail("Final name must be initialized with a value", ctx)
974+
954975
def read_only_property(self, name: str, type: TypeInfo,
955976
context: Context) -> None:
956977
self.fail('Property "{}" defined in "{}" is read-only'.format(

0 commit comments

Comments
 (0)