Skip to content

Commit ec45675

Browse files
committed
feat: hypothesis_graphql.nodes module to simplify working with custom scalars
1 parent 1dc5626 commit ec45675

File tree

7 files changed

+94
-64
lines changed

7 files changed

+94
-64
lines changed

README.rst

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ It is also possible to generate custom scalars. For example, ``Date``:
8989

9090
.. code:: python
9191
92-
from hypothesis import strategies as st
93-
import graphql
92+
from hypothesis import strategies as st, given
93+
from hypothesis_graphql import strategies as gql_st, nodes
9494
9595
SCHEMA = """
9696
scalar Date
@@ -107,7 +107,7 @@ It is also possible to generate custom scalars. For example, ``Date``:
107107
custom_scalars={
108108
# Standard scalars work out of the box, for custom ones you need
109109
# to pass custom strategies that generate proper AST nodes
110-
"Date": st.dates().map(lambda v: graphql.StringValueNode(value=str(v)))
110+
"Date": st.dates().map(nodes.String)
111111
},
112112
)
113113
)
@@ -118,6 +118,19 @@ It is also possible to generate custom scalars. For example, ``Date``:
118118
#
119119
...
120120
121+
The ``hypothesis_graphql.nodes`` module includes a few helpers to generate various node types:
122+
123+
- ``String`` -> ``graphql.StringValueNode``
124+
- ``Float`` -> ``graphql.FloatValueNode``
125+
- ``Int`` -> ``graphql.IntValueNode``
126+
- ``Object`` -> ``graphql.ObjectValueNode``
127+
- ``List`` -> ``graphql.ListValueNode``
128+
- ``Boolean`` -> ``graphql.BooleanValueNode``
129+
- ``Enum`` -> ``graphql.EnumValueNode``
130+
- ``Null`` -> ``graphql.NullValueNode`` (a constant, not a function)
131+
132+
They exist because classes like ``graphql.StringValueNode`` can't be directly used in ``map`` calls due to kwarg-only arguments.
133+
121134
.. |Build| image:: https://github.com/Stranger6667/hypothesis-graphql/workflows/build/badge.svg
122135
:target: https://github.com/Stranger6667/hypothesis-graphql/actions
123136
.. |Coverage| image:: https://codecov.io/gh/Stranger6667/hypothesis-graphql/branch/master/graph/badge.svg

docs/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changelog
44
`Unreleased`_ - TBD
55
-------------------
66

7+
**Added**
8+
9+
- ``hypothesis_graphql.nodes`` module to simplify working with custom scalars.
10+
711
`0.7.0`_ - 2022-04-26
812
---------------------
913

src/hypothesis_graphql/_strategies/factories.py

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Most of them exist to avoid using lambdas, which might become expensive in Hypothesis in some cases.
44
"""
55
from functools import lru_cache
6-
from typing import Callable, List, Optional, Tuple, Type
6+
from typing import Callable, List, Optional, Tuple
77

88
import graphql
99

@@ -52,44 +52,3 @@ def factory(value: graphql.ValueNode) -> graphql.ObjectFieldNode:
5252
return graphql.ObjectFieldNode(name=graphql.NameNode(value=name), value=value)
5353

5454
return factory
55-
56-
57-
def object_value(fields: List[graphql.ObjectFieldNode]) -> graphql.ObjectValueNode:
58-
return graphql.ObjectValueNode(fields=fields)
59-
60-
61-
def list_value(values: List[graphql.ValueNode]) -> graphql.ListValueNode:
62-
return graphql.ListValueNode(values=values)
63-
64-
65-
# Boolean & Enum nodes have a limited set of variants, therefore caching is effective in this case
66-
67-
68-
@lru_cache()
69-
def boolean(value: bool) -> graphql.BooleanValueNode:
70-
return graphql.BooleanValueNode(value=value)
71-
72-
73-
@lru_cache()
74-
def enum(value: str) -> graphql.EnumValueNode:
75-
return graphql.EnumValueNode(value=value)
76-
77-
78-
# Other types of nodes are not that cache-efficient.
79-
# Constructors are passed as locals to optimize the byte code a little
80-
81-
82-
def string(
83-
value: str, StringValueNode: Type[graphql.StringValueNode] = graphql.StringValueNode
84-
) -> graphql.StringValueNode:
85-
return StringValueNode(value=value)
86-
87-
88-
def float_(
89-
value: float, FloatValueNode: Type[graphql.FloatValueNode] = graphql.FloatValueNode
90-
) -> graphql.FloatValueNode:
91-
return FloatValueNode(value=str(value))
92-
93-
94-
def int_(value: int, IntValueNode: Type[graphql.IntValueNode] = graphql.IntValueNode) -> graphql.IntValueNode:
95-
return IntValueNode(value=str(value))

src/hypothesis_graphql/_strategies/primitives.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
"""Strategies for simple types like scalars or enums."""
22
from functools import lru_cache
3-
from typing import Tuple, Union
3+
from typing import Tuple, Type, Union
44

55
import graphql
66
from hypothesis import strategies as st
77
from hypothesis.errors import InvalidArgument
88

9+
from .. import nodes
910
from ..types import ScalarValueNode
10-
from . import factories
1111

1212
MIN_INT = -(2**31)
1313
MAX_INT = 2**31 - 1
1414

1515

16-
STRING_STRATEGY = st.text(alphabet=st.characters(blacklist_categories=("Cs",), max_codepoint=0xFFFF)).map(
17-
factories.string
18-
)
19-
INTEGER_STRATEGY = st.integers(min_value=MIN_INT, max_value=MAX_INT).map(factories.int_)
20-
FLOAT_STRATEGY = st.floats(allow_infinity=False, allow_nan=False).map(factories.float_)
21-
BOOLEAN_STRATEGY = st.booleans().map(factories.boolean)
22-
NULL_VALUE_NODE = graphql.NullValueNode()
23-
NULL_STRATEGY = st.just(NULL_VALUE_NODE)
16+
# `String` version without extra `str` call
17+
def _string(
18+
value: str, StringValueNode: Type[graphql.StringValueNode] = graphql.StringValueNode
19+
) -> graphql.StringValueNode:
20+
return StringValueNode(value=value)
21+
22+
23+
STRING_STRATEGY = st.text(alphabet=st.characters(blacklist_categories=("Cs",), max_codepoint=0xFFFF)).map(_string)
24+
INTEGER_STRATEGY = st.integers(min_value=MIN_INT, max_value=MAX_INT).map(nodes.Int)
25+
FLOAT_STRATEGY = st.floats(allow_infinity=False, allow_nan=False).map(nodes.Float)
26+
BOOLEAN_STRATEGY = st.booleans().map(nodes.Boolean)
27+
NULL_STRATEGY = st.just(nodes.Null)
2428

2529

2630
@lru_cache(maxsize=16)
@@ -69,4 +73,4 @@ def maybe_null(strategy: st.SearchStrategy, nullable: bool) -> st.SearchStrategy
6973

7074
@lru_cache(maxsize=64)
7175
def enum(values: Tuple[str], nullable: bool = True) -> st.SearchStrategy[graphql.EnumValueNode]:
72-
return maybe_null(st.sampled_from(values).map(factories.enum), nullable)
76+
return maybe_null(st.sampled_from(values).map(nodes.Enum), nullable)

src/hypothesis_graphql/_strategies/strategy.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from hypothesis.errors import InvalidArgument
1111
from hypothesis.strategies._internal.utils import cacheable
1212

13+
from .. import nodes
1314
from ..types import AstPrinter, CustomScalars, Field, InputTypeNode, InterfaceOrObject, SelectionNodes
1415
from . import factories, primitives
1516
from .ast import make_mutation, make_query
@@ -80,7 +81,7 @@ def values(self, type_: graphql.GraphQLInputType) -> st.SearchStrategy[InputType
8081
def lists(self, type_: graphql.GraphQLList, nullable: bool = True) -> st.SearchStrategy[graphql.ListValueNode]:
8182
"""Generate a `graphql.ListValueNode`."""
8283
strategy = st.lists(self.values(type_.of_type))
83-
return primitives.maybe_null(strategy.map(factories.list_value), nullable)
84+
return primitives.maybe_null(strategy.map(nodes.List), nullable)
8485

8586
@instance_cache(lambda type_, nullable=True: (type_.name, nullable))
8687
def objects(
@@ -89,7 +90,7 @@ def objects(
8990
"""Generate a `graphql.ObjectValueNode`."""
9091
fields = {name: field for name, field in type_.fields.items() if self.can_generate_field(field)}
9192
strategy = subset_of_fields(fields, force_required=True).flatmap(self.lists_of_object_fields)
92-
return primitives.maybe_null(strategy.map(factories.object_value), nullable)
93+
return primitives.maybe_null(strategy.map(nodes.Object), nullable)
9394

9495
def can_generate_field(self, field: graphql.GraphQLInputField) -> bool:
9596
"""Whether it is possible to generate values for the given field."""
@@ -199,11 +200,7 @@ def inner(draw: Any) -> List[graphql.ArgumentNode]:
199200
if not isinstance(argument.type, graphql.GraphQLNonNull):
200201
# If the type is nullable, then either generate `null` or skip it completely
201202
if draw(st.booleans()):
202-
args.append(
203-
graphql.ArgumentNode(
204-
name=graphql.NameNode(value=name), value=primitives.NULL_VALUE_NODE
205-
)
206-
)
203+
args.append(graphql.ArgumentNode(name=graphql.NameNode(value=name), value=nodes.Null))
207204
continue
208205
raise
209206
args.append(draw(argument_strategy.map(factories.argument(name))))

src/hypothesis_graphql/nodes.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import typing
2+
from functools import lru_cache
3+
4+
import graphql
5+
6+
# These types are not cache-efficient.
7+
# Constructors are passed as locals to optimize the byte code a little
8+
9+
10+
def String(
11+
value: typing.Any, StringValueNode: typing.Type[graphql.StringValueNode] = graphql.StringValueNode
12+
) -> graphql.StringValueNode:
13+
return StringValueNode(value=str(value))
14+
15+
16+
def Float(
17+
value: float, FloatValueNode: typing.Type[graphql.FloatValueNode] = graphql.FloatValueNode
18+
) -> graphql.FloatValueNode:
19+
return FloatValueNode(value=str(value))
20+
21+
22+
def Int(value: int, IntValueNode: typing.Type[graphql.IntValueNode] = graphql.IntValueNode) -> graphql.IntValueNode:
23+
return IntValueNode(value=str(value))
24+
25+
26+
def Object(
27+
fields: typing.List[graphql.ObjectFieldNode],
28+
ObjectValueNode: typing.Type[graphql.ObjectValueNode] = graphql.ObjectValueNode,
29+
) -> graphql.ObjectValueNode:
30+
return ObjectValueNode(fields=fields)
31+
32+
33+
def List(
34+
values: typing.List[graphql.ValueNode], ListValueNode: typing.Type[graphql.ListValueNode] = graphql.ListValueNode
35+
) -> graphql.ListValueNode:
36+
return ListValueNode(values=values)
37+
38+
39+
# Boolean & Enum nodes have a limited set of variants, therefore caching is effective in this case
40+
41+
42+
@lru_cache()
43+
def Boolean(value: bool) -> graphql.BooleanValueNode:
44+
return graphql.BooleanValueNode(value=value)
45+
46+
47+
@lru_cache()
48+
def Enum(value: str) -> graphql.EnumValueNode:
49+
return graphql.EnumValueNode(value=value)
50+
51+
52+
Null = graphql.NullValueNode()

test/test_customization.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from hypothesis import strategies as st
66
from hypothesis.errors import InvalidArgument
77

8+
from hypothesis_graphql import nodes
89
from hypothesis_graphql import strategies as gql_st
910
from hypothesis_graphql._strategies import factories
1011

@@ -132,7 +133,7 @@ def test_custom_scalar_registered(data, validate_operation):
132133
schema = CUSTOM_SCALAR_TEMPLATE.format(query="getByDate(created: Date!): Int")
133134
expected = "EXAMPLE"
134135

135-
query = data.draw(gql_st.queries(schema, custom_scalars={"Date": st.just(expected).map(factories.string)}))
136+
query = data.draw(gql_st.queries(schema, custom_scalars={"Date": st.just(expected).map(nodes.String)}))
136137
validate_operation(schema, query)
137138
assert f'getByDate(created: "{expected}")' in query
138139

0 commit comments

Comments
 (0)