Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the user to change InputObjectType's default value on non-specified inputs to a sentinel value #1506

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion graphene/types/inputobjecttype.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,39 @@ class InputObjectTypeOptions(BaseOptions):
container = None # type: InputObjectTypeContainer


# Currently in Graphene, we get a `None` whenever we access an (optional) field that was not set in an InputObjectType
# using the InputObjectType.<attribute> dot access syntax. This is ambiguous, because in this current (Graphene
# historical) arrangement, we cannot distinguish between a field not being set and a field being set to None.
# At the same time, we shouldn't break existing code that expects a `None` when accessing a field that was not set.
_INPUT_OBJECT_TYPE_DEFAULT_VALUE = None

# To mitigate this, we provide the function `set_input_object_type_default_value` to allow users to change the default
# value returned in non-specified fields in InputObjectType to another meaningful sentinel value (e.g. Undefined)
# if they want to. This way, we can keep code that expects a `None` working while we figure out a better solution (or
# a well-documented breaking change) for this issue.


def set_input_object_type_default_value(default_value):
"""
Change the sentinel value returned by non-specified fields in an InputObjectType
Useful to differentiate between a field not being set and a field being set to None by using a sentinel value
(e.g. Undefined is a good sentinel value for this purpose)

This function should be called at the beginning of the app or in some other place where it is guaranteed to
be called before any InputObjectType is defined.
"""
global _INPUT_OBJECT_TYPE_DEFAULT_VALUE
_INPUT_OBJECT_TYPE_DEFAULT_VALUE = default_value


class InputObjectTypeContainer(dict, BaseType): # type: ignore
class Meta:
abstract = True

def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
for key in self._meta.fields:
setattr(self, key, self.get(key, None))
setattr(self, key, self.get(key, _INPUT_OBJECT_TYPE_DEFAULT_VALUE))

def __init_subclass__(cls, *args, **kwargs):
pass
Expand Down
12 changes: 12 additions & 0 deletions graphene/types/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pytest
from graphql import Undefined

from graphene.types.inputobjecttype import set_input_object_type_default_value


@pytest.fixture()
def set_default_input_object_type_to_undefined():
"""This fixture is used to change the default value of optional inputs in InputObjectTypes for specific tests"""
set_input_object_type_default_value(Undefined)
yield
set_input_object_type_default_value(None)
31 changes: 31 additions & 0 deletions graphene/types/tests/test_inputobjecttype.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from graphql import Undefined

from ..argument import Argument
from ..field import Field
from ..inputfield import InputField
Expand All @@ -6,6 +8,7 @@
from ..scalars import Boolean, String
from ..schema import Schema
from ..unmountedtype import UnmountedType
from ... import NonNull


class MyType:
Expand Down Expand Up @@ -136,3 +139,31 @@ def resolve_is_child(self, info, parent):

assert not result.errors
assert result.data == {"isChild": True}


def test_inputobjecttype_default_input_as_undefined(
set_default_input_object_type_to_undefined,
):
class TestUndefinedInput(InputObjectType):
required_field = String(required=True)
optional_field = String()

class Query(ObjectType):
undefined_optionals_work = Field(NonNull(Boolean), input=TestUndefinedInput())

def resolve_undefined_optionals_work(self, info, input: TestUndefinedInput):
# Confirm that optional_field comes as Undefined
return (
input.required_field == "required" and input.optional_field is Undefined
)

schema = Schema(query=Query)
result = schema.execute(
"""query basequery {
undefinedOptionalsWork(input: {requiredField: "required"})
}
"""
)

assert not result.errors
assert result.data == {"undefinedOptionalsWork": True}
14 changes: 13 additions & 1 deletion graphene/types/tests/test_type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
from ..interface import Interface
from ..objecttype import ObjectType
from ..scalars import Int, String
from ..structures import List, NonNull
from ..schema import Schema
from ..structures import List, NonNull


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


def test_inputobject_undefined(set_default_input_object_type_to_undefined):
class OtherObjectType(InputObjectType):
optional_field = String()

type_map = create_type_map([OtherObjectType])
assert "OtherObjectType" in type_map
graphql_type = type_map["OtherObjectType"]

container = graphql_type.out_type({})
assert container.optional_field is Undefined


def test_objecttype_camelcase():
class MyObjectType(ObjectType):
"""Description"""
Expand Down
6 changes: 3 additions & 3 deletions graphene/validation/depth_limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
except ImportError:
# backwards compatibility for v3.6
from typing import Pattern
from typing import Callable, Dict, List, Optional, Union
from typing import Callable, Dict, List, Optional, Union, Tuple

from graphql import GraphQLError
from graphql.validation import ValidationContext, ValidationRule
Expand Down Expand Up @@ -82,7 +82,7 @@ def __init__(self, validation_context: ValidationContext):


def get_fragments(
definitions: List[DefinitionNode],
definitions: Tuple[DefinitionNode, ...],
) -> Dict[str, FragmentDefinitionNode]:
fragments = {}
for definition in definitions:
Expand All @@ -94,7 +94,7 @@ def get_fragments(
# This will actually get both queries and mutations.
# We can basically treat those the same
def get_queries_and_mutations(
definitions: List[DefinitionNode],
definitions: Tuple[DefinitionNode, ...],
) -> Dict[str, OperationDefinitionNode]:
operations = {}

Expand Down