Skip to content

Commit

Permalink
Do not allow ClassVar and Final in TypedDict and NamedTuple (p…
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolevn authored Dec 12, 2024
1 parent 40730c9 commit bec5cad
Show file tree
Hide file tree
Showing 10 changed files with 80 additions and 7 deletions.
5 changes: 4 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
11 changes: 10 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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())
Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions mypy/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions mypy/semanal_typeddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
26 changes: 21 additions & 5 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
19 changes: 19 additions & 0 deletions test-data/unit/check-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
19 changes: 19 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
1 change: 1 addition & 0 deletions test-data/unit/fixtures/typing-namedtuple.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Optional = 0
Self = 0
Tuple = 0
ClassVar = 0
Final = 0

T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/typing-typeddict.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Required = 0
NotRequired = 0
ReadOnly = 0
Self = 0
ClassVar = 0

T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
Expand Down

0 comments on commit bec5cad

Please sign in to comment.