From 8885da66306e6f6ac66b36f2ba00fd1b26be2994 Mon Sep 17 00:00:00 2001 From: SW Date: Sun, 29 Dec 2024 14:43:45 +0900 Subject: [PATCH 1/9] Allow to use Final and ClassVar after python 3.13 --- mypy/semanal.py | 2 +- test-data/unit/check-final.test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 6e3335aed4e1..392cbdfeb8d5 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3657,7 +3657,7 @@ 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 diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 763183159e94..154950770768 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -1134,7 +1134,7 @@ class A: a: Final[ClassVar[int]] # E: Variable should not be annotated with both ClassVar and Final b: ClassVar[Final[int]] # E: Final can be only used as an outermost qualifier in a variable annotation c: ClassVar[Final] = 1 # E: Final can be only used as an outermost qualifier in a variable annotation -[out] +[out version<3.13] [case testFinalClassWithAbstractMethod] from typing import final From 6c4d78bfc06bdcd6792a0be893238fe74e845006 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Dec 2024 05:48:04 +0000 Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/semanal.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 392cbdfeb8d5..261226b14dfd 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.options.python_version < (3, 13) 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 From 4a41e4e1679c4b060e2c8a60c61dd9c11d963a75 Mon Sep 17 00:00:00 2001 From: triumph1 Date: Sun, 29 Dec 2024 16:36:48 +0900 Subject: [PATCH 3/9] Use python 3.12 and 3.13 flag, Final can be inside ClassVar after py 3.13 --- mypy/typeanal.py | 2 +- test-data/unit/check-final.test | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 751ed85ea6f3..f16dd1d14cf5 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -605,7 +605,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ t, code=codes.VALID_TYPE, ) - else: + elif self.options.python_version < (3, 13): self.fail( "Final can be only used as an outermost qualifier in a variable annotation", t, diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 154950770768..dfccaf08bb59 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -1128,13 +1128,23 @@ class A: [builtins fixtures/tuple.pyi] [case testFinalUsedWithClassVar] +# flags: --python-version 3.12 from typing import Final, ClassVar class A: a: Final[ClassVar[int]] # E: Variable should not be annotated with both ClassVar and Final b: ClassVar[Final[int]] # E: Final can be only used as an outermost qualifier in a variable annotation c: ClassVar[Final] = 1 # E: Final can be only used as an outermost qualifier in a variable annotation -[out version<3.13] +[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 77efce237a6f0c34aef78c58558989a3dd760e78 Mon Sep 17 00:00:00 2001 From: SW Date: Mon, 30 Dec 2024 17:07:59 +0900 Subject: [PATCH 4/9] Introduce allow_final_in_classvar --- mypy/semanal.py | 6 +++++- mypy/typeanal.py | 20 +++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 261226b14dfd..ee9e37ed2dab 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -5084,7 +5084,7 @@ def check_classvar(self, s: AssignmentStmt) -> None: node = lvalue.node if isinstance(node, Var): node.is_classvar = True - analyzed = self.anal_type(s.type) + analyzed = self.anal_type(s.type, allow_final_in_classvar=self.options.python_version >= (3, 13)) assert self.type is not None if analyzed is not None and set(get_type_vars(analyzed)) & set( self.type.defn.type_vars @@ -7361,6 +7361,7 @@ def type_analyzer( allow_unbound_tvars: bool = False, allow_placeholder: bool = False, allow_typed_dict_special_forms: bool = False, + allow_final_in_classvar: bool = False, allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_types: bool = True, @@ -7382,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_in_classvar=allow_final_in_classvar, allow_param_spec_literals=allow_param_spec_literals, allow_unpack=allow_unpack, prohibit_self_type=prohibit_self_type, @@ -7406,6 +7408,7 @@ def anal_type( allow_unbound_tvars: bool = False, allow_placeholder: bool = False, allow_typed_dict_special_forms: bool = False, + allow_final_in_classvar: bool = False, allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_types: bool = True, @@ -7442,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_in_classvar=allow_final_in_classvar, 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 f16dd1d14cf5..e6b5fa181a40 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_in_classvar: 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_in_classvar = allow_final_in_classvar # 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? @@ -605,12 +608,13 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ t, code=codes.VALID_TYPE, ) - elif self.options.python_version < (3, 13): - self.fail( - "Final can be only used as an outermost qualifier in a variable annotation", - t, - code=codes.VALID_TYPE, - ) + else: + if not self.allow_final_in_classvar: + 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_in_classvar=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_in_classvar: 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_in_classvar = allow_final_in_classvar old_allow_ellipsis = self.allow_ellipsis self.allow_ellipsis = allow_ellipsis old_allow_unpack = self.allow_unpack From d2fdf97c8c9296062ff0e382cd1fd8a1c3f64e35 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 08:08:59 +0000 Subject: [PATCH 5/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/semanal.py | 4 +++- mypy/typeanal.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index ee9e37ed2dab..44cc3a4a3db1 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -5084,7 +5084,9 @@ def check_classvar(self, s: AssignmentStmt) -> None: node = lvalue.node if isinstance(node, Var): node.is_classvar = True - analyzed = self.anal_type(s.type, allow_final_in_classvar=self.options.python_version >= (3, 13)) + analyzed = self.anal_type( + s.type, allow_final_in_classvar=self.options.python_version >= (3, 13) + ) assert self.type is not None if analyzed is not None and set(get_type_vars(analyzed)) & set( self.type.defn.type_vars diff --git a/mypy/typeanal.py b/mypy/typeanal.py index e6b5fa181a40..c8dda8fdca42 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -695,7 +695,9 @@ 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], allow_final_in_classvar=self.options.python_version >= (3, 13)) + return self.anal_type( + t.args[0], allow_final_in_classvar=self.options.python_version >= (3, 13) + ) elif fullname in NEVER_NAMES: return UninhabitedType() elif fullname in LITERAL_TYPE_NAMES: From 7a7ea03bfd3896698703a45cc6c6ce8481c502c4 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 30 Dec 2024 00:41:07 -0800 Subject: [PATCH 6/9] remove (i think) unnecessary allowance of final --- mypy/semanal.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 44cc3a4a3db1..6801dfb7b6b9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -5084,9 +5084,7 @@ def check_classvar(self, s: AssignmentStmt) -> None: node = lvalue.node if isinstance(node, Var): node.is_classvar = True - analyzed = self.anal_type( - s.type, allow_final_in_classvar=self.options.python_version >= (3, 13) - ) + analyzed = self.anal_type(s.type) assert self.type is not None if analyzed is not None and set(get_type_vars(analyzed)) & set( self.type.defn.type_vars From a4eef39899531fa061838c964febb4b644cdabdc Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 30 Dec 2024 00:41:33 -0800 Subject: [PATCH 7/9] add a test on a different python version --- test-data/unit/check-final.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index dfccaf08bb59..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 From f7cbd1b82323f7f00874116edf88d6b74b784a03 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 30 Dec 2024 00:41:45 -0800 Subject: [PATCH 8/9] rename allow_final_in_classvar to allow_final --- mypy/semanal.py | 8 ++++---- mypy/typeanal.py | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 6801dfb7b6b9..1507f00119f4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -7361,7 +7361,7 @@ def type_analyzer( allow_unbound_tvars: bool = False, allow_placeholder: bool = False, allow_typed_dict_special_forms: bool = False, - allow_final_in_classvar: bool = False, + allow_final: bool = False, allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_types: bool = True, @@ -7383,7 +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_in_classvar=allow_final_in_classvar, + allow_final=allow_final, allow_param_spec_literals=allow_param_spec_literals, allow_unpack=allow_unpack, prohibit_self_type=prohibit_self_type, @@ -7408,7 +7408,7 @@ def anal_type( allow_unbound_tvars: bool = False, allow_placeholder: bool = False, allow_typed_dict_special_forms: bool = False, - allow_final_in_classvar: bool = False, + allow_final: bool = False, allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_types: bool = True, @@ -7445,7 +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_in_classvar=allow_final_in_classvar, + 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 c8dda8fdca42..dd48fd324ad4 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -225,7 +225,7 @@ def __init__( allow_unbound_tvars: bool = False, allow_placeholder: bool = False, allow_typed_dict_special_forms: bool = False, - allow_final_in_classvar: bool = True, + allow_final: bool = True, allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_types: bool = True, @@ -262,7 +262,7 @@ def __init__( # 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_in_classvar = allow_final_in_classvar + 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? @@ -609,7 +609,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ code=codes.VALID_TYPE, ) else: - if not self.allow_final_in_classvar: + if not self.allow_final: self.fail( "Final can be only used as an outermost qualifier in a variable annotation", t, @@ -696,7 +696,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ ) return AnyType(TypeOfAny.from_error) return self.anal_type( - t.args[0], allow_final_in_classvar=self.options.python_version >= (3, 13) + t.args[0], allow_final=self.options.python_version >= (3, 13) ) elif fullname in NEVER_NAMES: return UninhabitedType() @@ -1883,13 +1883,13 @@ def anal_type( allow_unpack: bool = False, allow_ellipsis: bool = False, allow_typed_dict_special_forms: bool = False, - allow_final_in_classvar: 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_in_classvar = allow_final_in_classvar + self.allow_final = allow_final old_allow_ellipsis = self.allow_ellipsis self.allow_ellipsis = allow_ellipsis old_allow_unpack = self.allow_unpack From 525e2a8b616aebffb0cb9bdc3d8348b550a8c429 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 08:43:21 +0000 Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/typeanal.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index dd48fd324ad4..c9a7539fda67 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -695,9 +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], allow_final=self.options.python_version >= (3, 13) - ) + 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: