Skip to content

Commit

Permalink
Allow to use Final and ClassVar after Python 3.13 (#18358)
Browse files Browse the repository at this point in the history
This PR allows to use Final and ClassVar after python 3.13

I saw this [PR](#10478)

and I saw recent changes of python 3.13
https://docs.python.org/3/library/typing.html#typing.Final

Final now can be nested with ClassVar. so I added a version check!

---------

Co-authored-by: triumph1 <[email protected]>
Co-authored-by: hauntsaninja <[email protected]>
  • Loading branch information
3 people authored Dec 30, 2024
1 parent 9e40be6 commit 80e5e8b
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 7 deletions.
10 changes: 9 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3658,7 +3658,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 @@ -7358,6 +7362,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 @@ -7379,6 +7384,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 @@ -7403,6 +7409,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 @@ -7439,6 +7446,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 @@ -226,6 +226,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 @@ -261,6 +262,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 @@ -607,11 +610,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 @@ -692,7 +696,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 @@ -1878,11 +1882,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

0 comments on commit 80e5e8b

Please sign in to comment.