mirror of
https://github.com/graphql-python/graphene.git
synced 2025-07-16 19:12:21 +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
|
||||
matrix:
|
||||
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.9', python: '3.9', os: ubuntu-latest, tox: py39}
|
||||
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
|
||||
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
|
||||
- {name: '3.6', python: '3.6', os: ubuntu-20.04, tox: py36}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- 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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
def get_human(name):
|
||||
first_name, last_name = name.split()
|
||||
return Person(first_name, last_name)
|
||||
|
||||
class Person(ObjectType):
|
||||
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.
|
||||
enum_members.pop("Meta", None)
|
||||
enum = PyEnum(cls.__name__, enum_members)
|
||||
return SubclassWithMeta_Meta.__new__(
|
||||
obj = SubclassWithMeta_Meta.__new__(
|
||||
cls, name_, bases, dict(classdict, __enum__=enum), **options
|
||||
)
|
||||
globals()[name_] = obj.__enum__
|
||||
return obj
|
||||
|
||||
def get(cls, value):
|
||||
return cls._meta.enum(value)
|
||||
|
@ -63,7 +65,7 @@ class EnumMeta(SubclassWithMeta_Meta):
|
|||
cls, enum, name=None, description=None, deprecation_reason=None
|
||||
): # noqa: N805
|
||||
name = name or enum.__name__
|
||||
description = description or enum.__doc__
|
||||
description = description or enum.__doc__ or "An enumeration."
|
||||
meta_dict = {
|
||||
"enum": enum,
|
||||
"description": description,
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from .base import BaseOptions, BaseType
|
||||
from .inputfield import InputField
|
||||
from .unmountedtype import UnmountedType
|
||||
from .utils import yank_fields_from_attrs
|
||||
|
||||
# For static type checking with Mypy
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# For static type checking with type checker
|
||||
if TYPE_CHECKING:
|
||||
from typing import Dict, Callable # NOQA
|
||||
|
||||
|
||||
|
@ -14,6 +15,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 +47,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
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from .base import BaseOptions, BaseType
|
||||
from .field import Field
|
||||
from .utils import yank_fields_from_attrs
|
||||
|
||||
# For static type checking with Mypy
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# For static type checking with type checker
|
||||
if TYPE_CHECKING:
|
||||
from typing import Dict, Iterable, Type # NOQA
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..utils.deprecated import warn_deprecation
|
||||
from ..utils.get_unbound_function import get_unbound_function
|
||||
from ..utils.props import props
|
||||
|
@ -6,9 +8,8 @@ from .objecttype import ObjectType, ObjectTypeOptions
|
|||
from .utils import yank_fields_from_attrs
|
||||
from .interface import Interface
|
||||
|
||||
# For static type checking with Mypy
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# For static type checking with type checker
|
||||
if TYPE_CHECKING:
|
||||
from .argument import Argument # 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 .field import Field
|
||||
from .interface import Interface
|
||||
|
@ -7,9 +9,8 @@ try:
|
|||
from dataclasses import make_dataclass, field
|
||||
except ImportError:
|
||||
from ..pyutils.dataclasses import make_dataclass, field # type: ignore
|
||||
# For static type checking with Mypy
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# For static type checking with type checker
|
||||
if TYPE_CHECKING:
|
||||
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
|
||||
|
||||
|
||||
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 custom_description(value):
|
||||
if not value:
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from .base import BaseOptions, BaseType
|
||||
from .unmountedtype import UnmountedType
|
||||
|
||||
# For static type checking with Mypy
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# For static type checking with type checker
|
||||
if TYPE_CHECKING:
|
||||
from .objecttype import ObjectType # NOQA
|
||||
from typing import Iterable, Type # NOQA
|
||||
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -1,5 +1,5 @@
|
|||
[tox]
|
||||
envlist = py3{6,7,8,9,10}, mypy, pre-commit
|
||||
envlist = py3{7,8,9,10,11}, mypy, pre-commit
|
||||
skipsdist = true
|
||||
|
||||
[testenv]
|
||||
|
@ -8,7 +8,7 @@ deps =
|
|||
setenv =
|
||||
PYTHONPATH = .:{envdir}
|
||||
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]
|
||||
basepython = python3.10
|
||||
|
|
Loading…
Reference in New Issue
Block a user