Skip to content

Commit b25c6d8

Browse files
cdce8pPierre-Sassoulas
authored andcommitted
Make TypedDict instance callable
1 parent a7e55b4 commit b25c6d8

File tree

3 files changed

+33
-6
lines changed

3 files changed

+33
-6
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ Release date: TBA
2525

2626
Closes PyCQA/pylint#4685
2727

28+
* Fix issue that ``TypedDict`` instance wasn't callable.
29+
30+
Closes PyCQA/pylint#4715
31+
2832

2933
What's New in astroid 2.6.2?
3034
============================

astroid/brain/brain_typing.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from functools import partial
1515

1616
from astroid import context, extract_node, inference_tip, node_classes
17-
from astroid.const import PY37_PLUS, PY39_PLUS
17+
from astroid.const import PY37_PLUS, PY38_PLUS, PY39_PLUS
1818
from astroid.exceptions import (
1919
AttributeInferenceError,
2020
InferenceError,
@@ -185,10 +185,18 @@ def infer_typing_attr(
185185

186186

187187
def _looks_like_typedDict( # pylint: disable=invalid-name
188-
node: FunctionDef,
188+
node: typing.Union[FunctionDef, ClassDef],
189189
) -> bool:
190190
"""Check if node is TypedDict FunctionDef."""
191-
return isinstance(node, FunctionDef) and node.name == "TypedDict"
191+
return node.qname() in ("typing.TypedDict", "typing_extensions.TypedDict")
192+
193+
194+
def infer_old_typedDict( # pylint: disable=invalid-name
195+
node: ClassDef, ctx: context.InferenceContext = None
196+
) -> typing.Iterator[ClassDef]:
197+
func_to_add = extract_node("dict")
198+
node.locals["__call__"] = [func_to_add]
199+
return iter([node])
192200

193201

194202
def infer_typedDict( # pylint: disable=invalid-name
@@ -202,6 +210,8 @@ def infer_typedDict( # pylint: disable=invalid-name
202210
parent=node.parent,
203211
)
204212
class_def.postinit(bases=[extract_node("dict")], body=[], decorators=None)
213+
func_to_add = extract_node("dict")
214+
class_def.locals["__call__"] = [func_to_add]
205215
return iter([class_def])
206216

207217

@@ -364,6 +374,10 @@ def infer_tuple_alias(
364374
AstroidManager().register_transform(
365375
FunctionDef, inference_tip(infer_typedDict), _looks_like_typedDict
366376
)
377+
elif PY38_PLUS:
378+
AstroidManager().register_transform(
379+
ClassDef, inference_tip(infer_old_typedDict), _looks_like_typedDict
380+
)
367381

368382
if PY37_PLUS:
369383
AstroidManager().register_transform(

tests/unittest_brain.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,19 +1613,28 @@ def test_typing_namedtuple_dont_crash_on_no_fields(self):
16131613

16141614
@test_utils.require_version("3.8")
16151615
def test_typed_dict(self):
1616-
node = builder.extract_node(
1616+
code = builder.extract_node(
16171617
"""
16181618
from typing import TypedDict
1619-
class CustomTD(TypedDict):
1619+
class CustomTD(TypedDict): #@
16201620
var: int
1621+
CustomTD(var=1) #@
16211622
"""
16221623
)
1623-
inferred_base = next(node.bases[0].infer())
1624+
inferred_base = next(code[0].bases[0].infer())
16241625
assert isinstance(inferred_base, nodes.ClassDef)
16251626
assert inferred_base.qname() == "typing.TypedDict"
16261627
typedDict_base = next(inferred_base.bases[0].infer())
16271628
assert typedDict_base.qname() == "builtins.dict"
16281629

1630+
# Test TypedDict has `__call__` method
1631+
local_call = inferred_base.locals.get("__call__", None)
1632+
assert local_call and len(local_call) == 1
1633+
assert isinstance(local_call[0], nodes.Name) and local_call[0].name == "dict"
1634+
1635+
# Test TypedDict instance is callable
1636+
assert next(code[1].infer()).callable() is True
1637+
16291638
@test_utils.require_version(minver="3.7")
16301639
def test_typing_alias_type(self):
16311640
"""

0 commit comments

Comments
 (0)