Skip to content

Commit b899682

Browse files
authored
Merge pull request #1599 from google/google_sync
Google sync
2 parents c225530 + 02a4afd commit b899682

21 files changed

Lines changed: 1214 additions & 470 deletions

pylintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ disable=
8080
use-dict-literal,
8181
useless-import-alias,
8282
useless-object-inheritance,
83+
useless-return,
8384
useless-super-delegation,
8485
wrong-import-order,
8586

pytype/CMakeLists.txt

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ py_library(
5757
DEPS
5858
._utils
5959
.debug
60-
.matcher
60+
.error_printer
61+
.pretty_printer
6162
pytype.abstract.abstract
6263
pytype.overlays.overlays
6364
pytype.pytd.pytd
@@ -114,6 +115,31 @@ py_library(
114115
pytype.directors.directors
115116
)
116117

118+
py_library(
119+
NAME
120+
pretty_printer
121+
SRCS
122+
pretty_printer.py
123+
DEPS
124+
pytype.abstract.abstract
125+
pytype.pytd.pytd
126+
pytype.typegraph.cfg
127+
)
128+
129+
py_library(
130+
NAME
131+
error_printer
132+
SRCS
133+
error_printer.py
134+
DEPS
135+
.matcher
136+
.pretty_printer
137+
pytype.abstract.abstract
138+
pytype.overlays.overlays
139+
pytype.pytd.pytd
140+
pytype.typegraph.cfg
141+
)
142+
117143
filegroup(
118144
NAME
119145
test_data
@@ -719,6 +745,19 @@ py_test(
719745
pytype.typegraph.cfg
720746
)
721747

748+
py_test(
749+
NAME
750+
pretty_printer_test
751+
SRCS
752+
pretty_printer_test.py
753+
DEPS
754+
.config
755+
.pretty_printer
756+
pytype.abstract.abstract
757+
pytype.pytd.pytd
758+
pytype.tests.test_base
759+
)
760+
722761
py_library(
723762
NAME
724763
pytype_main_deps

pytype/abstract/abstract_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,9 @@ def show_constant(val: _BaseValueType) -> str:
900900
Returns:
901901
A string of the pretty-printed constant.
902902
"""
903+
# TODO(mdemello): This is duplicated in pretty_printer.py. Modules within
904+
# abstract/ need to use the version in abstract_utils until we rework the code
905+
# structure a bit.
903906
def _ellipsis_printer(v):
904907
if _isinstance(v, "PythonConstant"):
905908
return v.str_of_constant(_ellipsis_printer)

pytype/error_printer.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"""A printer for human-readable output of error messages."""
2+
3+
import collections
4+
import dataclasses
5+
6+
from typing import List
7+
8+
from pytype import matcher
9+
from pytype import pretty_printer
10+
from pytype.abstract import abstract
11+
from pytype.abstract import function
12+
from pytype.overlays import typed_dict as typed_dict_overlay
13+
from pytype.pytd import pytd_utils
14+
from pytype.typegraph import cfg
15+
16+
17+
@dataclasses.dataclass
18+
class BadReturn:
19+
expected: str
20+
bad_actual: str
21+
full_actual: str
22+
error_details: List[str]
23+
24+
25+
@dataclasses.dataclass
26+
class BadCall:
27+
expected: str
28+
actual: str
29+
error_details: List[str]
30+
31+
32+
class BadCallPrinter:
33+
"""Print the details of an abstract.function.BadCall."""
34+
35+
def __init__(
36+
self, pp: pretty_printer.PrettyPrinter, bad_call: function.BadCall
37+
):
38+
self.bad_call = bad_call
39+
self._pp = pp
40+
41+
def _iter_sig(self):
42+
"""Iterate through a function.Signature object. Focus on a bad parameter."""
43+
sig = self.bad_call.sig
44+
for name in sig.posonly_params:
45+
yield "", name
46+
if sig.posonly_params:
47+
yield ("/", "")
48+
for name in sig.param_names[sig.posonly_count:]:
49+
yield "", name
50+
if sig.varargs_name is not None:
51+
yield "*", sig.varargs_name
52+
elif sig.kwonly_params:
53+
yield ("*", "")
54+
for name in sorted(sig.kwonly_params):
55+
yield "", name
56+
if sig.kwargs_name is not None:
57+
yield "**", sig.kwargs_name
58+
59+
def _iter_expected(self):
60+
"""Yield the prefix, name and type information for expected parameters."""
61+
bad_param = self.bad_call.bad_param
62+
sig = self.bad_call.sig
63+
for prefix, name in self._iter_sig():
64+
suffix = " = ..." if name in sig.defaults else ""
65+
if bad_param and name == bad_param.name:
66+
type_str = self._pp.print_as_expected_type(bad_param.typ)
67+
suffix = ": " + type_str + suffix
68+
yield prefix, name, suffix
69+
70+
def _iter_actual(self, literal):
71+
"""Yield the prefix, name and type information for actual parameters."""
72+
# We want to display the passed_args in the order they're defined in the
73+
# signature, unless there are starargs or starstarargs.
74+
# Map param names to their position in the list, then sort the list of
75+
# passed args so it's in the same order as the params.
76+
sig = self.bad_call.sig
77+
passed_args = self.bad_call.passed_args
78+
bad_param = self.bad_call.bad_param
79+
keys = {param: n for n, (_, param) in enumerate(self._iter_sig())}
80+
def key_f(arg):
81+
arg_name = arg[0]
82+
# starargs are given anonymous names, which won't be found in the sig.
83+
# Instead, use the same name as the varags param itself, if present.
84+
if arg_name not in keys and pytd_utils.ANON_PARAM.match(arg_name):
85+
return keys.get(sig.varargs_name, len(keys)+1)
86+
return keys.get(arg_name, len(keys)+1)
87+
for name, arg in sorted(passed_args, key=key_f):
88+
if bad_param and name == bad_param.name:
89+
suffix = ": " + self._pp.print_as_actual_type(arg, literal=literal)
90+
else:
91+
suffix = ""
92+
yield "", name, suffix
93+
94+
def _print_args(self, arg_iter):
95+
"""Pretty-print a list of arguments. Focus on a bad parameter."""
96+
# (foo, bar, broken : type, ...)
97+
bad_param = self.bad_call.bad_param
98+
printed_params = []
99+
found = False
100+
for prefix, name, suffix in arg_iter:
101+
if bad_param and name == bad_param.name:
102+
printed_params.append(prefix + name + suffix)
103+
found = True
104+
elif found:
105+
printed_params.append("...")
106+
break
107+
elif pytd_utils.ANON_PARAM.match(name):
108+
printed_params.append(prefix + "_")
109+
else:
110+
printed_params.append(prefix + name)
111+
return ", ".join(printed_params)
112+
113+
def print_call_details(self):
114+
bad_param = self.bad_call.bad_param
115+
expected = self._print_args(self._iter_expected())
116+
literal = "Literal[" in expected
117+
actual = self._print_args(self._iter_actual(literal))
118+
if bad_param and bad_param.error_details:
119+
mp = MatcherErrorPrinter(self._pp)
120+
details = mp.print_error_details(bad_param.error_details)
121+
else:
122+
details = []
123+
return BadCall(expected, actual, details)
124+
125+
126+
class MatcherErrorPrinter:
127+
"""Pretty printer for some specific matcher error types."""
128+
129+
def __init__(self, pp: pretty_printer.PrettyPrinter):
130+
self._pp = pp
131+
132+
def _print_protocol_error(self, error: matcher.ProtocolError) -> str:
133+
"""Pretty-print the protocol error."""
134+
convert = error.left_type.ctx.pytd_convert
135+
with convert.set_output_mode(convert.OutputMode.DETAILED):
136+
left = self._pp.print_pytd(error.left_type.get_instance_type())
137+
protocol = self._pp.print_pytd(error.other_type.get_instance_type())
138+
if isinstance(error, matcher.ProtocolMissingAttributesError):
139+
missing = ", ".join(sorted(error.missing))
140+
return (f"Attributes of protocol {protocol} are not implemented on "
141+
f"{left}: {missing}")
142+
else:
143+
assert isinstance(error, matcher.ProtocolTypeError)
144+
actual, expected = error.actual_type, error.expected_type
145+
if (isinstance(actual, abstract.Function) and
146+
isinstance(expected, abstract.Function)):
147+
# TODO(b/196434939): When matching a protocol like Sequence[int] the
148+
# protocol name will be Sequence[int] but the method signatures will be
149+
# displayed as f(self: Sequence[_T], ...).
150+
actual = self._pp.print_as_function_def(actual)
151+
expected = self._pp.print_as_function_def(expected)
152+
return (f"\nMethod {error.attribute_name} of protocol {protocol} has "
153+
f"the wrong signature in {left}:\n\n"
154+
f">> {protocol} expects:\n{expected}\n\n"
155+
f">> {left} defines:\n{actual}")
156+
else:
157+
with convert.set_output_mode(convert.OutputMode.DETAILED):
158+
actual = self._pp.print_pytd(error.actual_type.to_type())
159+
expected = self._pp.print_pytd(error.expected_type.to_type())
160+
return (f"Attribute {error.attribute_name} of protocol {protocol} has "
161+
f"wrong type in {left}: expected {expected}, got {actual}")
162+
163+
def _print_noniterable_str_error(self, error) -> str:
164+
"""Pretty-print the matcher.NonIterableStrError instance."""
165+
return (
166+
f"Note: {error.left_type.name} does not match string iterables by "
167+
"default. Learn more: https://github.com/google/pytype/blob/main/docs/faq.md#why-doesnt-str-match-against-string-iterables")
168+
169+
def _print_typed_dict_error(self, error) -> str:
170+
"""Pretty-print the matcher.TypedDictError instance."""
171+
ret = ""
172+
if error.missing:
173+
ret += "\nTypedDict missing keys: " + ", ".join(error.missing)
174+
if error.extra:
175+
ret += "\nTypedDict extra keys: " + ", ".join(error.extra)
176+
if error.bad:
177+
ret += "\nTypedDict type errors: "
178+
for k, bad in error.bad:
179+
for match in bad:
180+
actual = self._pp.print_as_actual_type(match.actual_binding.data)
181+
expected = self._pp.print_as_expected_type(match.expected.typ)
182+
ret += f"\n {{'{k}': ...}}: expected {expected}, got {actual}"
183+
return ret
184+
185+
def print_error_details(
186+
self, error_details: matcher.ErrorDetails
187+
) -> List[str]:
188+
printers = [
189+
(error_details.protocol, self._print_protocol_error),
190+
(error_details.noniterable_str, self._print_noniterable_str_error),
191+
(error_details.typed_dict, self._print_typed_dict_error)
192+
]
193+
return ["\n" + printer(err) if err else "" for err, printer in printers]
194+
195+
def prepare_errorlog_details(
196+
self, bad: List[matcher.BadMatch]
197+
) -> List[str]:
198+
"""Prepare printable annotation matching errors."""
199+
details = collections.defaultdict(set)
200+
for match in bad:
201+
d = self.print_error_details(match.error_details)
202+
for i, detail in enumerate(d):
203+
if detail:
204+
details[i].add(detail)
205+
ret = []
206+
for i in sorted(details.keys()):
207+
ret.extend(sorted(details[i]))
208+
return ret
209+
210+
def print_return_types(
211+
self, node: cfg.CFGNode, bad: List[matcher.BadMatch]
212+
) -> BadReturn:
213+
"""Print the actual and expected values for a return type."""
214+
formal = bad[0].expected.typ
215+
convert = formal.ctx.pytd_convert
216+
with convert.set_output_mode(convert.OutputMode.DETAILED):
217+
expected = self._pp.print_pytd(formal.get_instance_type(node))
218+
if isinstance(formal, typed_dict_overlay.TypedDictClass):
219+
expected = expected + "(TypedDict)"
220+
if "Literal[" in expected:
221+
output_mode = convert.OutputMode.LITERAL
222+
else:
223+
output_mode = convert.OutputMode.DETAILED
224+
with convert.set_output_mode(output_mode):
225+
bad_actual = self._pp.print_pytd(pytd_utils.JoinTypes(
226+
match.actual_binding.data.to_type(node, view=match.view)
227+
for match in bad))
228+
actual = bad[0].actual
229+
if len(actual.bindings) > len(bad):
230+
full_actual = self._pp.print_pytd(pytd_utils.JoinTypes(
231+
v.to_type(node) for v in actual.data))
232+
else:
233+
full_actual = bad_actual
234+
# typing.Never is a prettier alias for nothing.
235+
fmt = lambda ret: "Never" if ret == "nothing" else ret
236+
error_details = self.prepare_errorlog_details(bad)
237+
return BadReturn(
238+
fmt(expected), fmt(bad_actual), fmt(full_actual), error_details
239+
)

0 commit comments

Comments
 (0)