diff --git a/mypy/semanal.py b/mypy/semanal.py index 6e3335aed4e1..1507f00119f4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3657,7 +3657,11 @@ def unwrap_final(self, s: AssignmentStmt) -> bool: else: s.type = s.unanalyzed_type.args[0] - if s.type is not None and self.is_classvar(s.type): + if ( + s.type is not None + and self.options.python_version < (3, 13) + and self.is_classvar(s.type) + ): self.fail("Variable should not be annotated with both ClassVar and Final", s) return False @@ -7357,6 +7361,7 @@ def type_analyzer( allow_unbound_tvars: bool = False, allow_placeholder: bool = False, allow_typed_dict_special_forms: bool = False, + allow_final: bool = False, allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_types: bool = True, @@ -7378,6 +7383,7 @@ def type_analyzer( report_invalid_types=report_invalid_types, allow_placeholder=allow_placeholder, allow_typed_dict_special_forms=allow_typed_dict_special_forms, + allow_final=allow_final, allow_param_spec_literals=allow_param_spec_literals, allow_unpack=allow_unpack, prohibit_self_type=prohibit_self_type, @@ -7402,6 +7408,7 @@ def anal_type( allow_unbound_tvars: bool = False, allow_placeholder: bool = False, allow_typed_dict_special_forms: bool = False, + allow_final: bool = False, allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_types: bool = True, @@ -7438,6 +7445,7 @@ def anal_type( allow_tuple_literal=allow_tuple_literal, allow_placeholder=allow_placeholder, allow_typed_dict_special_forms=allow_typed_dict_special_forms, + allow_final=allow_final, allow_param_spec_literals=allow_param_spec_literals, allow_unpack=allow_unpack, report_invalid_types=report_invalid_types, diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 751ed85ea6f3..c9a7539fda67 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -225,6 +225,7 @@ def __init__( allow_unbound_tvars: bool = False, allow_placeholder: bool = False, allow_typed_dict_special_forms: bool = False, + allow_final: bool = True, allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_types: bool = True, @@ -260,6 +261,8 @@ def __init__( self.allow_placeholder = allow_placeholder # Are we in a context where Required[] is allowed? self.allow_typed_dict_special_forms = allow_typed_dict_special_forms + # Set True when we analyze ClassVar else False + self.allow_final = allow_final # Are we in a context where ParamSpec literals are allowed? self.allow_param_spec_literals = allow_param_spec_literals # Are we in context where literal "..." specifically is allowed? @@ -606,11 +609,12 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ code=codes.VALID_TYPE, ) else: - self.fail( - "Final can be only used as an outermost qualifier in a variable annotation", - t, - code=codes.VALID_TYPE, - ) + if not self.allow_final: + self.fail( + "Final can be only used as an outermost qualifier in a variable annotation", + t, + code=codes.VALID_TYPE, + ) return AnyType(TypeOfAny.from_error) elif fullname == "typing.Tuple" or ( fullname == "builtins.tuple" @@ -691,7 +695,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ "ClassVar[...] must have at most one type argument", t, code=codes.VALID_TYPE ) return AnyType(TypeOfAny.from_error) - return self.anal_type(t.args[0]) + return self.anal_type(t.args[0], allow_final=self.options.python_version >= (3, 13)) elif fullname in NEVER_NAMES: return UninhabitedType() elif fullname in LITERAL_TYPE_NAMES: @@ -1877,11 +1881,13 @@ def anal_type( allow_unpack: bool = False, allow_ellipsis: bool = False, allow_typed_dict_special_forms: bool = False, + allow_final: bool = False, ) -> Type: if nested: self.nesting_level += 1 old_allow_typed_dict_special_forms = self.allow_typed_dict_special_forms self.allow_typed_dict_special_forms = allow_typed_dict_special_forms + self.allow_final = allow_final old_allow_ellipsis = self.allow_ellipsis self.allow_ellipsis = allow_ellipsis old_allow_unpack = self.allow_unpack diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 763183159e94..51ce0edc66c2 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -194,6 +194,7 @@ def g(x: int) -> Final[int]: ... # E: Final can be only used as an outermost qu [out] [case testFinalDefiningNotInMethodExtensions] +# flags: --python-version 3.14 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 @@ -1128,6 +1129,7 @@ class A: [builtins fixtures/tuple.pyi] [case testFinalUsedWithClassVar] +# flags: --python-version 3.12 from typing import Final, ClassVar class A: @@ -1136,6 +1138,15 @@ class A: c: ClassVar[Final] = 1 # E: Final can be only used as an outermost qualifier in a variable annotation [out] +[case testFinalUsedWithClassVarAfterPy313] +# flags: --python-version 3.13 +from typing import Final, ClassVar + +class A: + a: Final[ClassVar[int]] = 1 + b: ClassVar[Final[int]] = 1 + c: ClassVar[Final] = 1 + [case testFinalClassWithAbstractMethod] from typing import final from abc import ABC, abstractmethod