Skip to content

Commit b3b9790

Browse files
authored
Change reveal_type() representation of TypedDicts (#3598)
Change the formatting to be closer to the TypedDict definition syntax and preserve TypedDict name when constructing using TD(...) for better reveal_type output. Note that this uses a new representation `'key'?: type` for non-required keys. Fixes #3590.
1 parent b7636cb commit b3b9790

File tree

9 files changed

+125
-106
lines changed

9 files changed

+125
-106
lines changed

docs/source/kinds_of_types.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,11 @@ value of type ``Awaitable[T]``:
10271027
my_coroutine = countdown_1("Millennium Falcon", 5)
10281028
reveal_type(my_coroutine) # has type 'Awaitable[str]'
10291029
1030+
.. note::
1031+
1032+
:ref:`reveal_type() <reveal-type>` displays the inferred static type of
1033+
an expression.
1034+
10301035
If you want to use coroutines in older versions of Python that do not support
10311036
the ``async def`` syntax, you can instead use the ``@asyncio.coroutine``
10321037
decorator to convert a generator into a coroutine.
@@ -1284,6 +1289,14 @@ just need to be careful with it, as it could result in a ``KeyError``.
12841289
Requiring ``get()`` everywhere would be too cumbersome. (Note that you
12851290
are free to use ``get()`` with total TypedDicts as well.)
12861291

1292+
Keys that aren't required are shown with a ``?`` in error messages:
1293+
1294+
.. code-block:: python
1295+
1296+
# Revealed type is 'TypedDict('GuiOptions', {'language'?: builtins.str,
1297+
# 'color'?: builtins.str})'
1298+
reveal_type(options)
1299+
12871300
Totality also affects structural compatibility. You can't use a partial
12881301
TypedDict when a total one is expected. Also, a total typed dict is not
12891302
valid when a partial one is expected.

mypy/checkexpr.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,10 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type:
188188
return self.accept(e.analyzed, self.type_context[-1])
189189
if isinstance(e.callee, NameExpr) and isinstance(e.callee.node, TypeInfo) and \
190190
e.callee.node.typeddict_type is not None:
191-
return self.check_typeddict_call(e.callee.node.typeddict_type,
192-
e.arg_kinds, e.arg_names, e.args, e)
191+
# Use named fallback for better error messages.
192+
typeddict_type = e.callee.node.typeddict_type.copy_modified(
193+
fallback=Instance(e.callee.node, []))
194+
return self.check_typeddict_call(typeddict_type, e.arg_kinds, e.arg_names, e.args, e)
193195
if isinstance(e.callee, NameExpr) and e.callee.name in ('isinstance', 'issubclass'):
194196
for typ in mypy.checker.flatten(e.args[1]):
195197
if isinstance(typ, NameExpr):
@@ -303,7 +305,6 @@ def check_typeddict_call_with_kwargs(self, callee: TypedDictType,
303305
context=context)
304306
return AnyType()
305307

306-
items = OrderedDict() # type: OrderedDict[str, Type]
307308
for (item_name, item_expected_type) in callee.items.items():
308309
if item_name in kwargs:
309310
item_value = kwargs[item_name]
@@ -312,12 +313,8 @@ def check_typeddict_call_with_kwargs(self, callee: TypedDictType,
312313
msg=messages.INCOMPATIBLE_TYPES,
313314
lvalue_name='TypedDict item "{}"'.format(item_name),
314315
rvalue_name='expression')
315-
items[item_name] = item_expected_type
316316

317-
mapping_value_type = join.join_type_list(list(items.values()))
318-
fallback = self.chk.named_generic_type('typing.Mapping',
319-
[self.chk.str_type(), mapping_value_type])
320-
return TypedDictType(items, set(callee.required_keys), fallback)
317+
return callee
321318

322319
# Types and methods that can be used to infer partial types.
323320
item_args = {'builtins.list': ['append'],

mypy/types.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,19 +1533,22 @@ def visit_tuple_type(self, t: TupleType) -> str:
15331533
return 'Tuple[{}]'.format(s)
15341534

15351535
def visit_typeddict_type(self, t: TypedDictType) -> str:
1536-
s = self.keywords_str(t.items.items())
1537-
if t.required_keys == set(t.items):
1538-
keys_str = ''
1539-
elif t.required_keys == set():
1540-
keys_str = ', _total=False'
1541-
else:
1542-
keys_str = ', _required_keys=[{}]'.format(', '.join(sorted(t.required_keys)))
1536+
def item_str(name: str, typ: str) -> str:
1537+
if name in t.required_keys:
1538+
return '{!r}: {}'.format(name, typ)
1539+
else:
1540+
return '{!r}?: {}'.format(name, typ)
1541+
1542+
s = '{' + ', '.join(item_str(name, typ.accept(self))
1543+
for name, typ in t.items.items()) + '}'
1544+
prefix = ''
1545+
suffix = ''
15431546
if t.fallback and t.fallback.type:
1544-
if s == '':
1545-
return 'TypedDict(_fallback={}{})'.format(t.fallback.accept(self), keys_str)
1547+
if t.fallback.type.fullname() != 'typing.Mapping':
1548+
prefix = repr(t.fallback.type.fullname()) + ', '
15461549
else:
1547-
return 'TypedDict({}, _fallback={}{})'.format(s, t.fallback.accept(self), keys_str)
1548-
return 'TypedDict({})'.format(s)
1550+
suffix = ', fallback={}'.format(t.fallback.accept(self))
1551+
return 'TypedDict({}{}{})'.format(prefix, s, suffix)
15491552

15501553
def visit_star_type(self, t: StarType) -> str:
15511554
s = t.type.accept(self)
@@ -1580,15 +1583,6 @@ def list_str(self, a: List[Type]) -> str:
15801583
res.append(str(t))
15811584
return ', '.join(res)
15821585

1583-
def keywords_str(self, a: Iterable[Tuple[str, Type]]) -> str:
1584-
"""Convert keywords to strings (pretty-print types)
1585-
and join the results with commas.
1586-
"""
1587-
return ', '.join([
1588-
'{}={}'.format(name, t.accept(self))
1589-
for (name, t) in a
1590-
])
1591-
15921586

15931587
class TypeQuery(SyntheticTypeVisitor[T]):
15941588
"""Visitor for performing queries of types.

test-data/unit/check-classes.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3365,8 +3365,8 @@ x: TD
33653365
x1 = TD({'x': []})
33663366
y: NM
33673367
y1 = NM(x=[])
3368-
reveal_type(x) # E: Revealed type is 'TypedDict(x=builtins.list[Any], _fallback=__main__.TD)'
3369-
reveal_type(x1) # E: Revealed type is 'TypedDict(x=builtins.list[Any], _fallback=typing.Mapping[builtins.str, builtins.list[Any]])'
3368+
reveal_type(x) # E: Revealed type is 'TypedDict('__main__.TD', {'x': builtins.list[Any]})'
3369+
reveal_type(x1) # E: Revealed type is 'TypedDict('__main__.TD', {'x': builtins.list[Any]})'
33703370
reveal_type(y) # E: Revealed type is 'Tuple[builtins.list[Any], fallback=__main__.NM]'
33713371
reveal_type(y1) # E: Revealed type is 'Tuple[builtins.list[Any], fallback=__main__.NM]'
33723372
[builtins fixtures/dict.pyi]

test-data/unit/check-incremental.test

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,11 +1901,11 @@ A = TypedDict('A', {'x': int, 'y': str})
19011901
x: A
19021902
[builtins fixtures/dict.pyi]
19031903
[out1]
1904-
main:2: error: Revealed type is 'TypedDict(x=builtins.int, y=builtins.str, _fallback=b.A)'
1905-
main:4: error: Revealed type is 'TypedDict(x=builtins.int, y=builtins.str, _fallback=b.A)'
1904+
main:2: error: Revealed type is 'TypedDict('b.A', {'x': builtins.int, 'y': builtins.str})'
1905+
main:4: error: Revealed type is 'TypedDict('b.A', {'x': builtins.int, 'y': builtins.str})'
19061906
[out2]
1907-
main:2: error: Revealed type is 'TypedDict(x=builtins.int, y=builtins.str, _fallback=b.A)'
1908-
main:4: error: Revealed type is 'TypedDict(x=builtins.int, y=builtins.str, _fallback=b.A)'
1907+
main:2: error: Revealed type is 'TypedDict('b.A', {'x': builtins.int, 'y': builtins.str})'
1908+
main:4: error: Revealed type is 'TypedDict('b.A', {'x': builtins.int, 'y': builtins.str})'
19091909

19101910
[case testSerializeMetaclass]
19111911
import b

test-data/unit/check-serialize.test

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,12 +1018,12 @@ class C:
10181018
self.c = A
10191019
[builtins fixtures/dict.pyi]
10201020
[out1]
1021-
main:2: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])'
1022-
main:3: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=ntcrash.C.A@4)'
1021+
main:2: error: Revealed type is 'TypedDict('ntcrash.C.A@4', {'x': builtins.int})'
1022+
main:3: error: Revealed type is 'TypedDict('ntcrash.C.A@4', {'x': builtins.int})'
10231023
main:4: error: Revealed type is 'def () -> ntcrash.C.A@4'
10241024
[out2]
1025-
main:2: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])'
1026-
main:3: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=ntcrash.C.A@4)'
1025+
main:2: error: Revealed type is 'TypedDict('ntcrash.C.A@4', {'x': builtins.int})'
1026+
main:3: error: Revealed type is 'TypedDict('ntcrash.C.A@4', {'x': builtins.int})'
10271027
main:4: error: Revealed type is 'def () -> ntcrash.C.A@4'
10281028

10291029
[case testSerializeNonTotalTypedDict]
@@ -1035,9 +1035,9 @@ D = TypedDict('D', {'x': int, 'y': str}, total=False)
10351035
d: D
10361036
[builtins fixtures/dict.pyi]
10371037
[out1]
1038-
main:2: error: Revealed type is 'TypedDict(x=builtins.int, y=builtins.str, _fallback=m.D, _total=False)'
1038+
main:2: error: Revealed type is 'TypedDict('m.D', {'x'?: builtins.int, 'y'?: builtins.str})'
10391039
[out2]
1040-
main:2: error: Revealed type is 'TypedDict(x=builtins.int, y=builtins.str, _fallback=m.D, _total=False)'
1040+
main:2: error: Revealed type is 'TypedDict('m.D', {'x'?: builtins.int, 'y'?: builtins.str})'
10411041

10421042
--
10431043
-- Modules

0 commit comments

Comments
 (0)