Skip to content

Commit 0fa5ff5

Browse files
committed
Add inspect() function to pyutils package (#12)
1 parent 523b818 commit 0fa5ff5

File tree

4 files changed

+228
-1
lines changed

4 files changed

+228
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ a query language for APIs created by Facebook.
1212
[![Python 3 Status](https://pyup.io/repos/github/graphql-python/graphql-core-next/python-3-shield.svg)](https://pyup.io/repos/github/graphql-python/graphql-core-next/)
1313

1414
The current version 1.0.1 of GraphQL-core-next is up-to-date with GraphQL.js version
15-
14.0.2. All parts of the API are covered by an extensive test suite of currently 1635
15+
14.0.2. All parts of the API are covered by an extensive test suite of currently 1655
1616
unit tests.
1717

1818

graphql/pyutils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .contain_subset import contain_subset
1414
from .dedent import dedent
1515
from .event_emitter import EventEmitter, EventEmitterAsyncIterator
16+
from .inspect import inspect
1617
from .is_finite import is_finite
1718
from .is_integer import is_integer
1819
from .is_invalid import is_invalid
@@ -30,6 +31,7 @@
3031
"dedent",
3132
"EventEmitter",
3233
"EventEmitterAsyncIterator",
34+
"inspect",
3335
"is_finite",
3436
"is_integer",
3537
"is_invalid",

graphql/pyutils/inspect.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from inspect import (
2+
isclass,
3+
ismethod,
4+
isfunction,
5+
isgeneratorfunction,
6+
isgenerator,
7+
iscoroutinefunction,
8+
iscoroutine,
9+
isasyncgenfunction,
10+
isasyncgen,
11+
)
12+
from typing import Any
13+
14+
from ..error import INVALID
15+
16+
17+
def inspect(value: Any) -> str:
18+
"""Inspect value and a return string representation for error messages.
19+
20+
Used to print values in error messages. We do not use repr() in order to not
21+
leak too much of the inner Python representation of unknown objects, and we
22+
do not use json.dumps() because no all objects can be serialized as JSON and
23+
we want to output strings with single quotes like Python repr() does it.
24+
"""
25+
if isinstance(value, (bool, int, float, str)) or value in (None, INVALID):
26+
return repr(value)
27+
if isinstance(value, list):
28+
return f"[{', '.join(map(inspect, value))}]"
29+
if isinstance(value, tuple):
30+
if len(value) == 1:
31+
return f"({inspect(value[0])},)"
32+
return f"({', '.join(map(inspect, value))})"
33+
if isinstance(value, dict):
34+
return (
35+
"{"
36+
+ ", ".join(
37+
map(lambda i: f"{inspect(i[0])}: {inspect(i[1])}", value.items())
38+
)
39+
+ "}"
40+
)
41+
if isinstance(value, set):
42+
if not len(value):
43+
return "<empty set>"
44+
return "{" + ", ".join(map(inspect, value)) + "}"
45+
if isinstance(value, Exception):
46+
type_ = "exception"
47+
value = type(value)
48+
elif isclass(value):
49+
type_ = "exception class" if issubclass(value, Exception) else "class"
50+
elif ismethod(value):
51+
type_ = "method"
52+
elif iscoroutinefunction(value):
53+
type_ = "coroutine function"
54+
elif isasyncgenfunction(value):
55+
type_ = "async generator function"
56+
elif isgeneratorfunction(value):
57+
type_ = "generator function"
58+
elif isfunction(value):
59+
type_ = "function"
60+
elif iscoroutine(value):
61+
type_ = "coroutine"
62+
elif isasyncgen(value):
63+
type_ = "async generator"
64+
elif isgenerator(value):
65+
type_ = "generator"
66+
else:
67+
try:
68+
name = type(value).__name__
69+
if not name or "<" in name or ">" in name:
70+
raise AttributeError
71+
except AttributeError:
72+
return "<object>"
73+
else:
74+
return f"<{name} instance>"
75+
try:
76+
name = value.__name__
77+
if not name or "<" in name or ">" in name:
78+
raise AttributeError
79+
except AttributeError:
80+
return f"<{type_}>"
81+
else:
82+
return f"<{type_} {name}>"

tests/pyutils/test_inspect.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from math import nan, inf
2+
3+
from pytest import mark
4+
5+
from graphql.error import INVALID
6+
from graphql.pyutils import inspect
7+
8+
9+
def describe_inspect():
10+
def invalid():
11+
assert inspect(INVALID) == "<INVALID>"
12+
13+
def none():
14+
assert inspect(None) == "None"
15+
16+
def boolean():
17+
assert inspect(True) == "True"
18+
assert inspect(False) == "False"
19+
20+
def string():
21+
assert inspect("") == "''"
22+
assert inspect("abc") == "'abc'"
23+
assert inspect('"') == repr('"')
24+
assert inspect("'") == repr("'")
25+
26+
def number():
27+
assert inspect(0) == "0"
28+
assert inspect(0.0) == "0.0"
29+
assert inspect(314) == "314"
30+
assert inspect(3.14) == "3.14"
31+
assert inspect(nan) == "nan"
32+
assert inspect(inf) == "inf"
33+
assert inspect(-inf) == "-inf"
34+
35+
def function():
36+
assert inspect(lambda: 0) == "<function>"
37+
38+
def test_func():
39+
return None
40+
41+
assert inspect(test_func) == "<function test_func>"
42+
43+
def exception():
44+
assert inspect(ValueError) == "<exception class ValueError>"
45+
assert inspect(ArithmeticError(42)) == "<exception ArithmeticError>"
46+
47+
def class_and_method():
48+
class TestClass:
49+
def test_method(self):
50+
return None
51+
52+
assert inspect(TestClass) == "<class TestClass>"
53+
assert inspect(TestClass()) == "<TestClass instance>"
54+
assert inspect(TestClass.test_method) == "<function test_method>"
55+
assert inspect(TestClass().test_method) == "<method test_method>"
56+
57+
def unknown_object():
58+
class MetaClass(type):
59+
__name__ = None
60+
61+
class TestClass(metaclass=MetaClass):
62+
pass
63+
64+
assert inspect(TestClass()) == "<object>"
65+
66+
def generator():
67+
def test_generator():
68+
yield None
69+
70+
assert inspect(test_generator) == "<generator function test_generator>"
71+
assert inspect(test_generator()) == "<generator test_generator>"
72+
73+
@mark.asyncio
74+
async def coroutine():
75+
async def test_coroutine():
76+
return None
77+
78+
assert inspect(test_coroutine) == "<coroutine function test_coroutine>"
79+
coroutine_object = test_coroutine()
80+
assert inspect(coroutine_object) == "<coroutine test_coroutine>"
81+
await coroutine_object # avoid warning
82+
83+
def async_generator():
84+
async def test_async_generator():
85+
yield None
86+
87+
assert inspect(test_async_generator) == (
88+
"<async generator function test_async_generator>"
89+
)
90+
assert inspect(test_async_generator()) == (
91+
"<async generator test_async_generator>"
92+
)
93+
94+
def lists():
95+
assert inspect([]) == "[]"
96+
assert inspect([None]) == "[None]"
97+
assert inspect([[[None]]]) == "[[[None]]]"
98+
assert inspect([1, nan]) == "[1, nan]"
99+
assert inspect([["a", "b"], "c"]) == "[['a', 'b'], 'c']"
100+
101+
def tuples():
102+
assert inspect(()) == "()"
103+
assert inspect((None,)) == "(None,)"
104+
assert inspect((((None,),),)) == "(((None,),),)"
105+
assert inspect((1, nan)) == "(1, nan)"
106+
assert inspect((("a", "b"), "c")) == "(('a', 'b'), 'c')"
107+
108+
def mixed_lists_and_tuples():
109+
assert inspect(["a", ("b",)]) == "['a', ('b',)]"
110+
111+
def mixed_lists_and_tuples_with_various_objects():
112+
class TestClass:
113+
pass
114+
115+
assert inspect([TestClass, (TestClass,), ValueError()]) == (
116+
"[<class TestClass>, (<class TestClass>,), <exception ValueError>]"
117+
)
118+
119+
def dicts():
120+
assert inspect({}) == "{}"
121+
assert inspect({"a": 1}) == "{'a': 1}"
122+
assert inspect({"a": 1, "b": 2}) == "{'a': 1, 'b': 2}"
123+
assert inspect({"list": [None, 0]}) == "{'list': [None, 0]}"
124+
assert inspect({"a": True, "b": None}) == "{'a': True, 'b': None}"
125+
126+
def sets():
127+
assert inspect(set()) == "<empty set>"
128+
assert inspect({"a"}) == "{'a'}"
129+
assert inspect({"a", 1}) in ("{'a', 1}", "{1, 'a'}") # sets are unordered
130+
131+
def mixed_dicts_and_sets():
132+
assert inspect({"a": {"b"}}) == "{'a': {'b'}}"
133+
assert inspect({1: [], 2: (), 3: set()}) == "{1: [], 2: (), 3: <empty set>}"
134+
assert inspect([(set(),), {None: {()}}]) == "[(<empty set>,), {None: {()}}]"
135+
136+
def mixed_dicts_and_sets_with_various_objects():
137+
class TestClass:
138+
pass
139+
140+
assert inspect({TestClass: {ValueError()}, ValueError: {TestClass()}}) == (
141+
"{<class TestClass>: {<exception ValueError>},"
142+
" <exception class ValueError>: {<TestClass instance>}}"
143+
)

0 commit comments

Comments
 (0)