Skip to content

Commit 41f1876

Browse files
committed
add value_to_literal()
Replicates graphql/graphql-js@c05c7e2
1 parent 2d348fb commit 41f1876

17 files changed

Lines changed: 861 additions & 63 deletions

docs/modules/utilities.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ Create a GraphQL language AST from a Python value:
6666

6767
.. autofunction:: ast_from_value
6868

69+
Create a GraphQL literal (AST) from a Python value:
70+
71+
.. autofunction:: value_to_literal
72+
73+
Replace any variables in an AST value with their literal values:
74+
75+
.. autofunction:: replace_variables
76+
6977
A helper to use within recursive-descent visitors which need to be aware of the GraphQL
7078
type system:
7179

src/graphql/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@
199199
value_from_ast_untyped,
200200
# Create a GraphQL language AST from a Python value.
201201
ast_from_value,
202+
# Create a GraphQL literal (AST) from a Python value.
203+
value_to_literal,
204+
# Replace any variables in an AST value with their literal values.
205+
replace_variables,
202206
# A helper to use within recursive-descent visitors which need to be aware of the
203207
# GraphQL type system.
204208
TypeInfo,
@@ -356,6 +360,8 @@
356360
GraphQLScalarSerializer,
357361
GraphQLScalarValueParser,
358362
GraphQLScalarLiteralParser,
363+
GraphQLScalarConstLiteralParser,
364+
GraphQLScalarValueToLiteral,
359365
GraphQLDefaultValueUsage,
360366
GraphQLIsTypeOfFn,
361367
GraphQLResolveInfo,
@@ -579,11 +585,13 @@
579585
"GraphQLOneOfDirective",
580586
"GraphQLOutputType",
581587
"GraphQLResolveInfo",
588+
"GraphQLScalarConstLiteralParser",
582589
"GraphQLScalarLiteralParser",
583590
"GraphQLScalarSerializer",
584591
"GraphQLScalarType",
585592
"GraphQLScalarTypeKwargs",
586593
"GraphQLScalarValueParser",
594+
"GraphQLScalarValueToLiteral",
587595
"GraphQLSchema",
588596
"GraphQLSchemaKwargs",
589597
"GraphQLSkipDirective",
@@ -803,6 +811,7 @@
803811
"print_source_location",
804812
"print_type",
805813
"recommended_rules",
814+
"replace_variables",
806815
"resolve_thunk",
807816
"separate_operations",
808817
"specified_directives",
@@ -815,6 +824,7 @@
815824
"validate_schema",
816825
"value_from_ast",
817826
"value_from_ast_untyped",
827+
"value_to_literal",
818828
"version",
819829
"version_info",
820830
"version_info_js",

src/graphql/type/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@
103103
GraphQLScalarSerializer,
104104
GraphQLScalarValueParser,
105105
GraphQLScalarLiteralParser,
106+
GraphQLScalarConstLiteralParser,
107+
GraphQLScalarValueToLiteral,
106108
GraphQLDefaultValueUsage,
107109
# Keyword Args
108110
GraphQLArgumentKwargs,
@@ -234,11 +236,13 @@
234236
"GraphQLOneOfDirective",
235237
"GraphQLOutputType",
236238
"GraphQLResolveInfo",
239+
"GraphQLScalarConstLiteralParser",
237240
"GraphQLScalarLiteralParser",
238241
"GraphQLScalarSerializer",
239242
"GraphQLScalarType",
240243
"GraphQLScalarTypeKwargs",
241244
"GraphQLScalarValueParser",
245+
"GraphQLScalarValueToLiteral",
242246
"GraphQLSchema",
243247
"GraphQLSchemaKwargs",
244248
"GraphQLSkipDirective",

src/graphql/type/definition.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,13 @@
109109
"GraphQLObjectTypeKwargs",
110110
"GraphQLOutputType",
111111
"GraphQLResolveInfo",
112+
"GraphQLScalarConstLiteralParser",
112113
"GraphQLScalarLiteralParser",
113114
"GraphQLScalarSerializer",
114115
"GraphQLScalarType",
115116
"GraphQLScalarTypeKwargs",
116117
"GraphQLScalarValueParser",
118+
"GraphQLScalarValueToLiteral",
117119
"GraphQLType",
118120
"GraphQLTypeResolver",
119121
"GraphQLUnionType",
@@ -316,6 +318,8 @@ def resolve_thunk(thunk: Thunk[T]) -> T:
316318
GraphQLScalarLiteralParser: TypeAlias = Callable[
317319
[ValueNode, dict[str, Any] | None], Any
318320
]
321+
GraphQLScalarConstLiteralParser: TypeAlias = Callable[[ConstValueNode], Any]
322+
GraphQLScalarValueToLiteral: TypeAlias = Callable[[Any], "ConstValueNode | None"]
319323

320324

321325
class GraphQLScalarTypeKwargs(GraphQLNamedTypeKwargs, total=False):
@@ -324,6 +328,8 @@ class GraphQLScalarTypeKwargs(GraphQLNamedTypeKwargs, total=False):
324328
serialize: GraphQLScalarSerializer | None
325329
parse_value: GraphQLScalarValueParser | None
326330
parse_literal: GraphQLScalarLiteralParser | None
331+
parse_const_literal: GraphQLScalarConstLiteralParser | None
332+
value_to_literal: GraphQLScalarValueToLiteral | None
327333
specified_by_url: str | None
328334

329335

@@ -359,12 +365,16 @@ def serialize_odd(value: Any) -> int:
359365
ast_node: ScalarTypeDefinitionNode | None
360366
extension_ast_nodes: tuple[ScalarTypeExtensionNode, ...]
361367

368+
value_to_literal: GraphQLScalarValueToLiteral | None
369+
362370
def __init__(
363371
self,
364372
name: str,
365373
serialize: GraphQLScalarSerializer | None = None,
366374
parse_value: GraphQLScalarValueParser | None = None,
367375
parse_literal: GraphQLScalarLiteralParser | None = None,
376+
parse_const_literal: GraphQLScalarConstLiteralParser | None = None,
377+
value_to_literal: GraphQLScalarValueToLiteral | None = None,
368378
description: str | None = None,
369379
specified_by_url: str | None = None,
370380
extensions: dict[str, Any] | None = None,
@@ -385,11 +395,20 @@ def __init__(
385395
self.parse_value = parse_value # type: ignore
386396
if parse_literal is not None:
387397
self.parse_literal = parse_literal # type: ignore
398+
if parse_const_literal is not None:
399+
self.parse_const_literal = parse_const_literal # type: ignore
400+
self.value_to_literal = value_to_literal
388401
if parse_literal is not None and parse_value is None:
389402
msg = (
390403
f"{name} must provide both 'parse_value' and 'parse_literal' functions."
391404
)
392405
raise TypeError(msg)
406+
if parse_const_literal is not None and parse_value is None:
407+
msg = (
408+
f"{name} must provide both 'parse_value'"
409+
" and 'parse_const_literal' functions."
410+
)
411+
raise TypeError(msg)
393412
self.specified_by_url = specified_by_url
394413

395414
def __repr__(self) -> str:
@@ -423,9 +442,21 @@ def parse_literal(
423442
424443
This default method uses the parse_value method and should be replaced
425444
with a more specific version when creating a scalar type.
445+
446+
.. deprecated:: 3.3
447+
Use ``replace_variables()`` and ``parse_const_literal()`` instead.
448+
``parse_literal()`` will be removed in a future version.
426449
"""
427450
return self.parse_value(value_from_ast_untyped(node, variables))
428451

452+
def parse_const_literal(self, node: ConstValueNode) -> Any:
453+
"""Parses an externally provided const literal value to use as an input.
454+
455+
This default method uses the parse_value method and should be replaced
456+
with a more specific version when creating a scalar type.
457+
"""
458+
return self.parse_value(value_from_ast_untyped(node))
459+
429460
def to_kwargs(self) -> GraphQLScalarTypeKwargs:
430461
"""Get corresponding arguments."""
431462
return GraphQLScalarTypeKwargs(
@@ -440,6 +471,11 @@ def to_kwargs(self) -> GraphQLScalarTypeKwargs:
440471
if getattr(self.parse_literal, "__func__", None)
441472
is GraphQLScalarType.parse_literal
442473
else self.parse_literal,
474+
parse_const_literal=None
475+
if getattr(self.parse_const_literal, "__func__", None)
476+
is GraphQLScalarType.parse_const_literal
477+
else self.parse_const_literal,
478+
value_to_literal=self.value_to_literal,
443479
specified_by_url=self.specified_by_url,
444480
)
445481

@@ -1228,8 +1264,17 @@ def parse_value(self, input_value: str) -> Any:
12281264
def parse_literal(
12291265
self, value_node: ValueNode, _variables: dict[str, Any] | None = None
12301266
) -> Any:
1231-
"""Parse literal value."""
1267+
"""Parse literal value.
1268+
1269+
.. deprecated:: 3.3
1270+
Use ``parse_const_literal()`` instead. ``parse_literal()`` will be
1271+
removed in a future version.
1272+
"""
12321273
# Note: variables will be resolved before calling this method.
1274+
return self.parse_const_literal(cast("ConstValueNode", value_node))
1275+
1276+
def parse_const_literal(self, value_node: ConstValueNode) -> Any:
1277+
"""Parse const literal value."""
12331278
if isinstance(value_node, EnumValueNode):
12341279
try:
12351280
enum_value = self.values[value_node.value]
@@ -1248,6 +1293,12 @@ def parse_literal(
12481293
)
12491294
raise GraphQLError(msg, value_node)
12501295

1296+
def value_to_literal(self, value: Any) -> ConstValueNode | None:
1297+
"""Convert an external value to an enum literal (AST)."""
1298+
if isinstance(value, str) and self.values.get(value):
1299+
return EnumValueNode(value=value)
1300+
return None
1301+
12511302

12521303
def is_enum_type(type_: Any) -> TypeGuard[GraphQLEnumType]:
12531304
"""Check whether this is a GraphQL enum type."""

src/graphql/type/scalars.py

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,29 @@
22

33
from __future__ import annotations
44

5+
import re
56
from math import isfinite
67
from typing import TYPE_CHECKING, Any, TypeGuard
78

89
from ..error import GraphQLError
910
from ..language.ast import (
1011
BooleanValueNode,
12+
ConstValueNode,
1113
FloatValueNode,
1214
IntValueNode,
1315
StringValueNode,
1416
ValueNode,
1517
)
1618
from ..language.printer import print_ast
1719
from ..pyutils import inspect
20+
from ..utilities.value_to_literal import default_scalar_value_to_literal
1821
from .definition import GraphQLNamedType, GraphQLScalarType
1922

2023
if TYPE_CHECKING:
2124
from collections.abc import Mapping
2225

26+
_re_integer_string = re.compile("^-?(?:0|[1-9][0-9]*)$")
27+
2328
__all__ = [
2429
"GRAPHQL_MAX_INT",
2530
"GRAPHQL_MIN_INT",
@@ -104,14 +109,28 @@ def parse_int_literal(value_node: ValueNode, _variables: Any = None) -> int:
104109
return num
105110

106111

112+
def int_value_to_literal(value: Any) -> ConstValueNode | None:
113+
"""Convert an integer value to an Int literal in the AST."""
114+
if (
115+
isinstance(value, (int, float))
116+
and not isinstance(value, bool)
117+
and isfinite(value)
118+
and value == int(value)
119+
and GRAPHQL_MIN_INT <= value <= GRAPHQL_MAX_INT
120+
):
121+
return IntValueNode(value=str(int(value)))
122+
return None
123+
124+
107125
GraphQLInt = GraphQLScalarType(
108126
name="Int",
109127
description="The `Int` scalar type represents"
110128
" non-fractional signed whole numeric values."
111129
" Int can represent values between -(2^31) and 2^31 - 1.",
112130
serialize=serialize_int,
113131
parse_value=coerce_int,
114-
parse_literal=parse_int_literal,
132+
parse_const_literal=parse_int_literal,
133+
value_to_literal=int_value_to_literal,
115134
)
116135

117136

@@ -151,6 +170,14 @@ def parse_float_literal(value_node: ValueNode, _variables: Any = None) -> float:
151170
return float(value_node.value)
152171

153172

173+
def float_value_to_literal(value: Any) -> ConstValueNode | None:
174+
"""Convert a float value to a Float literal in the AST."""
175+
literal = default_scalar_value_to_literal(value)
176+
if isinstance(literal, (FloatValueNode, IntValueNode)):
177+
return literal
178+
return None
179+
180+
154181
GraphQLFloat = GraphQLScalarType(
155182
name="Float",
156183
description="The `Float` scalar type represents"
@@ -159,7 +186,8 @@ def parse_float_literal(value_node: ValueNode, _variables: Any = None) -> float:
159186
"(https://en.wikipedia.org/wiki/IEEE_floating_point).",
160187
serialize=serialize_float,
161188
parse_value=coerce_float,
162-
parse_literal=parse_float_literal,
189+
parse_const_literal=parse_float_literal,
190+
value_to_literal=float_value_to_literal,
163191
)
164192

165193

@@ -197,6 +225,14 @@ def parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str:
197225
return value_node.value
198226

199227

228+
def string_value_to_literal(value: Any) -> ConstValueNode | None:
229+
"""Convert a string value to a String literal in the AST."""
230+
literal = default_scalar_value_to_literal(value)
231+
if isinstance(literal, StringValueNode):
232+
return literal
233+
return None
234+
235+
200236
GraphQLString = GraphQLScalarType(
201237
name="String",
202238
description="The `String` scalar type represents textual data,"
@@ -205,7 +241,8 @@ def parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str:
205241
" to represent free-form human-readable text.",
206242
serialize=serialize_string,
207243
parse_value=coerce_string,
208-
parse_literal=parse_string_literal,
244+
parse_const_literal=parse_string_literal,
245+
value_to_literal=string_value_to_literal,
209246
)
210247

211248

@@ -239,12 +276,21 @@ def parse_boolean_literal(value_node: ValueNode, _variables: Any = None) -> bool
239276
return value_node.value
240277

241278

279+
def boolean_value_to_literal(value: Any) -> ConstValueNode | None:
280+
"""Convert a boolean value to a Boolean literal in the AST."""
281+
literal = default_scalar_value_to_literal(value)
282+
if isinstance(literal, BooleanValueNode):
283+
return literal
284+
return None
285+
286+
242287
GraphQLBoolean = GraphQLScalarType(
243288
name="Boolean",
244289
description="The `Boolean` scalar type represents `true` or `false`.",
245290
serialize=serialize_boolean,
246291
parse_value=coerce_boolean,
247-
parse_literal=parse_boolean_literal,
292+
parse_const_literal=parse_boolean_literal,
293+
value_to_literal=boolean_value_to_literal,
248294
)
249295

250296

@@ -291,6 +337,27 @@ def parse_id_literal(value_node: ValueNode, _variables: Any = None) -> str:
291337
return value_node.value
292338

293339

340+
def id_value_to_literal(value: Any) -> ConstValueNode | None:
341+
"""Convert an ID value to an Int or String literal in the AST."""
342+
# ID types can use number values and Int literals.
343+
string_value: Any = (
344+
str(int(value))
345+
if isinstance(value, (int, float))
346+
and not isinstance(value, bool)
347+
and isfinite(value)
348+
and value == int(value)
349+
else value
350+
)
351+
if isinstance(string_value, str):
352+
# Will parse as an IntValue if it consists only of integer digits.
353+
return (
354+
IntValueNode(value=string_value)
355+
if _re_integer_string.match(string_value)
356+
else StringValueNode(value=string_value, block=False)
357+
)
358+
return None
359+
360+
294361
GraphQLID = GraphQLScalarType(
295362
name="ID",
296363
description="The `ID` scalar type represents a unique identifier,"
@@ -301,7 +368,8 @@ def parse_id_literal(value_node: ValueNode, _variables: Any = None) -> str:
301368
" `4`) input value will be accepted as an ID.",
302369
serialize=serialize_id,
303370
parse_value=coerce_id,
304-
parse_literal=parse_id_literal,
371+
parse_const_literal=parse_id_literal,
372+
value_to_literal=id_value_to_literal,
305373
)
306374

307375
specified_scalar_types: Mapping[str, GraphQLScalarType] = {

0 commit comments

Comments
 (0)