mirror of
https://github.com/graphql-python/graphene.git
synced 2025-07-18 03:52:24 +03:00
Merge branch 'master' into master
This commit is contained in:
commit
035f19c0aa
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#  [Graphene](http://graphene-python.org) [](https://travis-ci.org/graphql-python/graphene) [](https://badge.fury.io/py/graphene) [](https://coveralls.io/github/graphql-python/graphene?branch=master) [](https://discord.gg/T6Gp6NFYHe)
|
#  [Graphene](http://graphene-python.org) [](https://badge.fury.io/py/graphene) [](https://coveralls.io/github/graphql-python/graphene?branch=master) [](https://discord.gg/T6Gp6NFYHe)
|
||||||
|
|
||||||
[💬 Join the community on Discord](https://discord.gg/T6Gp6NFYHe)
|
[💬 Join the community on Discord](https://discord.gg/T6Gp6NFYHe)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
41
graphene/tests/issues/test_1293.py
Normal file
41
graphene/tests/issues/test_1293.py
Normal 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"
|
27
graphene/tests/issues/test_881.py
Normal file
27
graphene/tests/issues/test_881.py
Normal 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
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
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)
|
|
@ -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:
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
|
||||||
|
|
4
tox.ini
4
tox.ini
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user