Skip to content

Commit 94b93eb

Browse files
committed
Restrict output of inspect() function
Particularly, trim recursive or overly nested structures. Replicates some ideas from graphql/graphql-js#1771 but truncates strings like in pytest, and puts an ellipsis in the middle of long lists, dicts, sets etc. This is done to avoid server memory and performance issues.
1 parent 5e2e538 commit 94b93eb

File tree

3 files changed

+268
-57
lines changed

3 files changed

+268
-57
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ a query language for APIs created by Facebook.
1313
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
1414

1515
The current version 1.0.1 of GraphQL-core-next is up-to-date with GraphQL.js version
16-
14.1.1. All parts of the API are covered by an extensive test suite of currently 1701
16+
14.1.1. All parts of the API are covered by an extensive test suite of currently 1723
1717
unit tests.
1818

1919

graphql/pyutils/inspect.py

+96-29
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,89 @@
99
isasyncgenfunction,
1010
isasyncgen,
1111
)
12-
from typing import Any
12+
from typing import Any, List
1313

1414
from ..error import INVALID
1515

16+
__all__ = ["inspect"]
1617

17-
def inspect(value: Any) -> str:
18+
19+
def inspect(value: Any, max_depth: int = 2, depth: int = 0) -> str:
1820
"""Inspect value and a return string representation for error messages.
1921
2022
Used to print values in error messages. We do not use repr() in order to not
2123
leak too much of the inner Python representation of unknown objects, and we
2224
do not use json.dumps() because not all objects can be serialized as JSON and
2325
we want to output strings with single quotes like Python repr() does it.
26+
27+
We also restrict the size of the representation by truncating strings and
28+
collections and allowing only a maximum recursion depth.
2429
"""
25-
if isinstance(value, (bool, int, float, str)) or value in (None, INVALID):
30+
if value is None or value is INVALID or isinstance(value, (bool, float, complex)):
2631
return repr(value)
27-
# check if we have a custom inspect method
28-
try:
29-
inspect_method = value.__inspect__
30-
if callable(inspect_method):
31-
value = inspect_method()
32-
return value if isinstance(value, str) else inspect(value)
33-
except AttributeError:
34-
pass
35-
if isinstance(value, list):
36-
return f"[{', '.join(map(inspect, value))}]"
37-
if isinstance(value, tuple):
38-
if len(value) == 1:
39-
return f"({inspect(value[0])},)"
40-
return f"({', '.join(map(inspect, value))})"
41-
if isinstance(value, dict):
42-
return (
43-
"{"
44-
+ ", ".join(
45-
map(lambda i: f"{inspect(i[0])}: {inspect(i[1])}", value.items())
46-
)
47-
+ "}"
48-
)
49-
if isinstance(value, set):
50-
if not len(value):
51-
return "<empty set>"
52-
return "{" + ", ".join(map(inspect, value)) + "}"
32+
if isinstance(value, (int, str, bytes, bytearray)):
33+
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
47+
if isinstance(value, (list, tuple, dict, set, frozenset)):
48+
if not value:
49+
return repr(value)
50+
if isinstance(value, list):
51+
items = value
52+
elif isinstance(value, dict):
53+
items = list(value.items())
54+
else:
55+
items = list(value)
56+
items = trunc_list(items)
57+
depth += 1
58+
if isinstance(value, dict):
59+
s = ", ".join(
60+
"..."
61+
if v is ELLIPSIS
62+
else inspect(v[0], max_depth, depth)
63+
+ ": "
64+
+ inspect(v[1], max_depth, depth)
65+
for v in items
66+
)
67+
else:
68+
s = ", ".join(
69+
"..." if v is ELLIPSIS else inspect(v, max_depth, depth)
70+
for v in items
71+
)
72+
if isinstance(value, tuple):
73+
if len(items) == 1:
74+
return f"({s},)"
75+
return f"({s})"
76+
if isinstance(value, (dict, set)):
77+
return "{" + s + "}"
78+
if isinstance(value, frozenset):
79+
return f"frozenset({{{s}}})"
80+
return f"[{s}]"
81+
else:
82+
if isinstance(value, (list, tuple, dict, set, frozenset)):
83+
if not value:
84+
return repr(value)
85+
if isinstance(value, list):
86+
return "[...]"
87+
if isinstance(value, tuple):
88+
return "(...)"
89+
if isinstance(value, dict):
90+
return "{...}"
91+
if isinstance(value, set):
92+
return "set(...)"
93+
if isinstance(value, frozenset):
94+
return "frozenset(...)"
5395
if isinstance(value, Exception):
5496
type_ = "exception"
5597
value = type(value)
@@ -95,3 +137,28 @@ def inspect(value: Any) -> str:
95137
return f"<{type_}>"
96138
else:
97139
return f"<{type_} {name}>"
140+
141+
142+
def trunc_str(s: str, max_string=240) -> str:
143+
"""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)
147+
s = s[:i] + "..." + s[-j:]
148+
return s
149+
150+
151+
def trunc_list(s: List, max_list=10) -> List:
152+
"""Truncate lists to maximum length."""
153+
if len(s) > max_list:
154+
i = max_list // 2
155+
j = i - 1
156+
s = s[:i] + [ELLIPSIS] + s[-j:]
157+
return s
158+
159+
160+
class InspectEllipsisType:
161+
"""Singleton class for indicating ellipses in sequences."""
162+
163+
164+
ELLIPSIS = InspectEllipsisType()

0 commit comments

Comments
 (0)