Repository: graphql-python/graphene
Pull request #1472 (2022-11-12 13:15:20)

Complete deprecated fields and arguments support
**Implementing enhancement from #1446**
In this PR I ensure that `Argument`, `Field` and `InputField` support **deprecation** functionality.

TODOs from the issue:

- [x]  Is this already supported in graphql-core?
- [ ] Add deprecation reason field to Meta Options Classes and [Input]Field logic 
       Skipped this one, as meta is not populated to the Mounted object.
- [x] Check that required NonNull input fields are not deprecatable (see spec)

Notes:
1. `Field` and `InputField` already had this field, whereas `Argument` didn't.
2. When constructing schema, some objects were already passing deprecation reason. I added the missing parts: `InputField` and `Argument`. `Enum` and `Field` were compliant.
3. When mounting `InputObject` or `Enum` **implicitly**, meta fields are not populated to the `Mounted` object. 

codecov[bot]: # [Codecov](https://codecov.io/gh/graphql-python/graphene/pull/1472?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) Report
Base: **95.88**% // Head: **95.91**% // Increases project coverage by **`+0.03%`** :tada:
> Coverage data is based on head [(`26e9db7`)](https://codecov.io/gh/graphql-python/graphene/pull/1472?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) compared to base [(`ccdd35b`)](https://codecov.io/gh/graphql-python/graphene/commit/ccdd35b354007c3899a7b62ffe132414ee226fec?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1472      +/-   ##
==========================================
+ Coverage   95.88%   95.91%   +0.03%     
==========================================
  Files          50       50              
  Lines        1724     1737      +13     
==========================================
+ Hits         1653     1666      +13     
  Misses         71       71              
```


| [Impacted Files](https://codecov.io/gh/graphql-python/graphene/pull/1472?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) | Coverage Δ | |
|---|---|---|
| [graphene/types/schema.py](https://codecov.io/gh/graphql-python/graphene/pull/1472/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python#diff-Z3JhcGhlbmUvdHlwZXMvc2NoZW1hLnB5) | `95.31% <ø> (ø)` | |
| [graphene/types/argument.py](https://codecov.io/gh/graphql-python/graphene/pull/1472/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python#diff-Z3JhcGhlbmUvdHlwZXMvYXJndW1lbnQucHk=) | `97.82% <100.00%> (+0.09%)` | :arrow_up: |
| [graphene/types/field.py](https://codecov.io/gh/graphql-python/graphene/pull/1472/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python#diff-Z3JhcGhlbmUvdHlwZXMvZmllbGQucHk=) | `96.07% <100.00%> (+0.07%)` | :arrow_up: |
| [graphene/types/inputfield.py](https://codecov.io/gh/graphql-python/graphene/pull/1472/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python#diff-Z3JhcGhlbmUvdHlwZXMvaW5wdXRmaWVsZC5weQ==) | `100.00% <100.00%> (ø)` | |
| [graphene/types/inputobjecttype.py](https://codecov.io/gh/graphql-python/graphene/pull/1472/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python#diff-Z3JhcGhlbmUvdHlwZXMvaW5wdXRvYmplY3R0eXBlLnB5) | `97.43% <100.00%> (+0.13%)` | :arrow_up: |
| [graphene/types/mountedtype.py](https://codecov.io/gh/graphql-python/graphene/pull/1472/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python#diff-Z3JhcGhlbmUvdHlwZXMvbW91bnRlZHR5cGUucHk=) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/graphql-python/graphene/pull/1472?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python).

erikwrede: That was fast! Thank you for the PR :) I'll check it out ASAP!
vhutov: @erikwrede I felt some courage to play with `MountedType` and `SubclassWithMeta` support. Now it's generic and works with `Enum`'s, `InputObject`'s and `Scalar`'s. I added a class method which describes which Meta fields to inherit. I think it's clear to a reader what to expect. LMKWYT
vhutov: @erikwrede Hey! Have you had a chance to take a look?
erikwrede: @vhutov Sorry for taking to long, I was actually just looking at your new changes right now. 
Need to play around with it to give you a more thorough opinion though, as this is had become a larger change than just adding the deprecation now. Trying to get back to you on the weekend 🙂 
vhutov: @erikwrede no worries, I'm not in rush. I just thought it'd be better to add Meta support as well. I can certainly split them in two, if you think it's better. Thanks for looking at this!
erikwrede: Having thought about it again, think it's better to split up the two PRs and think about the `mountable_meta` again. The current proposal would  also require users to adapt the new `mountable_meta` classmethod,  potentially breaking things by requiring them to implement `mountable_meta` for all `kwargs` they want to be inheritable.

If we are talking about a redesign of that part, I'd personally prefer to explicitly get the inheritable fields from a type, without having to implement a second `mountable_meta` method.
That configuration, it would be less redundant and field name changes wouldn't pose an issue if someone forgot to modify the return of `mountable_meta`.

A rough concept could look something like this:

Assume we have a base class (roughly based on the actual `InputTypeOptions` class in Graphene)
```python
class InputObjectTypeOptions:
   description: str
   deprecation_reason: str
   abcd: str
```
we would have an explicit definition of inheritable fields from that class. It is also extendable by subclassing it.

We could now adapt this for mounted types like `Argument`.
If a mounted Type specifies a class `MountedTypeOptions`, we can use that to extract all fields that are contained in the `MountedTypeOptions` from the `UnmountedTypeOptions` **iff** they exist on both types:

```python 
InputObjectType.meta_type = InputObjectTypeOptions
class ArgumentOptions:
   description: str
   deprecation_reason: str
   required: bool



Mount InputObjectType
# Extract deprecation reason from InputObjectTypeOptions (InputObjectType._meta)
# Don't extract required: field not defined on InputObjectTypeOptions
# Don't extract abcd: field not defined on ArgumentOptions
```
 If no `MountedTypeOptions` class exists for the mounted type,  we fall back to the legacy behavior.

This is just an initial idea, and by no means a finalized design. Also open to suggestions on why we should use `mountable_meta` instead. LMKWYT!
vhutov: @erikwrede cleaned up the PR to contain only the deprecation reason ✅ 
Let me think about the proposed idea meanwhile. 
vhutov: **UPD:** Changed PR description

Patch:
diff --git a/graphene/types/argument.py b/graphene/types/argument.py
index f9dc843..d9283c4 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 791ca6a..e7ededb 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 7fa046d..bceede6 100644
--- a/graphene/types/schema.py
+++ b/graphene/types/schema.py
@@ -316,6 +316,7 @@ class TypeMap(dict):
                     default_value=field.default_value,
                     out_name=name,
                     description=field.description,
+                    deprecation_reason=field.deprecation_reason,
                 )
             else:
                 args = {}
@@ -327,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(


Test patch:
diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py
index db4d6c2..c5521b6 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_field.py b/graphene/types/tests/test_field.py
index 669ada4..f0401bf 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 bfedfb0..9b10012 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)