Skip to content

Commit

Permalink
Make TypedDict instance callable
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p authored and Pierre-Sassoulas committed Jul 19, 2021
1 parent a7e55b4 commit b25c6d8
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 6 deletions.
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Release date: TBA

Closes PyCQA/pylint#4685

* Fix issue that ``TypedDict`` instance wasn't callable.

Closes PyCQA/pylint#4715


What's New in astroid 2.6.2?
============================
Expand Down
20 changes: 17 additions & 3 deletions astroid/brain/brain_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from functools import partial

from astroid import context, extract_node, inference_tip, node_classes
from astroid.const import PY37_PLUS, PY39_PLUS
from astroid.const import PY37_PLUS, PY38_PLUS, PY39_PLUS
from astroid.exceptions import (
AttributeInferenceError,
InferenceError,
Expand Down Expand Up @@ -185,10 +185,18 @@ def infer_typing_attr(


def _looks_like_typedDict( # pylint: disable=invalid-name
node: FunctionDef,
node: typing.Union[FunctionDef, ClassDef],
) -> bool:
"""Check if node is TypedDict FunctionDef."""
return isinstance(node, FunctionDef) and node.name == "TypedDict"
return node.qname() in ("typing.TypedDict", "typing_extensions.TypedDict")


def infer_old_typedDict( # pylint: disable=invalid-name
node: ClassDef, ctx: context.InferenceContext = None
) -> typing.Iterator[ClassDef]:
func_to_add = extract_node("dict")
node.locals["__call__"] = [func_to_add]
return iter([node])


def infer_typedDict( # pylint: disable=invalid-name
Expand All @@ -202,6 +210,8 @@ def infer_typedDict( # pylint: disable=invalid-name
parent=node.parent,
)
class_def.postinit(bases=[extract_node("dict")], body=[], decorators=None)
func_to_add = extract_node("dict")
class_def.locals["__call__"] = [func_to_add]
return iter([class_def])


Expand Down Expand Up @@ -364,6 +374,10 @@ def infer_tuple_alias(
AstroidManager().register_transform(
FunctionDef, inference_tip(infer_typedDict), _looks_like_typedDict
)
elif PY38_PLUS:
AstroidManager().register_transform(
ClassDef, inference_tip(infer_old_typedDict), _looks_like_typedDict
)

if PY37_PLUS:
AstroidManager().register_transform(
Expand Down
15 changes: 12 additions & 3 deletions tests/unittest_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1613,19 +1613,28 @@ def test_typing_namedtuple_dont_crash_on_no_fields(self):

@test_utils.require_version("3.8")
def test_typed_dict(self):
node = builder.extract_node(
code = builder.extract_node(
"""
from typing import TypedDict
class CustomTD(TypedDict):
class CustomTD(TypedDict): #@
var: int
CustomTD(var=1) #@
"""
)
inferred_base = next(node.bases[0].infer())
inferred_base = next(code[0].bases[0].infer())
assert isinstance(inferred_base, nodes.ClassDef)
assert inferred_base.qname() == "typing.TypedDict"
typedDict_base = next(inferred_base.bases[0].infer())
assert typedDict_base.qname() == "builtins.dict"

# Test TypedDict has `__call__` method
local_call = inferred_base.locals.get("__call__", None)
assert local_call and len(local_call) == 1
assert isinstance(local_call[0], nodes.Name) and local_call[0].name == "dict"

# Test TypedDict instance is callable
assert next(code[1].infer()).callable() is True

@test_utils.require_version(minver="3.7")
def test_typing_alias_type(self):
"""
Expand Down

0 comments on commit b25c6d8

Please sign in to comment.