Skip to content

Commit 223d740

Browse files
committed
Inspect: Detect recursive objects
Replicates graphql/graphql-js@1375776
1 parent 24464ab commit 223d740

File tree

2 files changed

+93
-33
lines changed

2 files changed

+93
-33
lines changed

graphql/pyutils/inspect.py

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515

1616
__all__ = ["inspect"]
1717

18+
max_recursive_depth = 2
19+
max_str_size = 240
20+
max_list_size = 10
1821

19-
def inspect(value: Any, max_depth: int = 2, depth: int = 0) -> str:
22+
23+
def inspect(value: Any) -> str:
2024
"""Inspect value and a return string representation for error messages.
2125
2226
Used to print values in error messages. We do not use repr() in order to not
@@ -27,46 +31,47 @@ def inspect(value: Any, max_depth: int = 2, depth: int = 0) -> str:
2731
We also restrict the size of the representation by truncating strings and
2832
collections and allowing only a maximum recursion depth.
2933
"""
34+
return inspect_recursive(value, [])
35+
36+
37+
def inspect_recursive(value: Any, seen_values: List) -> str:
3038
if value is None or value is INVALID or isinstance(value, (bool, float, complex)):
3139
return repr(value)
3240
if isinstance(value, (int, str, bytes, bytearray)):
3341
return trunc_str(repr(value))
34-
if depth < max_depth:
35-
try:
36-
# check if we have a custom inspect method
37-
inspect_method = value.__inspect__
38-
if callable(inspect_method):
39-
s = inspect_method()
40-
return (
41-
trunc_str(s)
42-
if isinstance(s, str)
43-
else inspect(s, max_depth, depth + 1)
44-
)
45-
except AttributeError:
46-
pass
42+
if len(seen_values) < max_recursive_depth and value not in seen_values:
43+
# check if we have a custom inspect method
44+
inspect_method = getattr(value, "__inspect__", None)
45+
if inspect_method is not None and callable(inspect_method):
46+
s = inspect_method()
47+
if isinstance(s, str):
48+
return trunc_str(s)
49+
seen_values = [*seen_values, value]
50+
return inspect_recursive(s, seen_values)
51+
# recursively inspect collections
4752
if isinstance(value, (list, tuple, dict, set, frozenset)):
4853
if not value:
4954
return repr(value)
55+
seen_values = [*seen_values, value]
5056
if isinstance(value, list):
5157
items = value
5258
elif isinstance(value, dict):
5359
items = list(value.items())
5460
else:
5561
items = list(value)
5662
items = trunc_list(items)
57-
depth += 1
5863
if isinstance(value, dict):
5964
s = ", ".join(
6065
"..."
6166
if v is ELLIPSIS
62-
else inspect(v[0], max_depth, depth)
67+
else inspect_recursive(v[0], seen_values)
6368
+ ": "
64-
+ inspect(v[1], max_depth, depth)
69+
+ inspect_recursive(v[1], seen_values)
6570
for v in items
6671
)
6772
else:
6873
s = ", ".join(
69-
"..." if v is ELLIPSIS else inspect(v, max_depth, depth)
74+
"..." if v is ELLIPSIS else inspect_recursive(v, seen_values)
7075
for v in items
7176
)
7277
if isinstance(value, tuple):
@@ -79,6 +84,7 @@ def inspect(value: Any, max_depth: int = 2, depth: int = 0) -> str:
7984
return f"frozenset({{{s}}})"
8085
return f"[{s}]"
8186
else:
87+
# handle collections that are nested too deep
8288
if isinstance(value, (list, tuple, dict, set, frozenset)):
8389
if not value:
8490
return repr(value)
@@ -139,19 +145,19 @@ def inspect(value: Any, max_depth: int = 2, depth: int = 0) -> str:
139145
return f"<{type_} {name}>"
140146

141147

142-
def trunc_str(s: str, max_string=240) -> str:
148+
def trunc_str(s: str) -> str:
143149
"""Truncate strings to maximum length."""
144-
if len(s) > max_string:
145-
i = max(0, (max_string - 3) // 2)
146-
j = max(0, max_string - 3 - i)
150+
if len(s) > max_str_size:
151+
i = max(0, (max_str_size - 3) // 2)
152+
j = max(0, max_str_size - 3 - i)
147153
s = s[:i] + "..." + s[-j:]
148154
return s
149155

150156

151-
def trunc_list(s: List, max_list=10) -> List:
157+
def trunc_list(s: List) -> List:
152158
"""Truncate lists to maximum length."""
153-
if len(s) > max_list:
154-
i = max_list // 2
159+
if len(s) > max_list_size:
160+
i = max_list_size // 2
155161
j = i - 1
156162
s = s[:i] + [ELLIPSIS] + s[-j:]
157163
return s

tests/pyutils/test_inspect.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from math import nan, inf
2+
from contextlib import contextmanager
3+
from importlib import import_module
24

35
from pytest import mark
46

@@ -13,6 +15,35 @@
1315
GraphQLString,
1416
)
1517

18+
inspect_module = import_module(inspect.__module__)
19+
20+
21+
@contextmanager
22+
def increased_recursive_depth():
23+
inspect_module.max_recursive_depth += 1
24+
try:
25+
yield inspect
26+
finally:
27+
inspect_module.max_recursive_depth -= 1
28+
29+
30+
@contextmanager
31+
def increased_str_size():
32+
inspect_module.max_str_size *= 2
33+
try:
34+
yield inspect
35+
finally:
36+
inspect_module.max_str_size //= 2
37+
38+
39+
@contextmanager
40+
def increased_list_size():
41+
inspect_module.max_list_size *= 2
42+
try:
43+
yield inspect
44+
finally:
45+
inspect_module.max_list_size //= 2
46+
1647

1748
def describe_inspect():
1849
def inspect_invalid():
@@ -33,6 +64,8 @@ def overly_large_string():
3364
s = "foo" * 100
3465
r = repr(s)
3566
assert inspect(s) == r[:118] + "..." + r[-119:]
67+
with increased_str_size():
68+
assert inspect(s) == r
3669

3770
def inspect_bytes():
3871
for b in b"", b"abc", b"foo\tbar \x7f\xff\0", b"'", b"'":
@@ -62,6 +95,8 @@ def overly_large_int():
6295
n = int("123" * 100)
6396
r = repr(n)
6497
assert inspect(n) == r[:118] + "..." + r[-119:]
98+
with increased_str_size():
99+
assert inspect(n) == r
65100

66101
def inspect_function():
67102
assert inspect(lambda: 0) == "<function>"
@@ -132,18 +167,21 @@ def inspect_lists():
132167
def inspect_overly_large_list():
133168
s = list(range(20))
134169
assert inspect(s) == "[0, 1, 2, 3, 4, ..., 16, 17, 18, 19]"
170+
with increased_list_size():
171+
assert inspect(s) == repr(s)
135172

136173
def inspect_overly_nested_list():
137174
s = [[[]]]
138175
assert inspect(s) == "[[[]]]"
139176
s = [[[1, 2, 3]]]
140177
assert inspect(s) == "[[[...]]]"
141-
assert inspect(s, max_depth=3) == repr(s)
178+
with increased_recursive_depth():
179+
assert inspect(s) == repr(s)
142180

143181
def inspect_recursive_list():
144182
s = [1, 2, 3]
145183
s[1] = s
146-
assert inspect(s) == "[1, [1, [...], 3], 3]"
184+
assert inspect(s) == "[1, [...], 3]"
147185

148186
def inspect_tuples():
149187
assert inspect(()) == "()"
@@ -155,13 +193,16 @@ def inspect_tuples():
155193
def inspect_overly_large_tuple():
156194
s = tuple(range(20))
157195
assert inspect(s) == "(0, 1, 2, 3, 4, ..., 16, 17, 18, 19)"
196+
with increased_list_size():
197+
assert inspect(s) == repr(s)
158198

159199
def inspect_overly_nested_tuple():
160200
s = (((),),)
161201
assert inspect(s) == "(((),),)"
162202
s = (((1, 2, 3),),)
163203
assert inspect(s) == "(((...),),)"
164-
assert inspect(s, max_depth=3) == repr(s)
204+
with increased_recursive_depth():
205+
assert inspect(s) == repr(s)
165206

166207
def inspect_recursive_tuple():
167208
s = [1, 2, 3]
@@ -193,18 +234,21 @@ def inspect_overly_large_dict():
193234
inspect(s) == "{'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4,"
194235
" ..., 'q': 16, 'r': 17, 's': 18, 't': 19}"
195236
)
237+
with increased_list_size():
238+
assert inspect(s) == repr(s)
196239

197240
def inspect_overly_nested_dict():
198241
s = {"a": {"b": {}}}
199242
assert inspect(s) == "{'a': {'b': {}}}"
200243
s = {"a": {"b": {"c": 3}}}
201244
assert inspect(s) == "{'a': {'b': {...}}}"
202-
assert inspect(s, max_depth=3) == repr(s)
245+
with increased_recursive_depth():
246+
assert inspect(s) == repr(s)
203247

204248
def inspect_recursive_dict():
205249
s = {}
206250
s[1] = s
207-
assert inspect(s) == "{1: {1: {...}}}"
251+
assert inspect(s) == "{1: {...}}"
208252

209253
def inspect_sets():
210254
assert inspect(set()) == "set()"
@@ -217,13 +261,16 @@ def inspect_overly_large_set():
217261
assert r.startswith("{") and r.endswith("}")
218262
assert "..., " in r and "5" not in s # sets are unordered
219263
assert len(r) == 36
264+
with increased_list_size():
265+
assert inspect(s) == repr(s)
220266

221267
def inspect_overly_nested_set():
222268
s = [[set()]]
223269
assert inspect(s) == "[[set()]]"
224270
s = [[set([1, 2, 3])]]
225271
assert inspect(s) == "[[set(...)]]"
226-
assert inspect(s, max_depth=3) == repr(s)
272+
with increased_recursive_depth():
273+
assert inspect(s) == repr(s)
227274

228275
def inspect_frozensets():
229276
assert inspect(frozenset()) == "frozenset()"
@@ -239,13 +286,16 @@ def inspect_overly_large_frozenset():
239286
assert r.startswith("frozenset({") and r.endswith("})")
240287
assert "..., " in r and "5" not in s # frozensets are unordered
241288
assert len(r) == 47
289+
with increased_list_size():
290+
assert inspect(s) == repr(s)
242291

243292
def inspect_overly_nested_frozenset():
244293
s = frozenset([frozenset([frozenset()])])
245294
assert inspect(s) == "frozenset({frozenset({frozenset()})})"
246295
s = frozenset([frozenset([frozenset([1, 2, 3])])])
247296
assert inspect(s) == "frozenset({frozenset({frozenset(...)})})"
248-
assert inspect(s, max_depth=3) == repr(s)
297+
with increased_recursive_depth():
298+
assert inspect(s) == repr(s)
249299

250300
def mixed_recursive_dict_and_list():
251301
s = {1: []}
@@ -312,7 +362,11 @@ class TestClass:
312362
def __inspect__():
313363
return s
314364

315-
assert inspect(TestClass()) == s[:118] + "..." + s[-119:]
365+
value = TestClass()
366+
367+
assert inspect(value) == s[:118] + "..." + s[-119:]
368+
with increased_str_size():
369+
assert inspect(value) == s
316370

317371
def custom_inspect_that_is_recursive():
318372
class TestClass:

0 commit comments

Comments
 (0)