diff --git a/mypy/checker.py b/mypy/checker.py index 2edcaa6bc5c5..8b7d5207711c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3565,7 +3565,7 @@ def check_final(self, s: AssignmentStmt | OperatorAssignmentStmt | AssignmentExp else: lvs = [s.lvalue] is_final_decl = s.is_final_def if isinstance(s, AssignmentStmt) else False - if is_final_decl and self.scope.active_class(): + if is_final_decl and (active_class := self.scope.active_class()): lv = lvs[0] assert isinstance(lv, RefExpr) if lv.node is not None: @@ -3579,6 +3579,9 @@ def check_final(self, s: AssignmentStmt | OperatorAssignmentStmt | AssignmentExp # then we already reported the error about missing r.h.s. isinstance(s, AssignmentStmt) and s.type is not None + # Avoid extra error message for NamedTuples, + # they were reported during semanal + and not active_class.is_named_tuple ): self.msg.final_without_value(s) for lv in lvs: diff --git a/mypy/semanal.py b/mypy/semanal.py index edcc50e66e30..e90ab9f160e0 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3646,7 +3646,12 @@ def unwrap_final(self, s: AssignmentStmt) -> bool: invalid_bare_final = False if not s.unanalyzed_type.args: s.type = None - if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs: + if ( + isinstance(s.rvalue, TempNode) + and s.rvalue.no_rhs + # Filter duplicate errors, we already reported this: + and not (self.type and self.type.is_named_tuple) + ): invalid_bare_final = True self.fail("Type in Final[...] can only be omitted if there is an initializer", s) else: @@ -7351,6 +7356,7 @@ def type_analyzer( allow_unpack: bool = False, report_invalid_types: bool = True, prohibit_self_type: str | None = None, + prohibit_special_class_field_types: str | None = None, allow_type_any: bool = False, ) -> TypeAnalyser: if tvar_scope is None: @@ -7370,6 +7376,7 @@ def type_analyzer( allow_param_spec_literals=allow_param_spec_literals, allow_unpack=allow_unpack, prohibit_self_type=prohibit_self_type, + prohibit_special_class_field_types=prohibit_special_class_field_types, allow_type_any=allow_type_any, ) tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic()) @@ -7394,6 +7401,7 @@ def anal_type( allow_unpack: bool = False, report_invalid_types: bool = True, prohibit_self_type: str | None = None, + prohibit_special_class_field_types: str | None = None, allow_type_any: bool = False, ) -> Type | None: """Semantically analyze a type. @@ -7429,6 +7437,7 @@ def anal_type( allow_unpack=allow_unpack, report_invalid_types=report_invalid_types, prohibit_self_type=prohibit_self_type, + prohibit_special_class_field_types=prohibit_special_class_field_types, allow_type_any=allow_type_any, ) tag = self.track_incomplete_refs() diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 7c6da7721e8f..dfc99576e617 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -191,6 +191,7 @@ def check_namedtuple_classdef( stmt.type, allow_placeholder=not self.api.is_func_scope(), prohibit_self_type="NamedTuple item type", + prohibit_special_class_field_types="NamedTuple", ) if analyzed is None: # Something is incomplete. We need to defer this named tuple. @@ -483,6 +484,7 @@ def parse_namedtuple_fields_with_types( type, allow_placeholder=not self.api.is_func_scope(), prohibit_self_type="NamedTuple item type", + prohibit_special_class_field_types="NamedTuple", ) # Workaround #4987 and avoid introducing a bogus UnboundType if isinstance(analyzed, UnboundType): diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index cb0bdebab724..941a16a7fd5d 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -185,6 +185,7 @@ def anal_type( allow_placeholder: bool = False, report_invalid_types: bool = True, prohibit_self_type: str | None = None, + prohibit_special_class_field_types: str | None = None, ) -> Type | None: raise NotImplementedError diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index d081898bf010..7b6e48eacb39 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -330,6 +330,7 @@ def analyze_typeddict_classdef_fields( allow_typed_dict_special_forms=True, allow_placeholder=not self.api.is_func_scope(), prohibit_self_type="TypedDict item type", + prohibit_special_class_field_types="TypedDict", ) if analyzed is None: return None, [], [], set(), set() # Need to defer @@ -561,6 +562,7 @@ def parse_typeddict_fields_with_types( allow_typed_dict_special_forms=True, allow_placeholder=not self.api.is_func_scope(), prohibit_self_type="TypedDict item type", + prohibit_special_class_field_types="TypedDict", ) if analyzed is None: return None diff --git a/mypy/typeanal.py b/mypy/typeanal.py index bc340c194cdc..32aad5ba4089 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -229,6 +229,7 @@ def __init__( allow_unpack: bool = False, report_invalid_types: bool = True, prohibit_self_type: str | None = None, + prohibit_special_class_field_types: str | None = None, allowed_alias_tvars: list[TypeVarLikeType] | None = None, allow_type_any: bool = False, alias_type_params_names: list[str] | None = None, @@ -275,6 +276,8 @@ def __init__( # Names of type aliases encountered while analysing a type will be collected here. self.aliases_used: set[str] = set() self.prohibit_self_type = prohibit_self_type + # Set when we analyze TypedDicts or NamedTuples, since they are special: + self.prohibit_special_class_field_types = prohibit_special_class_field_types # Allow variables typed as Type[Any] and type (useful for base classes). self.allow_type_any = allow_type_any self.allow_type_var_tuple = False @@ -596,11 +599,18 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ elif fullname == "typing.Any" or fullname == "builtins.Any": return AnyType(TypeOfAny.explicit, line=t.line, column=t.column) elif fullname in FINAL_TYPE_NAMES: - self.fail( - "Final can be only used as an outermost qualifier in a variable annotation", - t, - code=codes.VALID_TYPE, - ) + if self.prohibit_special_class_field_types: + self.fail( + f"Final[...] can't be used inside a {self.prohibit_special_class_field_types}", + t, + 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, + ) return AnyType(TypeOfAny.from_error) elif fullname == "typing.Tuple" or ( fullname == "builtins.tuple" @@ -668,6 +678,12 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ self.fail( "Invalid type: ClassVar nested inside other type", t, code=codes.VALID_TYPE ) + if self.prohibit_special_class_field_types: + self.fail( + f"ClassVar[...] can't be used inside a {self.prohibit_special_class_field_types}", + t, + code=codes.VALID_TYPE, + ) if len(t.args) == 0: return AnyType(TypeOfAny.from_omitted_generics, line=t.line, column=t.column) if len(t.args) != 1: diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index df2c7ffc8067..566b5ef57350 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -1441,3 +1441,22 @@ def bar() -> None: misspelled_var_name # E: Name "misspelled_var_name" is not defined [builtins fixtures/tuple.pyi] [typing fixtures/typing-namedtuple.pyi] + + +[case testNamedTupleFinalAndClassVar] +from typing import NamedTuple, Final, ClassVar + +class My(NamedTuple): + a: Final # E: Final[...] can't be used inside a NamedTuple + b: Final[int] # E: Final[...] can't be used inside a NamedTuple + c: ClassVar # E: ClassVar[...] can't be used inside a NamedTuple + d: ClassVar[int] # E: ClassVar[...] can't be used inside a NamedTuple + +Func = NamedTuple('Func', [ + ('a', Final), # E: Final[...] can't be used inside a NamedTuple + ('b', Final[int]), # E: Final[...] can't be used inside a NamedTuple + ('c', ClassVar), # E: ClassVar[...] can't be used inside a NamedTuple + ('d', ClassVar[int]), # E: ClassVar[...] can't be used inside a NamedTuple +]) +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-namedtuple.pyi] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index a30fec1b9422..6a86dd63a3cd 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -4053,3 +4053,22 @@ d: D = {"a": 1, "b": "x"} c: C = d # E: Incompatible types in assignment (expression has type "D", variable has type "C") [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + + +[case testTypedDictFinalAndClassVar] +from typing import TypedDict, Final, ClassVar + +class My(TypedDict): + a: Final # E: Final[...] can't be used inside a TypedDict + b: Final[int] # E: Final[...] can't be used inside a TypedDict + c: ClassVar # E: ClassVar[...] can't be used inside a TypedDict + d: ClassVar[int] # E: ClassVar[...] can't be used inside a TypedDict + +Func = TypedDict('Func', { + 'a': Final, # E: Final[...] can't be used inside a TypedDict + 'b': Final[int], # E: Final[...] can't be used inside a TypedDict + 'c': ClassVar, # E: ClassVar[...] can't be used inside a TypedDict + 'd': ClassVar[int], # E: ClassVar[...] can't be used inside a TypedDict +}) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] diff --git a/test-data/unit/fixtures/typing-namedtuple.pyi b/test-data/unit/fixtures/typing-namedtuple.pyi index bcdcfc44c3d2..fbb4e43b62e6 100644 --- a/test-data/unit/fixtures/typing-namedtuple.pyi +++ b/test-data/unit/fixtures/typing-namedtuple.pyi @@ -8,6 +8,7 @@ Optional = 0 Self = 0 Tuple = 0 ClassVar = 0 +Final = 0 T = TypeVar('T') T_co = TypeVar('T_co', covariant=True) diff --git a/test-data/unit/fixtures/typing-typeddict.pyi b/test-data/unit/fixtures/typing-typeddict.pyi index 7e9c642cf261..a54dc8bcfa94 100644 --- a/test-data/unit/fixtures/typing-typeddict.pyi +++ b/test-data/unit/fixtures/typing-typeddict.pyi @@ -28,6 +28,7 @@ Required = 0 NotRequired = 0 ReadOnly = 0 Self = 0 +ClassVar = 0 T = TypeVar('T') T_co = TypeVar('T_co', covariant=True)