Skip to content

Allow to use Final and ClassVar after python 3.13 #18358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 30, 2024
10 changes: 9 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 12 additions & 6 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions test-data/unit/check-final.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1128,6 +1129,7 @@ class A:
[builtins fixtures/tuple.pyi]

[case testFinalUsedWithClassVar]
# flags: --python-version 3.12
from typing import Final, ClassVar

class A:
Expand All @@ -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
Expand Down
Loading