Merge branch 'master' into master

This commit is contained in:
Erik Wrede 2023-07-19 09:12:36 +02:00 committed by GitHub
commit 035f19c0aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 200 additions and 27 deletions

View File

@ -25,12 +25,11 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- {name: '3.11', python: '3.11-dev', os: ubuntu-latest, tox: py311} - {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311}
- {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310}
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
- {name: '3.6', python: '3.6', os: ubuntu-20.04, tox: py36}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4

View File

@ -1,4 +1,4 @@
# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) [![](https://dcbadge.vercel.app/api/server/T6Gp6NFYHe?style=flat)](https://discord.gg/T6Gp6NFYHe) # ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) [![](https://dcbadge.vercel.app/api/server/T6Gp6NFYHe?style=flat)](https://discord.gg/T6Gp6NFYHe)
[💬 Join the community on Discord](https://discord.gg/T6Gp6NFYHe) [💬 Join the community on Discord](https://discord.gg/T6Gp6NFYHe)

View File

@ -80,6 +80,10 @@ If we have a schema with Person type and one field on the root query.
from graphene import ObjectType, String, Field from graphene import ObjectType, String, Field
def get_human(name):
first_name, last_name = name.split()
return Person(first_name, last_name)
class Person(ObjectType): class Person(ObjectType):
full_name = String() full_name = String()

View File

@ -0,0 +1,41 @@
# https://github.com/graphql-python/graphene/issues/1293
import datetime
import graphene
from graphql.utilities import print_schema
class Filters(graphene.InputObjectType):
datetime_after = graphene.DateTime(
required=False,
default_value=datetime.datetime.utcfromtimestamp(1434549820776 / 1000),
)
datetime_before = graphene.DateTime(
required=False,
default_value=datetime.datetime.utcfromtimestamp(1444549820776 / 1000),
)
class SetDatetime(graphene.Mutation):
class Arguments:
filters = Filters(required=True)
ok = graphene.Boolean()
def mutate(root, info, filters):
return SetDatetime(ok=True)
class Query(graphene.ObjectType):
goodbye = graphene.String()
class Mutations(graphene.ObjectType):
set_datetime = SetDatetime.Field()
def test_schema_printable_with_default_datetime_value():
schema = graphene.Schema(query=Query, mutation=Mutations)
schema_str = print_schema(schema.graphql_schema)
assert schema_str, "empty schema printed"

View File

@ -0,0 +1,27 @@
import pickle
from ...types.enum import Enum
class PickleEnum(Enum):
# is defined outside of test because pickle unable to dump class inside ot pytest function
A = "a"
B = 1
def test_enums_pickling():
a = PickleEnum.A
pickled = pickle.dumps(a)
restored = pickle.loads(pickled)
assert type(a) is type(restored)
assert a == restored
assert a.value == restored.value
assert a.name == restored.name
b = PickleEnum.B
pickled = pickle.dumps(b)
restored = pickle.loads(pickled)
assert type(a) is type(restored)
assert b == restored
assert b.value == restored.value
assert b.name == restored.name

View File

@ -31,9 +31,11 @@ class EnumMeta(SubclassWithMeta_Meta):
# with the enum values. # with the enum values.
enum_members.pop("Meta", None) enum_members.pop("Meta", None)
enum = PyEnum(cls.__name__, enum_members) enum = PyEnum(cls.__name__, enum_members)
return SubclassWithMeta_Meta.__new__( obj = SubclassWithMeta_Meta.__new__(
cls, name_, bases, dict(classdict, __enum__=enum), **options cls, name_, bases, dict(classdict, __enum__=enum), **options
) )
globals()[name_] = obj.__enum__
return obj
def get(cls, value): def get(cls, value):
return cls._meta.enum(value) return cls._meta.enum(value)
@ -63,7 +65,7 @@ class EnumMeta(SubclassWithMeta_Meta):
cls, enum, name=None, description=None, deprecation_reason=None cls, enum, name=None, description=None, deprecation_reason=None
): # noqa: N805 ): # noqa: N805
name = name or enum.__name__ name = name or enum.__name__
description = description or enum.__doc__ description = description or enum.__doc__ or "An enumeration."
meta_dict = { meta_dict = {
"enum": enum, "enum": enum,
"description": description, "description": description,

View File

@ -1,11 +1,12 @@
from typing import TYPE_CHECKING
from .base import BaseOptions, BaseType from .base import BaseOptions, BaseType
from .inputfield import InputField from .inputfield import InputField
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
from .utils import yank_fields_from_attrs from .utils import yank_fields_from_attrs
# For static type checking with Mypy # For static type checking with type checker
MYPY = False if TYPE_CHECKING:
if MYPY:
from typing import Dict, Callable # NOQA from typing import Dict, Callable # NOQA
@ -14,6 +15,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 +47,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

View File

@ -1,10 +1,11 @@
from typing import TYPE_CHECKING
from .base import BaseOptions, BaseType from .base import BaseOptions, BaseType
from .field import Field from .field import Field
from .utils import yank_fields_from_attrs from .utils import yank_fields_from_attrs
# For static type checking with Mypy # For static type checking with type checker
MYPY = False if TYPE_CHECKING:
if MYPY:
from typing import Dict, Iterable, Type # NOQA from typing import Dict, Iterable, Type # NOQA

View File

@ -1,3 +1,5 @@
from typing import TYPE_CHECKING
from ..utils.deprecated import warn_deprecation from ..utils.deprecated import warn_deprecation
from ..utils.get_unbound_function import get_unbound_function from ..utils.get_unbound_function import get_unbound_function
from ..utils.props import props from ..utils.props import props
@ -6,9 +8,8 @@ from .objecttype import ObjectType, ObjectTypeOptions
from .utils import yank_fields_from_attrs from .utils import yank_fields_from_attrs
from .interface import Interface from .interface import Interface
# For static type checking with Mypy # For static type checking with type checker
MYPY = False if TYPE_CHECKING:
if MYPY:
from .argument import Argument # NOQA from .argument import Argument # NOQA
from typing import Dict, Type, Callable, Iterable # NOQA from typing import Dict, Type, Callable, Iterable # NOQA

View File

@ -1,3 +1,5 @@
from typing import TYPE_CHECKING
from .base import BaseOptions, BaseType, BaseTypeMeta from .base import BaseOptions, BaseType, BaseTypeMeta
from .field import Field from .field import Field
from .interface import Interface from .interface import Interface
@ -7,9 +9,8 @@ try:
from dataclasses import make_dataclass, field from dataclasses import make_dataclass, field
except ImportError: except ImportError:
from ..pyutils.dataclasses import make_dataclass, field # type: ignore from ..pyutils.dataclasses import make_dataclass, field # type: ignore
# For static type checking with Mypy # For static type checking with type checker
MYPY = False if TYPE_CHECKING:
if MYPY:
from typing import Dict, Iterable, Type # NOQA from typing import Dict, Iterable, Type # NOQA

View 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)

View File

@ -65,6 +65,21 @@ def test_enum_from_builtin_enum():
assert RGB.BLUE assert RGB.BLUE
def test_enum_custom_description_in_constructor():
description = "An enumeration, but with a custom description"
RGB = Enum(
"RGB",
"RED,GREEN,BLUE",
description=description,
)
assert RGB._meta.description == description
def test_enum_from_python3_enum_uses_default_builtin_doc():
RGB = Enum("RGB", "RED,GREEN,BLUE")
assert RGB._meta.description == "An enumeration."
def test_enum_from_builtin_enum_accepts_lambda_description(): def test_enum_from_builtin_enum_accepts_lambda_description():
def custom_description(value): def custom_description(value):
if not value: if not value:

View File

@ -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}

View File

@ -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"""

View File

@ -1,9 +1,10 @@
from typing import TYPE_CHECKING
from .base import BaseOptions, BaseType from .base import BaseOptions, BaseType
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
# For static type checking with Mypy # For static type checking with type checker
MYPY = False if TYPE_CHECKING:
if MYPY:
from .objecttype import ObjectType # NOQA from .objecttype import ObjectType # NOQA
from typing import Iterable, Type # NOQA from typing import Iterable, Type # NOQA

View File

@ -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 = {}

View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py3{6,7,8,9,10}, mypy, pre-commit envlist = py3{7,8,9,10,11}, mypy, pre-commit
skipsdist = true skipsdist = true
[testenv] [testenv]
@ -8,7 +8,7 @@ deps =
setenv = setenv =
PYTHONPATH = .:{envdir} PYTHONPATH = .:{envdir}
commands = commands =
py{36,37,38,39,310}: pytest --cov=graphene graphene --cov-report=term --cov-report=xml examples {posargs} py{37,38,39,310,311}: pytest --cov=graphene graphene --cov-report=term --cov-report=xml examples {posargs}
[testenv:pre-commit] [testenv:pre-commit]
basepython = python3.10 basepython = python3.10