From 8ede21e06381c096589c424960a6cfaca304badb Mon Sep 17 00:00:00 2001 From: Firas Kafri <3097061+firaskafri@users.noreply.github.com> Date: Thu, 25 May 2023 13:21:55 +0300 Subject: [PATCH 1/8] chore: default enum description to "An enumeration." (#1502) * Default enum description to "An enumeration." default to this string, which is used in many tests, is causing * Use the docstring descriptions of enums when they are present * Added tests * chore: add missing newline * Fix new line --------- Co-authored-by: Erik Wrede --- graphene/types/enum.py | 2 +- graphene/types/tests/test_enum.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 58e65c69..7d68ccd4 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -63,7 +63,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, diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 9b3082df..e6fce66c 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -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: From 2da8e9db5cd6527ca740914ce0095e5004054dfd Mon Sep 17 00:00:00 2001 From: Cadu Date: Sun, 4 Jun 2023 18:01:05 -0300 Subject: [PATCH 2/8] 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. --- graphene/types/inputobjecttype.py | 27 ++++++++++++++++- graphene/types/tests/conftest.py | 12 ++++++++ graphene/types/tests/test_inputobjecttype.py | 31 ++++++++++++++++++++ graphene/types/tests/test_type_map.py | 14 ++++++++- graphene/validation/depth_limit.py | 6 ++-- 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 graphene/types/tests/conftest.py diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 5d278510..fdf38ba0 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -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. 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 diff --git a/graphene/types/tests/conftest.py b/graphene/types/tests/conftest.py new file mode 100644 index 00000000..43f7d726 --- /dev/null +++ b/graphene/types/tests/conftest.py @@ -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) diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index 0fb7e394..0d7bcf80 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -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} diff --git a/graphene/types/tests/test_type_map.py b/graphene/types/tests/test_type_map.py index 55b1706e..55665b6b 100644 --- a/graphene/types/tests/test_type_map.py +++ b/graphene/types/tests/test_type_map.py @@ -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""" diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index b4599e66..e0f28663 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -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 = {} From c636d984c646cf303303f4c5bdb35e5d27846436 Mon Sep 17 00:00:00 2001 From: senseysensor Date: Mon, 5 Jun 2023 00:10:05 +0300 Subject: [PATCH 3/8] fix: Corrected enum metaclass to fix pickle.dumps() (#1495) * Corrected enum metaclass to fix pickle.dumps() * considered case with colliding class names (try to distinguish by file name) * reverted simple solution back (without attempt to support duplicate Enum class names) --------- Co-authored-by: sgrekov Co-authored-by: Erik Wrede --- graphene/tests/issues/test_881.py | 27 +++++++++++++++++++++++++++ graphene/types/enum.py | 4 +++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 graphene/tests/issues/test_881.py diff --git a/graphene/tests/issues/test_881.py b/graphene/tests/issues/test_881.py new file mode 100644 index 00000000..f97b5917 --- /dev/null +++ b/graphene/tests/issues/test_881.py @@ -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 diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 7d68ccd4..d3469a15 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -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) From d77d0b057137452d6d93067002fd7a2c56164e75 Mon Sep 17 00:00:00 2001 From: Jeongseok Kang Date: Mon, 5 Jun 2023 06:49:26 +0900 Subject: [PATCH 4/8] chore: Use `typing.TYPE_CHECKING` instead of MYPY (#1503) Co-authored-by: Erik Wrede --- graphene/types/inputobjecttype.py | 7 ++++--- graphene/types/interface.py | 7 ++++--- graphene/types/mutation.py | 7 ++++--- graphene/types/objecttype.py | 7 ++++--- graphene/types/union.py | 7 ++++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index fdf38ba0..257f48be 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -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 diff --git a/graphene/types/interface.py b/graphene/types/interface.py index 6503b78b..31bcc7f9 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -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 diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index ad47c62a..2de21b36 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -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 diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index 1ff29a2e..b3b829fe 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -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 diff --git a/graphene/types/union.py b/graphene/types/union.py index f77e833a..b7c5dc62 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -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 From 03cf2e131e655402ccc0a9e2d9897c39d7f7f86a Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Tue, 6 Jun 2023 20:45:01 +0200 Subject: [PATCH 5/8] chore: remove travis ci link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ba0737d..7beb975c 100644 --- a/README.md +++ b/README.md @@ -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) From 99f0103e37ab5846c3d8678ff5ae17a04572266f Mon Sep 17 00:00:00 2001 From: Ransom Williams <3261168+ransomw@users.noreply.github.com> Date: Wed, 19 Jul 2023 02:00:30 -0500 Subject: [PATCH 6/8] test: print schema with InputObjectType with DateTime field with default_value (#1293) (#1513) * test [1293]: regression test print schema with InputObjectType with DateTime field with default_value * chore: clarify test title and assertion --------- Co-authored-by: Erik Wrede --- graphene/tests/issues/test_1293.py | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 graphene/tests/issues/test_1293.py diff --git a/graphene/tests/issues/test_1293.py b/graphene/tests/issues/test_1293.py new file mode 100644 index 00000000..20bcde95 --- /dev/null +++ b/graphene/tests/issues/test_1293.py @@ -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" From 74db349da4fb72268b309b674361ffbd9e8885e0 Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Wed, 19 Jul 2023 16:01:00 +0900 Subject: [PATCH 7/8] docs: add get_human function (#1380) Co-authored-by: Erik Wrede --- docs/types/objecttypes.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 3cc8d830..f142d4a6 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -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() From 6b8cd2dc780c503ca7742314900f19d91c164d97 Mon Sep 17 00:00:00 2001 From: Dulmandakh Date: Wed, 19 Jul 2023 15:08:19 +0800 Subject: [PATCH 8/8] ci: drop python 3.6 (#1507) Co-authored-by: Erik Wrede --- .github/workflows/tests.yml | 3 +-- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6635a35b..5162d051 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/tox.ini b/tox.ini index 65fceadd..872d528c 100644 --- a/tox.ini +++ b/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