Skip to content

Allow @final on TypedDict #13557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1469,8 +1469,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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the info is None? Does that happen if there's a circular reference? It would be good to get a test covering that code path.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be FakeInfo for some reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the info is None? Does that happen if there's a circular reference?

When there's a forward declaration in the TypedDict body, analyze_typeddict_classdef gets called multiple times. info is initially None in that case, but eventually it's a TypeInfo.

It would be good to get a test covering that code path.

Done.

It could be FakeInfo for some reason.

When would that happen?

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