Skip to content

Commit

Permalink
Add missing TypedDict special case to checkmember.py (#18604)
Browse files Browse the repository at this point in the history
Fixes #18600
  • Loading branch information
sterliakov authored Feb 5, 2025
1 parent 88d6890 commit 6f32ef9
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 16 deletions.
22 changes: 7 additions & 15 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
import mypy.errorcodes as codes
from mypy import applytype, erasetype, join, message_registry, nodes, operators, types
from mypy.argmap import ArgTypeExpander, map_actuals_to_formals, map_formals_to_actuals
from mypy.checkmember import analyze_member_access, freeze_all_type_vars, type_object_type
from mypy.checkmember import (
analyze_member_access,
freeze_all_type_vars,
type_object_type,
typeddict_callable,
)
from mypy.checkstrformat import StringFormatterChecker
from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars
from mypy.errors import ErrorWatcher, report_internal_error
Expand Down Expand Up @@ -955,20 +960,7 @@ def typeddict_callable(self, info: TypeInfo) -> CallableType:
Note it is not safe to move this to type_object_type() since it will crash
on plugin-generated TypedDicts, that may not have the special_alias.
"""
assert info.special_alias is not None
target = info.special_alias.target
assert isinstance(target, ProperType) and isinstance(target, TypedDictType)
expected_types = list(target.items.values())
kinds = [ArgKind.ARG_NAMED] * len(expected_types)
names = list(target.items.keys())
return CallableType(
expected_types,
kinds,
names,
target,
self.named_type("builtins.type"),
variables=info.defn.type_vars,
)
return typeddict_callable(info, self.named_type)

def typeddict_callable_from_context(self, callee: TypedDictType) -> CallableType:
return CallableType(
Expand Down
36 changes: 35 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ARG_STAR2,
EXCLUDED_ENUM_ATTRIBUTES,
SYMBOL_FUNCBASE_TYPES,
ArgKind,
Context,
Decorator,
FuncBase,
Expand Down Expand Up @@ -1148,8 +1149,16 @@ def analyze_class_attribute_access(
)
return AnyType(TypeOfAny.from_error)

# TODO: some logic below duplicates analyze_ref_expr in checkexpr.py
if isinstance(node.node, TypeInfo):
return type_object_type(node.node, mx.named_type)
if node.node.typeddict_type:
# We special-case TypedDict, because they don't define any constructor.
return typeddict_callable(node.node, mx.named_type)
elif node.node.fullname == "types.NoneType":
# We special case NoneType, because its stub definition is not related to None.
return TypeType(NoneType())
else:
return type_object_type(node.node, mx.named_type)

if isinstance(node.node, MypyFile):
# Reference to a module object.
Expand Down Expand Up @@ -1330,6 +1339,31 @@ class B(A[str]): pass
return t


def typeddict_callable(info: TypeInfo, named_type: Callable[[str], Instance]) -> CallableType:
"""Construct a reasonable type for a TypedDict type in runtime context.
If it appears as a callee, it will be special-cased anyway, e.g. it is
also allowed to accept a single positional argument if it is a dict literal.
Note it is not safe to move this to type_object_type() since it will crash
on plugin-generated TypedDicts, that may not have the special_alias.
"""
assert info.special_alias is not None
target = info.special_alias.target
assert isinstance(target, ProperType) and isinstance(target, TypedDictType)
expected_types = list(target.items.values())
kinds = [ArgKind.ARG_NAMED] * len(expected_types)
names = list(target.items.keys())
return CallableType(
expected_types,
kinds,
names,
target,
named_type("builtins.type"),
variables=info.defn.type_vars,
)


def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> ProperType:
"""Return the type of a type object.
Expand Down
20 changes: 20 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -4118,3 +4118,23 @@ Func = TypedDict('Func', {
})
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testTypedDictNestedInClassAndInherited]
from typing_extensions import TypedDict

class Base:
class Params(TypedDict):
name: str

class Derived(Base):
pass

class DerivedOverride(Base):
class Params(Base.Params):
pass

Base.Params(name="Robert")
Derived.Params(name="Robert")
DerivedOverride.Params(name="Robert")
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

0 comments on commit 6f32ef9

Please sign in to comment.