Skip to content

Commit e8b3940

Browse files
committed
Add ast_to_dict utility function (#136)
This function existed in the legacy version 2 and may be sometimes useful.
1 parent a3e7b48 commit e8b3940

File tree

7 files changed

+695
-3
lines changed

7 files changed

+695
-3
lines changed

docs/modules/utilities.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ Create a GraphQLType from a GraphQL language AST:
5454

5555
.. autofunction:: type_from_ast
5656

57+
Convert a language AST to a dictionary:
58+
59+
.. autofunction:: ast_to_dict
60+
5761
Create a Python value from a GraphQL language AST with a type:
5862

5963
.. autofunction:: value_from_ast

src/graphql/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@
189189
print_introspection_schema,
190190
# Create a GraphQLType from a GraphQL language AST.
191191
type_from_ast,
192+
# Convert a language AST to a dictionary.
193+
ast_to_dict,
192194
# Create a Python value from a GraphQL language AST with a Type.
193195
value_from_ast,
194196
# Create a Python value from a GraphQL language AST without a Type.
@@ -744,6 +746,7 @@
744746
"value_from_ast",
745747
"value_from_ast_untyped",
746748
"ast_from_value",
749+
"ast_to_dict",
747750
"TypeInfo",
748751
"coerce_input_value",
749752
"concat_ast",

src/graphql/language/ast.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ class Node:
321321
loc: Optional[Location]
322322

323323
kind: str = "ast" # the kind of the node as a snake_case string
324-
keys = ["loc"] # the names of the attributes of this node
324+
keys: Tuple[str, ...] = ("loc",) # the names of the attributes of this node
325325

326326
def __init__(self, **kwargs: Any) -> None:
327327
"""Initialize the node with the given keyword arguments."""
@@ -349,6 +349,7 @@ def __hash__(self) -> int:
349349
# Caching the hash values improves the performance of AST validators
350350
hashed = getattr(self, "_hash", None)
351351
if hashed is None:
352+
self._hash = id(self) # avoid recursion
352353
hashed = hash(tuple(getattr(self, key) for key in self.keys))
353354
self._hash = hashed
354355
return hashed
@@ -386,7 +387,12 @@ def __init_subclass__(cls) -> None:
386387
# noinspection PyUnresolvedReferences
387388
keys.extend(base.keys) # type: ignore
388389
keys.extend(cls.__slots__)
389-
cls.keys = keys
390+
cls.keys = tuple(keys)
391+
392+
def to_dict(self, locations: bool = False) -> Dict:
393+
from ..utilities import ast_to_dict
394+
395+
return ast_to_dict(self, locations)
390396

391397

392398
# Name

src/graphql/utilities/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
# Create a GraphQLType from a GraphQL language AST.
4040
from .type_from_ast import type_from_ast
4141

42+
# Convert a language AST to a dictionary.
43+
from .ast_to_dict import ast_to_dict
44+
4245
# Create a Python value from a GraphQL language AST with a type.
4346
from .value_from_ast import value_from_ast
4447

@@ -91,6 +94,7 @@
9194
"TypeInfoVisitor",
9295
"assert_valid_name",
9396
"ast_from_value",
97+
"ast_to_dict",
9498
"build_ast_schema",
9599
"build_client_schema",
96100
"build_schema",

src/graphql/utilities/ast_to_dict.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from typing import Any, Collection, Dict, List, Optional, overload
2+
3+
from ..language import Node, OperationType
4+
from ..pyutils import is_iterable
5+
6+
7+
__all__ = ["ast_to_dict"]
8+
9+
10+
@overload
11+
def ast_to_dict(
12+
node: Node, locations: bool = False, cache: Optional[Dict[Node, Any]] = None
13+
) -> Dict:
14+
...
15+
16+
17+
@overload
18+
def ast_to_dict(
19+
node: Collection[Node],
20+
locations: bool = False,
21+
cache: Optional[Dict[Node, Any]] = None,
22+
) -> List[Node]:
23+
...
24+
25+
26+
@overload
27+
def ast_to_dict(
28+
node: OperationType,
29+
locations: bool = False,
30+
cache: Optional[Dict[Node, Any]] = None,
31+
) -> str:
32+
...
33+
34+
35+
def ast_to_dict(
36+
node: Any, locations: bool = False, cache: Optional[Dict[Node, Any]] = None
37+
) -> Any:
38+
"""Convert a language AST to a nested Python dictionary.
39+
40+
Set `location` to True in order to get the locations as well.
41+
"""
42+
43+
"""Convert a node to a nested Python dictionary."""
44+
if isinstance(node, Node):
45+
if cache is None:
46+
cache = {}
47+
elif node in cache:
48+
return cache[node]
49+
cache[node] = res = {}
50+
res.update(
51+
{
52+
key: ast_to_dict(getattr(node, key), locations, cache)
53+
for key in ("kind",) + node.keys[1:]
54+
}
55+
)
56+
if locations:
57+
loc = node.loc
58+
if loc:
59+
res["loc"] = dict(start=loc.start, end=loc.end)
60+
return res
61+
if is_iterable(node):
62+
return [ast_to_dict(sub_node, locations, cache) for sub_node in node]
63+
if isinstance(node, OperationType):
64+
return node.value
65+
return node

tests/language/test_ast.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,32 @@ class Foo(Node):
240240
assert Foo.kind == "foo"
241241

242242
def provides_keys_as_class_attribute():
243-
assert SampleTestNode.keys == ["loc", "alpha", "beta"]
243+
assert SampleTestNode.keys == ("loc", "alpha", "beta")
244+
245+
def can_can_convert_to_dict():
246+
node = SampleTestNode(alpha=1, beta=2)
247+
res = node.to_dict()
248+
assert node.to_dict(locations=True) == res
249+
assert res == {"kind": "sample_test", "alpha": 1, "beta": 2}
250+
assert list(res) == ["kind", "alpha", "beta"]
251+
252+
def can_can_convert_to_dict_with_locations():
253+
token = Token(
254+
kind=TokenKind.NAME,
255+
start=1,
256+
end=3,
257+
line=1,
258+
column=1,
259+
value="foo",
260+
)
261+
loc = Location(token, token, Source("foo"))
262+
node = SampleTestNode(alpha=1, beta=2, loc=loc)
263+
res = node.to_dict(locations=True)
264+
assert res == {
265+
"kind": "sample_test",
266+
"alpha": 1,
267+
"beta": 2,
268+
"loc": {"start": 1, "end": 3},
269+
}
270+
assert list(res) == ["kind", "alpha", "beta", "loc"]
271+
assert list(res["loc"]) == ["start", "end"]

0 commit comments

Comments
 (0)