mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-02 04:34:13 +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
|
||||
|
||||
|
||||
# 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
|
||||
|
@ -21,7 +46,7 @@ class InputObjectTypeContainer(dict, BaseType): # type: ignore
|
|||
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
|
||||
|
|
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 ..field import Field
|
||||
from ..inputfield import InputField
|
||||
|
@ -6,6 +8,7 @@ from ..objecttype import ObjectType
|
|||
from ..scalars import Boolean, String
|
||||
from ..schema import Schema
|
||||
from ..unmountedtype import UnmountedType
|
||||
from ... import NonNull
|
||||
|
||||
|
||||
class MyType:
|
||||
|
@ -136,3 +139,31 @@ def test_inputobjecttype_of_input():
|
|||
|
||||
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}
|
||||
|
|
|
@ -20,8 +20,8 @@ from ..inputobjecttype import InputObjectType
|
|||
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):
|
||||
|
@ -227,6 +227,18 @@ def test_inputobject():
|
|||
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"""
|
||||
|
|
|
@ -30,7 +30,7 @@ try:
|
|||
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
|
||||
|
@ -82,7 +82,7 @@ def depth_limit_validator(
|
|||
|
||||
|
||||
def get_fragments(
|
||||
definitions: List[DefinitionNode],
|
||||
definitions: Tuple[DefinitionNode, ...],
|
||||
) -> Dict[str, FragmentDefinitionNode]:
|
||||
fragments = {}
|
||||
for definition in definitions:
|
||||
|
@ -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 = {}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user