diff --git a/docs/index.rst b/docs/index.rst index 54f1f99c..85905788 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,12 +1,6 @@ Graphene ======== ------------- - -The documentation below is for the ``dev`` (prerelease) version of Graphene. To view the documentation for the latest stable Graphene version go to the `v2 docs `_. - ------------- - Contents: .. toctree:: diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index f47fffea..2a55245d 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -271,7 +271,7 @@ The following is an example for creating a DateTime scalar: @staticmethod def parse_literal(node, _variables=None): - if isinstance(node, ast.StringValue): + if isinstance(node, ast.StringValueNode): return datetime.datetime.strptime( node.value, "%Y-%m-%dT%H:%M:%S.%f") diff --git a/graphene/__init__.py b/graphene/__init__.py index af83f059..b901506e 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -46,7 +46,7 @@ from .types import ( from .utils.module_loading import lazy_import from .utils.resolve_only_args import resolve_only_args -VERSION = (3, 1, 1, "final", 0) +VERSION = (3, 2, 1, "final", 0) __version__ = get_version(VERSION) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index f9dc843b..d9283c41 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -31,18 +31,22 @@ class Argument(MountedType): type (class for a graphene.UnmountedType): must be a class (not an instance) of an unmounted graphene type (ex. scalar or object) which is used for the type of this argument in the GraphQL schema. - required (bool): indicates this argument as not null in the graphql schema. Same behavior + required (optional, bool): indicates this argument as not null in the graphql schema. Same behavior as graphene.NonNull. Default False. - name (str): the name of the GraphQL argument. Defaults to parameter name. - description (str): the description of the GraphQL argument in the schema. - default_value (Any): The value to be provided if the user does not set this argument in + name (optional, str): the name of the GraphQL argument. Defaults to parameter name. + description (optional, str): the description of the GraphQL argument in the schema. + default_value (optional, Any): The value to be provided if the user does not set this argument in the operation. + deprecation_reason (optional, str): Setting this value indicates that the argument is + depreciated and may provide instruction or reason on how for clients to proceed. Cannot be + set if the argument is required (see spec). """ def __init__( self, type_, default_value=Undefined, + deprecation_reason=None, description=None, name=None, required=False, @@ -51,12 +55,16 @@ class Argument(MountedType): super(Argument, self).__init__(_creation_counter=_creation_counter) if required: + assert ( + deprecation_reason is None + ), f"Argument {name} is required, cannot deprecate it." type_ = NonNull(type_) self.name = name self._type = type_ self.default_value = default_value self.description = description + self.deprecation_reason = deprecation_reason @property def type(self): @@ -68,6 +76,7 @@ class Argument(MountedType): and self.type == other.type and self.default_value == other.default_value and self.description == other.description + and self.deprecation_reason == other.deprecation_reason ) diff --git a/graphene/types/inputfield.py b/graphene/types/inputfield.py index 791ca6a4..e7ededb0 100644 --- a/graphene/types/inputfield.py +++ b/graphene/types/inputfield.py @@ -55,11 +55,14 @@ class InputField(MountedType): description=None, required=False, _creation_counter=None, - **extra_args + **extra_args, ): super(InputField, self).__init__(_creation_counter=_creation_counter) self.name = name if required: + assert ( + deprecation_reason is None + ), f"InputField {name} is required, cannot deprecate it." type_ = NonNull(type_) self._type = type_ self.deprecation_reason = deprecation_reason diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 1a33a93d..bceede6a 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -1,3 +1,4 @@ +from enum import Enum as PyEnum import inspect from functools import partial @@ -169,10 +170,16 @@ class TypeMap(dict): values = {} for name, value in graphene_type._meta.enum.__members__.items(): description = getattr(value, "description", None) - deprecation_reason = getattr(value, "deprecation_reason", None) + # if the "description" attribute is an Enum, it is likely an enum member + # called description, not a description property + if isinstance(description, PyEnum): + description = None if not description and callable(graphene_type._meta.description): description = graphene_type._meta.description(value) + deprecation_reason = getattr(value, "deprecation_reason", None) + if isinstance(deprecation_reason, PyEnum): + deprecation_reason = None if not deprecation_reason and callable( graphene_type._meta.deprecation_reason ): @@ -309,6 +316,7 @@ class TypeMap(dict): default_value=field.default_value, out_name=name, description=field.description, + deprecation_reason=field.deprecation_reason, ) else: args = {} @@ -320,6 +328,7 @@ class TypeMap(dict): out_name=arg_name, description=arg.description, default_value=arg.default_value, + deprecation_reason=arg.deprecation_reason, ) subscribe = field.wrap_subscribe( self.get_function_for_type( diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py index db4d6c24..c5521b6c 100644 --- a/graphene/types/tests/test_argument.py +++ b/graphene/types/tests/test_argument.py @@ -18,8 +18,20 @@ def test_argument(): def test_argument_comparasion(): - arg1 = Argument(String, name="Hey", description="Desc", default_value="default") - arg2 = Argument(String, name="Hey", description="Desc", default_value="default") + arg1 = Argument( + String, + name="Hey", + description="Desc", + default_value="default", + deprecation_reason="deprecated", + ) + arg2 = Argument( + String, + name="Hey", + description="Desc", + default_value="default", + deprecation_reason="deprecated", + ) assert arg1 == arg2 assert arg1 != String() @@ -40,6 +52,30 @@ def test_to_arguments(): } +def test_to_arguments_deprecated(): + args = {"unmounted_arg": String(required=False, deprecation_reason="deprecated")} + + my_args = to_arguments(args) + assert my_args == { + "unmounted_arg": Argument( + String, required=False, deprecation_reason="deprecated" + ), + } + + +def test_to_arguments_required_deprecated(): + args = { + "unmounted_arg": String( + required=True, name="arg", deprecation_reason="deprecated" + ) + } + + with raises(AssertionError) as exc_info: + to_arguments(args) + + assert str(exc_info.value) == "Argument arg is required, cannot deprecate it." + + def test_to_arguments_raises_if_field(): args = {"arg_string": Field(String)} diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 298cc233..9b3082df 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -565,3 +565,36 @@ def test_iterable_instance_creation_enum(): for c in TestEnum: result.append(c.name) assert result == expected_values + + +# https://github.com/graphql-python/graphene/issues/1321 +def test_enum_description_member_not_interpreted_as_property(): + class RGB(Enum): + """Description""" + + red = "red" + green = "green" + blue = "blue" + description = "description" + deprecation_reason = "deprecation_reason" + + class Query(ObjectType): + color = RGB() + + def resolve_color(_, info): + return RGB.description + + values = RGB._meta.enum.__members__.values() + assert sorted(v.name for v in values) == [ + "blue", + "deprecation_reason", + "description", + "green", + "red", + ] + + schema = Schema(query=Query) + + results = schema.execute("query { color }") + assert not results.errors + assert results.data["color"] == RGB.description.name diff --git a/graphene/types/tests/test_field.py b/graphene/types/tests/test_field.py index 669ada4f..f0401bfa 100644 --- a/graphene/types/tests/test_field.py +++ b/graphene/types/tests/test_field.py @@ -128,13 +128,20 @@ def test_field_name_as_argument(): def test_field_source_argument_as_kw(): MyType = object() - field = Field(MyType, b=NonNull(True), c=Argument(None), a=NonNull(False)) + deprecation_reason = "deprecated" + field = Field( + MyType, + b=NonNull(True), + c=Argument(None, deprecation_reason=deprecation_reason), + a=NonNull(False), + ) assert list(field.args) == ["b", "c", "a"] assert isinstance(field.args["b"], Argument) assert isinstance(field.args["b"].type, NonNull) assert field.args["b"].type.of_type is True assert isinstance(field.args["c"], Argument) assert field.args["c"].type is None + assert field.args["c"].deprecation_reason == deprecation_reason assert isinstance(field.args["a"], Argument) assert isinstance(field.args["a"].type, NonNull) assert field.args["a"].type.of_type is False diff --git a/graphene/types/tests/test_inputfield.py b/graphene/types/tests/test_inputfield.py index bfedfb05..9b100128 100644 --- a/graphene/types/tests/test_inputfield.py +++ b/graphene/types/tests/test_inputfield.py @@ -1,5 +1,7 @@ from functools import partial +from pytest import raises + from ..inputfield import InputField from ..structures import NonNull from .utils import MyLazyType @@ -12,6 +14,22 @@ def test_inputfield_required(): assert field.type.of_type == MyType +def test_inputfield_deprecated(): + MyType = object() + deprecation_reason = "deprecated" + field = InputField(MyType, required=False, deprecation_reason=deprecation_reason) + assert isinstance(field.type, type(MyType)) + assert field.deprecation_reason == deprecation_reason + + +def test_inputfield_required_deprecated(): + MyType = object() + with raises(AssertionError) as exc_info: + InputField(MyType, name="input", required=True, deprecation_reason="deprecated") + + assert str(exc_info.value) == "InputField input is required, cannot deprecate it." + + def test_inputfield_with_lazy_type(): MyType = object() field = InputField(lambda: MyType)