Skip to content

Commit 2da8e9d

Browse files
authored
feat: Enable use of Undefined in InputObjectTypes (#1506)
* Changed InputObjectType's default builder-from-dict argument to be `Undefined` instead of `None`, removing ambiguity of undefined optional inputs using dot notation access syntax. * Move `set_default_input_object_type_to_undefined()` fixture into conftest.py for sharing it between multiple test files.
1 parent 8ede21e commit 2da8e9d

File tree

5 files changed

+85
-5
lines changed

5 files changed

+85
-5
lines changed

graphene/types/inputobjecttype.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,39 @@ class InputObjectTypeOptions(BaseOptions):
1414
container = None # type: InputObjectTypeContainer
1515

1616

17+
# Currently in Graphene, we get a `None` whenever we access an (optional) field that was not set in an InputObjectType
18+
# using the InputObjectType.<attribute> dot access syntax. This is ambiguous, because in this current (Graphene
19+
# historical) arrangement, we cannot distinguish between a field not being set and a field being set to None.
20+
# At the same time, we shouldn't break existing code that expects a `None` when accessing a field that was not set.
21+
_INPUT_OBJECT_TYPE_DEFAULT_VALUE = None
22+
23+
# To mitigate this, we provide the function `set_input_object_type_default_value` to allow users to change the default
24+
# value returned in non-specified fields in InputObjectType to another meaningful sentinel value (e.g. Undefined)
25+
# if they want to. This way, we can keep code that expects a `None` working while we figure out a better solution (or
26+
# a well-documented breaking change) for this issue.
27+
28+
29+
def set_input_object_type_default_value(default_value):
30+
"""
31+
Change the sentinel value returned by non-specified fields in an InputObjectType
32+
Useful to differentiate between a field not being set and a field being set to None by using a sentinel value
33+
(e.g. Undefined is a good sentinel value for this purpose)
34+
35+
This function should be called at the beginning of the app or in some other place where it is guaranteed to
36+
be called before any InputObjectType is defined.
37+
"""
38+
global _INPUT_OBJECT_TYPE_DEFAULT_VALUE
39+
_INPUT_OBJECT_TYPE_DEFAULT_VALUE = default_value
40+
41+
1742
class InputObjectTypeContainer(dict, BaseType): # type: ignore
1843
class Meta:
1944
abstract = True
2045

2146
def __init__(self, *args, **kwargs):
2247
dict.__init__(self, *args, **kwargs)
2348
for key in self._meta.fields:
24-
setattr(self, key, self.get(key, None))
49+
setattr(self, key, self.get(key, _INPUT_OBJECT_TYPE_DEFAULT_VALUE))
2550

2651
def __init_subclass__(cls, *args, **kwargs):
2752
pass

graphene/types/tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import pytest
2+
from graphql import Undefined
3+
4+
from graphene.types.inputobjecttype import set_input_object_type_default_value
5+
6+
7+
@pytest.fixture()
8+
def set_default_input_object_type_to_undefined():
9+
"""This fixture is used to change the default value of optional inputs in InputObjectTypes for specific tests"""
10+
set_input_object_type_default_value(Undefined)
11+
yield
12+
set_input_object_type_default_value(None)

graphene/types/tests/test_inputobjecttype.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from graphql import Undefined
2+
13
from ..argument import Argument
24
from ..field import Field
35
from ..inputfield import InputField
@@ -6,6 +8,7 @@
68
from ..scalars import Boolean, String
79
from ..schema import Schema
810
from ..unmountedtype import UnmountedType
11+
from ... import NonNull
912

1013

1114
class MyType:
@@ -136,3 +139,31 @@ def resolve_is_child(self, info, parent):
136139

137140
assert not result.errors
138141
assert result.data == {"isChild": True}
142+
143+
144+
def test_inputobjecttype_default_input_as_undefined(
145+
set_default_input_object_type_to_undefined,
146+
):
147+
class TestUndefinedInput(InputObjectType):
148+
required_field = String(required=True)
149+
optional_field = String()
150+
151+
class Query(ObjectType):
152+
undefined_optionals_work = Field(NonNull(Boolean), input=TestUndefinedInput())
153+
154+
def resolve_undefined_optionals_work(self, info, input: TestUndefinedInput):
155+
# Confirm that optional_field comes as Undefined
156+
return (
157+
input.required_field == "required" and input.optional_field is Undefined
158+
)
159+
160+
schema = Schema(query=Query)
161+
result = schema.execute(
162+
"""query basequery {
163+
undefinedOptionalsWork(input: {requiredField: "required"})
164+
}
165+
"""
166+
)
167+
168+
assert not result.errors
169+
assert result.data == {"undefinedOptionalsWork": True}

graphene/types/tests/test_type_map.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from ..interface import Interface
2121
from ..objecttype import ObjectType
2222
from ..scalars import Int, String
23-
from ..structures import List, NonNull
2423
from ..schema import Schema
24+
from ..structures import List, NonNull
2525

2626

2727
def create_type_map(types, auto_camelcase=True):
@@ -227,6 +227,18 @@ def resolve_foo_bar(self, args, info):
227227
assert foo_field.description == "Field description"
228228

229229

230+
def test_inputobject_undefined(set_default_input_object_type_to_undefined):
231+
class OtherObjectType(InputObjectType):
232+
optional_field = String()
233+
234+
type_map = create_type_map([OtherObjectType])
235+
assert "OtherObjectType" in type_map
236+
graphql_type = type_map["OtherObjectType"]
237+
238+
container = graphql_type.out_type({})
239+
assert container.optional_field is Undefined
240+
241+
230242
def test_objecttype_camelcase():
231243
class MyObjectType(ObjectType):
232244
"""Description"""

graphene/validation/depth_limit.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
except ImportError:
3131
# backwards compatibility for v3.6
3232
from typing import Pattern
33-
from typing import Callable, Dict, List, Optional, Union
33+
from typing import Callable, Dict, List, Optional, Union, Tuple
3434

3535
from graphql import GraphQLError
3636
from graphql.validation import ValidationContext, ValidationRule
@@ -82,7 +82,7 @@ def __init__(self, validation_context: ValidationContext):
8282

8383

8484
def get_fragments(
85-
definitions: List[DefinitionNode],
85+
definitions: Tuple[DefinitionNode, ...],
8686
) -> Dict[str, FragmentDefinitionNode]:
8787
fragments = {}
8888
for definition in definitions:
@@ -94,7 +94,7 @@ def get_fragments(
9494
# This will actually get both queries and mutations.
9595
# We can basically treat those the same
9696
def get_queries_and_mutations(
97-
definitions: List[DefinitionNode],
97+
definitions: Tuple[DefinitionNode, ...],
9898
) -> Dict[str, OperationDefinitionNode]:
9999
operations = {}
100100

0 commit comments

Comments
 (0)