Skip to content

Commit 6dd4c9b

Browse files
committed
feat: Support for using default values of arguments and input fields
Ref: #71
1 parent 0973020 commit 6dd4c9b

File tree

4 files changed

+169
-35
lines changed

4 files changed

+169
-35
lines changed

docs/changelog.rst

Lines changed: 5 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+
- Support for using default values of arguments and input fields. `#71`_
10+
711
**Fixed**
812

913
- Duplicated inline fragments that may miss aliases. `#69`_
@@ -166,6 +170,7 @@ Invalid queries:
166170
.. _0.3.1: https://github.com/stranger6667/hypothesis-graphql/compare/v0.3.0...v0.3.1
167171
.. _0.3.0: https://github.com/stranger6667/hypothesis-graphql/compare/v0.2.0...v0.3.0
168172

173+
.. _#71: https://github.com/Stranger6667/hypothesis-graphql/71
169174
.. _#69: https://github.com/Stranger6667/hypothesis-graphql/69
170175
.. _#57: https://github.com/Stranger6667/hypothesis-graphql/57
171176
.. _#51: https://github.com/Stranger6667/hypothesis-graphql/51
Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Strategies for simple types like scalars or enums."""
22
from functools import lru_cache
3-
from typing import Tuple, Type, Union
3+
from typing import Optional, Tuple, Type, TypeVar, Union
44

55
import graphql
66
from hypothesis import strategies as st
@@ -9,6 +9,7 @@
99
from .. import nodes
1010
from ..types import ScalarValueNode
1111

12+
T = TypeVar("T")
1213
MIN_INT = -(2**31)
1314
MAX_INT = 2**31 - 1
1415

@@ -28,49 +29,86 @@ def _string(
2829

2930

3031
@lru_cache(maxsize=16)
31-
def scalar(type_name: str, nullable: bool = True) -> st.SearchStrategy[ScalarValueNode]:
32+
def scalar(
33+
type_name: str, nullable: bool = True, default: Optional[graphql.ValueNode] = None
34+
) -> st.SearchStrategy[ScalarValueNode]:
3235
if type_name == "Int":
33-
return int_(nullable)
36+
return int_(nullable, default)
3437
if type_name == "Float":
35-
return float_(nullable)
38+
return float_(nullable, default)
3639
if type_name == "String":
37-
return string(nullable)
40+
return string(nullable, default)
3841
if type_name == "ID":
39-
return id_(nullable)
42+
return id_(nullable, default)
4043
if type_name == "Boolean":
41-
return boolean(nullable)
44+
return boolean(nullable, default)
4245
raise InvalidArgument(
4346
f"Scalar {type_name!r} is not supported. "
4447
"Provide a Hypothesis strategy via the `custom_scalars` argument to generate it."
4548
)
4649

4750

48-
def int_(nullable: bool = True) -> st.SearchStrategy[graphql.IntValueNode]:
49-
return maybe_null(INTEGER_STRATEGY, nullable)
51+
def int_(nullable: bool = True, default: Optional[graphql.ValueNode] = None) -> st.SearchStrategy[graphql.IntValueNode]:
52+
return maybe_default(maybe_null(INTEGER_STRATEGY, nullable), default=default)
5053

5154

52-
def float_(nullable: bool = True) -> st.SearchStrategy[graphql.FloatValueNode]:
53-
return maybe_null(FLOAT_STRATEGY, nullable)
55+
def float_(
56+
nullable: bool = True, default: Optional[graphql.ValueNode] = None
57+
) -> st.SearchStrategy[graphql.FloatValueNode]:
58+
return maybe_default(maybe_null(FLOAT_STRATEGY, nullable), default=default)
5459

5560

56-
def string(nullable: bool = True) -> st.SearchStrategy[graphql.StringValueNode]:
57-
return maybe_null(STRING_STRATEGY, nullable)
61+
def string(
62+
nullable: bool = True, default: Optional[graphql.ValueNode] = None
63+
) -> st.SearchStrategy[graphql.StringValueNode]:
64+
return maybe_default(
65+
maybe_null(STRING_STRATEGY, nullable),
66+
default=default,
67+
)
5868

5969

60-
def id_(nullable: bool = True) -> st.SearchStrategy[Union[graphql.StringValueNode, graphql.IntValueNode]]:
61-
return string(nullable) | int_(nullable)
70+
def id_(
71+
nullable: bool = True, default: Optional[graphql.ValueNode] = None
72+
) -> st.SearchStrategy[Union[graphql.StringValueNode, graphql.IntValueNode]]:
73+
return maybe_default(string(nullable) | int_(nullable), default=default)
6274

6375

64-
def boolean(nullable: bool = True) -> st.SearchStrategy[graphql.BooleanValueNode]:
65-
return maybe_null(BOOLEAN_STRATEGY, nullable)
76+
def boolean(
77+
nullable: bool = True, default: Optional[graphql.ValueNode] = None
78+
) -> st.SearchStrategy[graphql.BooleanValueNode]:
79+
return maybe_default(maybe_null(BOOLEAN_STRATEGY, nullable), default=default)
6680

6781

68-
def maybe_null(strategy: st.SearchStrategy, nullable: bool) -> st.SearchStrategy:
82+
def maybe_null(strategy: st.SearchStrategy[T], nullable: bool) -> st.SearchStrategy[T]:
6983
if nullable:
7084
strategy |= NULL_STRATEGY
7185
return strategy
7286

7387

88+
def custom(
89+
strategy: st.SearchStrategy, nullable: bool = True, default: Optional[graphql.ValueNode] = None
90+
) -> st.SearchStrategy:
91+
return maybe_default(maybe_null(strategy, nullable), default=default)
92+
93+
94+
def maybe_default(
95+
strategy: st.SearchStrategy[T], *, default: Optional[graphql.ValueNode] = None
96+
) -> st.SearchStrategy[T]:
97+
if default is not None:
98+
strategy |= st.just(default)
99+
return strategy
100+
101+
74102
@lru_cache(maxsize=64)
75-
def enum(values: Tuple[str], nullable: bool = True) -> st.SearchStrategy[graphql.EnumValueNode]:
76-
return maybe_null(st.sampled_from(values).map(nodes.Enum), nullable)
103+
def enum(
104+
values: Tuple[str], nullable: bool = True, default: Optional[graphql.ValueNode] = None
105+
) -> st.SearchStrategy[graphql.EnumValueNode]:
106+
return maybe_default(maybe_null(st.sampled_from(values).map(nodes.Enum), nullable), default=default)
107+
108+
109+
def list_(
110+
strategy: st.SearchStrategy[graphql.ListValueNode],
111+
nullable: bool = True,
112+
default: Optional[graphql.ValueNode] = None,
113+
) -> st.SearchStrategy[graphql.ListValueNode]:
114+
return maybe_default(maybe_null(strategy.map(nodes.List), nullable), default=default)

src/hypothesis_graphql/_strategies/strategy.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ class GraphQLStrategy:
5050
# This is a per-method cache without limits as they are proportionate to the schema size
5151
_cache: Dict[str, Dict] = attr.ib(factory=dict)
5252

53-
def values(self, type_: graphql.GraphQLInputType) -> st.SearchStrategy[InputTypeNode]:
53+
def values(
54+
self, type_: graphql.GraphQLInputType, default: Optional[graphql.ValueNode] = None
55+
) -> st.SearchStrategy[InputTypeNode]:
5456
"""Generate value nodes of a type, that corresponds to the input type.
5557
5658
They correspond to all `GraphQLInputType` variants:
@@ -67,23 +69,25 @@ def values(self, type_: graphql.GraphQLInputType) -> st.SearchStrategy[InputType
6769
if isinstance(type_, graphql.GraphQLScalarType):
6870
type_name = type_.name
6971
if type_name in self.custom_scalars:
70-
return primitives.maybe_null(self.custom_scalars[type_name], nullable)
71-
return primitives.scalar(type_name, nullable)
72+
return primitives.custom(self.custom_scalars[type_name], nullable, default=default)
73+
return primitives.scalar(type_name, nullable, default=default)
7274
if isinstance(type_, graphql.GraphQLEnumType):
7375
values = tuple(type_.values)
74-
return primitives.enum(values, nullable)
76+
return primitives.enum(values, nullable, default=default)
7577
# Types with children
7678
if isinstance(type_, graphql.GraphQLList):
77-
return self.lists(type_, nullable)
79+
return self.lists(type_, nullable, default=default)
7880
if isinstance(type_, graphql.GraphQLInputObjectType):
7981
return self.objects(type_, nullable)
8082
raise TypeError(f"Type {type_.__class__.__name__} is not supported.")
8183

82-
@instance_cache(lambda type_, nullable=True: (make_type_name(type_), nullable))
83-
def lists(self, type_: graphql.GraphQLList, nullable: bool = True) -> st.SearchStrategy[graphql.ListValueNode]:
84+
@instance_cache(lambda type_, nullable=True, default=None: (make_type_name(type_), nullable, default))
85+
def lists(
86+
self, type_: graphql.GraphQLList, nullable: bool = True, default: Optional[graphql.ValueNode] = None
87+
) -> st.SearchStrategy[graphql.ListValueNode]:
8488
"""Generate a `graphql.ListValueNode`."""
8589
strategy = st.lists(self.values(type_.of_type))
86-
return primitives.maybe_null(strategy.map(nodes.List), nullable)
90+
return primitives.list_(strategy, nullable, default=default)
8791

8892
@instance_cache(lambda type_, nullable=True: (type_.name, nullable))
8993
def objects(
@@ -113,11 +117,16 @@ def can_generate_field(self, field: graphql.GraphQLInputField) -> bool:
113117
)
114118

115119
def lists_of_object_fields(
116-
self, items: List[Tuple[str, Field]]
120+
self, items: List[Tuple[str, graphql.GraphQLInputField]]
117121
) -> st.SearchStrategy[List[graphql.ObjectFieldNode]]:
118-
return st.tuples(*(self.values(field.type).map(factories.object_field(name)) for name, field in items)).map(
119-
list
120-
)
122+
return st.tuples(
123+
*(
124+
self.values(field.type, field.ast_node.default_value if field.ast_node is not None else None).map(
125+
factories.object_field(name)
126+
)
127+
for name, field in items
128+
)
129+
).map(list)
121130

122131
@instance_cache(lambda interface, implementations: (interface.name, tuple(impl.name for impl in implementations)))
123132
def interfaces(
@@ -202,8 +211,9 @@ def list_of_arguments(
202211
def inner(draw: Any) -> List[graphql.ArgumentNode]:
203212
args = []
204213
for name, argument in arguments.items():
214+
default = argument.ast_node.default_value if argument.ast_node is not None else None
205215
try:
206-
argument_strategy = self.values(argument.type)
216+
argument_strategy = self.values(argument.type, default=default)
207217
except InvalidArgument:
208218
if not isinstance(argument.type, graphql.GraphQLNonNull):
209219
# If the type is nullable, then either generate `null` or skip it completely
@@ -305,8 +315,8 @@ def add_alias(frag: graphql.InlineFragmentNode) -> graphql.InlineFragmentNode:
305315

306316

307317
def subset_of_fields(
308-
fields: Dict[str, Field], *, force_required: bool = False
309-
) -> st.SearchStrategy[List[Tuple[str, Field]]]:
318+
fields: Dict[str, graphql.GraphQLInputField], *, force_required: bool = False
319+
) -> st.SearchStrategy[List[Tuple[str, graphql.GraphQLInputField]]]:
310320
"""A helper to select a subset of fields."""
311321
field_pairs = sorted(fields.items())
312322
# if we need to always generate required fields, then return them and extend with a subset of optional fields

test/test_queries.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from hypothesis import assume, find, given, settings
55
from hypothesis import strategies as st
66

7+
from hypothesis_graphql import nodes
78
from hypothesis_graphql import strategies as gql_st
89
from hypothesis_graphql._strategies.strategy import GraphQLStrategy
910
from hypothesis_graphql.cache import cached_build_schema
@@ -372,3 +373,83 @@ def test(query):
372373
assert query == "DocumentNode"
373374

374375
test()
376+
377+
378+
@pytest.mark.parametrize(
379+
"type_name, default",
380+
(
381+
("String!", '"foo"'),
382+
("[String!]", '["foo"]'),
383+
("[String!]!", '["foo"]'),
384+
("String", "null"),
385+
("ID!", "4432841242"),
386+
("ID!", '"Foo"'),
387+
("[ID!]", '["Foo"]'),
388+
("[ID!]", '["Foo", 42]'),
389+
("Int!", "4432841"),
390+
("[Int!]", "[4432841]"),
391+
("Float!", "4432841242.123"),
392+
("[Float!]", "[4432841242.123]"),
393+
("Date!", '"2022-04-27"'),
394+
("[Date!]", '["2022-04-27"]'),
395+
# These are kind of useless, but covers some code path
396+
("Boolean!", "true"),
397+
("Color", "null"),
398+
("Color!", "GREEN"),
399+
("[Color!]", "[GREEN]"),
400+
("[Color!]", "null"),
401+
),
402+
)
403+
@pytest.mark.parametrize(
404+
"format_kwargs",
405+
(
406+
lambda x: {
407+
"inner_type_name": x["type_name"],
408+
"inner_default": x["default"],
409+
"outer_type_name": x["type_name"],
410+
"outer_default": x["default"],
411+
},
412+
lambda x: {
413+
"inner_type_name": x["type_name"],
414+
"inner_default": x["default"],
415+
"outer_type_name": "InputData!",
416+
"outer_default": f'{{ inner: {x["default"]} }}',
417+
},
418+
),
419+
)
420+
def test_default_values(validate_operation, type_name, default, format_kwargs):
421+
# When the input schema contains nodes with default values
422+
schema = """
423+
scalar Date
424+
425+
enum Color {{
426+
RED
427+
GREEN
428+
BLUE
429+
}}
430+
431+
input InputData {{
432+
inner: {inner_type_name} = {inner_default},
433+
}}
434+
435+
type Query {{
436+
getValue(arg: {outer_type_name} = {outer_default}): Int!
437+
}}
438+
""".format(
439+
**format_kwargs({"type_name": type_name, "default": default})
440+
)
441+
strategy = gql_st.queries(schema, custom_scalars={"Date": st.just("2022-04-26").map(nodes.String)})
442+
# Then these default values should be used in generated queries
443+
444+
all_valid = True
445+
446+
def validate_and_find(query):
447+
nonlocal all_valid
448+
try:
449+
validate_operation(schema, query)
450+
except AssertionError:
451+
all_valid = False
452+
return default in query
453+
454+
find(strategy, validate_and_find)
455+
assert all_valid

0 commit comments

Comments
 (0)