Skip to content

Commit

Permalink
Allow @final on TypedDict (#13557)
Browse files Browse the repository at this point in the history
Allow a `TypedDict` to be decorated with `@final`. Like a regular class,
mypy will emit an error if a final `TypedDict` is subclassed.

Allow `@final` to be applied to a `TypedDict`, and have mypy emit an
error if class is derived from a final `TypedDict`. This goes some way
towards closing #7981 and closing a feature gap with pyright, though not
the whole way, as #7981 also asks for additional type narrowing for a
final `TypedDict`.
  • Loading branch information
godlygeek authored Sep 8, 2022
1 parent cc59b56 commit 4de0caa
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 5 deletions.
4 changes: 2 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1478,8 +1478,8 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool:
for decorator in defn.decorators:
decorator.accept(self)
if isinstance(decorator, RefExpr):
if decorator.fullname in FINAL_DECORATOR_NAMES:
self.fail("@final cannot be used with TypedDict", decorator)
if decorator.fullname in FINAL_DECORATOR_NAMES and info is not None:
info.is_final = True
if info is None:
self.mark_incomplete(defn.name, defn)
else:
Expand Down
5 changes: 4 additions & 1 deletion mypy/semanal_typeddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing_extensions import Final

from mypy import errorcodes as codes
from mypy import errorcodes as codes, message_registry
from mypy.errorcodes import ErrorCode
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
from mypy.messages import MessageBuilder
Expand Down Expand Up @@ -79,6 +79,9 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
self.api.accept(base_expr)
if base_expr.fullname in TPDICT_NAMES or self.is_typeddict(base_expr):
possible = True
if isinstance(base_expr.node, TypeInfo) and base_expr.node.is_final:
err = message_registry.CANNOT_INHERIT_FROM_FINAL
self.fail(err.format(base_expr.node.name).value, defn, code=err.code)
if not possible:
return False, None
existing_info = None
Expand Down
23 changes: 21 additions & 2 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -2012,16 +2012,35 @@ v = {bad2: 2} # E: Extra key "bad" for TypedDict "Value"
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testCannotUseFinalDecoratorWithTypedDict]
[case testCannotSubclassFinalTypedDict]
from typing import TypedDict
from typing_extensions import final

@final # E: @final cannot be used with TypedDict
@final
class DummyTypedDict(TypedDict):
int_val: int
float_val: float
str_val: str

class SubType(DummyTypedDict): # E: Cannot inherit from final class "DummyTypedDict"
pass

[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testCannotSubclassFinalTypedDictWithForwardDeclarations]
from typing import TypedDict
from typing_extensions import final

@final
class DummyTypedDict(TypedDict):
forward_declared: "ForwardDeclared"

class SubType(DummyTypedDict): # E: Cannot inherit from final class "DummyTypedDict"
pass

class ForwardDeclared: pass

[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

Expand Down

0 comments on commit 4de0caa

Please sign in to comment.