mirror of
				https://github.com/graphql-python/graphene.git
				synced 2025-10-31 07:57:26 +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