mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-02 12:44:15 +03:00
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.
This commit is contained in:
parent
8ede21e063
commit
2da8e9db5c
|
@ -14,6 +14,31 @@ class InputObjectTypeOptions(BaseOptions):
|
||||||
container = None # type: InputObjectTypeContainer
|
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 InputObjectTypeContainer(dict, BaseType): # type: ignore
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -21,7 +46,7 @@ class InputObjectTypeContainer(dict, BaseType): # type: ignore
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
dict.__init__(self, *args, **kwargs)
|
dict.__init__(self, *args, **kwargs)
|
||||||
for key in self._meta.fields:
|
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):
|
def __init_subclass__(cls, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
12
graphene/types/tests/conftest.py
Normal file
12
graphene/types/tests/conftest.py
Normal file
|
@ -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)
|
|
@ -1,3 +1,5 @@
|
||||||
|
from graphql import Undefined
|
||||||
|
|
||||||
from ..argument import Argument
|
from ..argument import Argument
|
||||||
from ..field import Field
|
from ..field import Field
|
||||||
from ..inputfield import InputField
|
from ..inputfield import InputField
|
||||||
|
@ -6,6 +8,7 @@ from ..objecttype import ObjectType
|
||||||
from ..scalars import Boolean, String
|
from ..scalars import Boolean, String
|
||||||
from ..schema import Schema
|
from ..schema import Schema
|
||||||
from ..unmountedtype import UnmountedType
|
from ..unmountedtype import UnmountedType
|
||||||
|
from ... import NonNull
|
||||||
|
|
||||||
|
|
||||||
class MyType:
|
class MyType:
|
||||||
|
@ -136,3 +139,31 @@ def test_inputobjecttype_of_input():
|
||||||
|
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"isChild": True}
|
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}
|
||||||
|
|
|
@ -20,8 +20,8 @@ from ..inputobjecttype import InputObjectType
|
||||||
from ..interface import Interface
|
from ..interface import Interface
|
||||||
from ..objecttype import ObjectType
|
from ..objecttype import ObjectType
|
||||||
from ..scalars import Int, String
|
from ..scalars import Int, String
|
||||||
from ..structures import List, NonNull
|
|
||||||
from ..schema import Schema
|
from ..schema import Schema
|
||||||
|
from ..structures import List, NonNull
|
||||||
|
|
||||||
|
|
||||||
def create_type_map(types, auto_camelcase=True):
|
def create_type_map(types, auto_camelcase=True):
|
||||||
|
@ -227,6 +227,18 @@ def test_inputobject():
|
||||||
assert foo_field.description == "Field description"
|
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():
|
def test_objecttype_camelcase():
|
||||||
class MyObjectType(ObjectType):
|
class MyObjectType(ObjectType):
|
||||||
"""Description"""
|
"""Description"""
|
||||||
|
|
|
@ -30,7 +30,7 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# backwards compatibility for v3.6
|
# backwards compatibility for v3.6
|
||||||
from typing import Pattern
|
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 import GraphQLError
|
||||||
from graphql.validation import ValidationContext, ValidationRule
|
from graphql.validation import ValidationContext, ValidationRule
|
||||||
|
@ -82,7 +82,7 @@ def depth_limit_validator(
|
||||||
|
|
||||||
|
|
||||||
def get_fragments(
|
def get_fragments(
|
||||||
definitions: List[DefinitionNode],
|
definitions: Tuple[DefinitionNode, ...],
|
||||||
) -> Dict[str, FragmentDefinitionNode]:
|
) -> Dict[str, FragmentDefinitionNode]:
|
||||||
fragments = {}
|
fragments = {}
|
||||||
for definition in definitions:
|
for definition in definitions:
|
||||||
|
@ -94,7 +94,7 @@ def get_fragments(
|
||||||
# This will actually get both queries and mutations.
|
# This will actually get both queries and mutations.
|
||||||
# We can basically treat those the same
|
# We can basically treat those the same
|
||||||
def get_queries_and_mutations(
|
def get_queries_and_mutations(
|
||||||
definitions: List[DefinitionNode],
|
definitions: Tuple[DefinitionNode, ...],
|
||||||
) -> Dict[str, OperationDefinitionNode]:
|
) -> Dict[str, OperationDefinitionNode]:
|
||||||
operations = {}
|
operations = {}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user