Repository: strawberry-graphql/strawberry

Issue:
Add support for description in fields
```python
@strawberry.type
class Query:
    @strawberry.field
    async def todos(self, info) -> typing.List[str]:
        """A list of TODOs"""
        return ["a", "b"]
```

```python
@strawberry.type
class Query:
    @strawberry.field(description="A list of TODOs")
    async def todos(self, info) -> typing.List[str]:
        return ["a", "b"]
```

We also need a way to define descriptions on simple fields

```python
@strawberry.type
class TODO:
    id: strawberry.ID
    """This would be unique"""
```

```python
@strawberry.type
class TODO:
    id: str = strawberry.field(description="This would be unique")
```

See:
- https://www.python.org/dev/peps/pep-0257/
- https://stackoverflow.com/a/20529741/169274
- http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-autoattribute




Discussion:

Resolving patch:
diff --git a/strawberry/enum.py b/strawberry/enum.py
--- a/strawberry/enum.py
+++ b/strawberry/enum.py
@@ -7,7 +7,7 @@
 from .type_converter import REGISTRY
 
 
-def _process_enum(cls, name="", description=""):
+def _process_enum(cls, name=None, description=None):
     if not isinstance(cls, EnumMeta):
         raise NotAnEnum()
 
@@ -21,6 +21,8 @@ def repr_(self):
 
     setattr(cls, "__repr__", repr_)
 
+    description = description or cls.__doc__
+
     cls.field = GraphQLEnumType(
         name=name,
         values=[(item.name, GraphQLEnumValue(item.value)) for item in cls],
@@ -30,7 +32,7 @@ def repr_(self):
     return cls
 
 
-def enum(_cls=None, *, name="", description=""):
+def enum(_cls=None, *, name=None, description=None):
     """Registers the enum in the GraphQL type system.
 
     If name is passed, the name of the GraphQL type will be
diff --git a/strawberry/field.py b/strawberry/field.py
--- a/strawberry/field.py
+++ b/strawberry/field.py
@@ -17,6 +17,46 @@
 )
 
 
+class strawberry_field:
+    """A small wrapper for a field in strawberry.
+
+    You shouldn't be using this directly as this is used internally
+    when using `strawberry.field`.
+
+    This allows to use the following two syntaxes when using the type
+    decorator:
+
+    >>> class X:
+    >>>     field_abc: str = strawberry.field(description="ABC")
+
+    >>> class X:
+    >>>     @strawberry.field(description="ABC")
+    >>>     def field_a(self, info) -> str:
+    >>>         return "abc"
+
+    When calling this class as strawberry_field it creates a field
+    that stores metadata (such as field description). In addition
+    to that it also acts as decorator when called as a function,
+    allowing us to us both syntaxes.
+    """
+
+    def __init__(self, *, is_subscription=False, **kwargs):
+        self.field = dataclasses.field()
+        self.is_subscription = is_subscription
+        self.description = kwargs.get("description", None)
+        self.kwargs = kwargs
+
+    def __call__(self, wrap):
+        setattr(wrap, IS_STRAWBERRY_FIELD, True)
+
+        self.kwargs["description"] = self.description or wrap.__doc__
+
+        wrap.field = _get_field(
+            wrap, is_subscription=self.is_subscription, **self.kwargs
+        )
+        return wrap
+
+
 def convert_args(args, annotations):
     """Converts a nested dictionary to a dictionary of strawberry input types."""
 
@@ -50,14 +90,7 @@ def convert_args(args, annotations):
     return converted_args
 
 
-def field(wrap=None, *, is_subscription=False):
-    if not wrap:
-        # when used as a function we return a dataclass field
-        # in the future we can use the `metadata` argument of `field` to pass
-        # extra arguments like the description
-        return dataclasses.field()
-
-    setattr(wrap, IS_STRAWBERRY_FIELD, True)
+def _get_field(wrap, *, is_subscription=False, **kwargs):
     annotations = get_type_hints(wrap)
 
     name = wrap.__name__
@@ -96,9 +129,40 @@ def resolver(source, info, **args):
         def _resolve(event, info):
             return event
 
-        kwargs = {"subscribe": resolver, "resolve": _resolve}
+        kwargs.update({"subscribe": resolver, "resolve": _resolve})
     else:
-        kwargs = {"resolve": resolver}
+        kwargs.update({"resolve": resolver})
+
+    kwargs["description"] = kwargs.get("description", wrap.__doc__)
+
+    return GraphQLField(field_type, args=arguments, **kwargs)
+
+
+def field(wrap=None, *, is_subscription=False, description=None):
+    """Annotates a method or property as a GraphQL field.
+
+    This is normally used inside a type declaration:
+
+    >>> @strawberry.type:
+    >>> class X:
+    >>>     field_abc: str = strawberry.field(description="ABC")
+
+    >>>     @strawberry.field(description="ABC")
+    >>>     def field_with_resolver(self, info) -> str:
+    >>>         return "abc"
+
+    it can be used both as decorator and as a normal function.
+    """
+
+    field = strawberry_field(description=description, is_subscription=is_subscription)
+
+    # when calling this with parens we are going to return a strawberry_field
+    # instance, so it can be used as both decorator and function.
+
+    if wrap is None:
+        return field
+
+    # otherwise we run the decorator directly,
+    # when called as @strawberry.field, without parens.
 
-    wrap.field = GraphQLField(field_type, args=arguments, **kwargs)
-    return wrap
+    return field(wrap)
diff --git a/strawberry/type.py b/strawberry/type.py
--- a/strawberry/type.py
+++ b/strawberry/type.py
@@ -1,7 +1,6 @@
 import typing
 from functools import partial
 
-import dataclasses
 from dataclasses import dataclass
 from graphql import (
     GraphQLField,
@@ -31,50 +30,77 @@ def _resolver(obj, info):
     return _resolver
 
 
-def type(cls, *, is_input=False):
-    def wrap():
-        name = cls.__name__
-        REGISTRY[name] = cls
+def _convert_annotations_fields(cls, *, is_input=False):
+    FieldClass = GraphQLInputField if is_input else GraphQLField
+    annotations = typing.get_type_hints(cls, None, REGISTRY)
 
-        def repr_(self):
-            return print_type(self.field)
+    fields = {}
 
-        setattr(cls, "__repr__", repr_)
+    for key, annotation in annotations.items():
+        field_name = to_camel_case(key)
+        class_field = getattr(cls, key, None)
 
-        annotations = typing.get_type_hints(cls, None, REGISTRY)
+        description = getattr(class_field, "description", None)
 
-        def _get_fields():
-            FieldClass = GraphQLInputField if is_input else GraphQLField
+        fields[field_name] = FieldClass(
+            get_graphql_type_for_annotation(annotation, key),
+            description=description,
+            **({} if is_input else {"resolve": _get_resolver(cls, key)})
+        )
 
-            fields = {
-                to_camel_case(field.name): FieldClass(
-                    get_graphql_type_for_annotation(
-                        annotations[field.name], field.name
-                    ),
-                    **({} if is_input else {"resolve": _get_resolver(cls, field.name)})
-                )
-                for field in dataclasses.fields(cls)
+    return fields
+
+
+def _process_type(cls, *, is_input=False, description=None):
+    name = cls.__name__
+    REGISTRY[name] = cls
+
+    def repr_(self):
+        return print_type(self.field)
+
+    setattr(cls, "__repr__", repr_)
+
+    def _get_fields():
+        fields = _convert_annotations_fields(cls, is_input=is_input)
+
+        fields.update(
+            {
+                to_camel_case(key): value.field
+                for key, value in cls.__dict__.items()
+                if getattr(value, IS_STRAWBERRY_FIELD, False)
             }
+        )
+
+        return fields
+
+    if is_input:
+        setattr(cls, IS_STRAWBERRY_INPUT, True)
+
+    extra_kwargs = {"description": description or cls.__doc__}
+
+    TypeClass = GraphQLInputObjectType if is_input else GraphQLObjectType
+    cls.field = TypeClass(name, lambda: _get_fields(), **extra_kwargs)
+
+    return dataclass(cls, repr=False)
+
+
+def type(cls=None, *, is_input=False, description=None):
+    """Annotates a class as a GraphQL type.
 
-            fields.update(
-                {
-                    to_camel_case(key): value.field
-                    for key, value in cls.__dict__.items()
-                    if getattr(value, IS_STRAWBERRY_FIELD, False)
-                }
-            )
+    Example usage:
 
-            return fields
+    >>> @strawberry.type:
+    >>> class X:
+    >>>     field_abc: str = "ABC"
+    """
 
-        if is_input:
-            cls.field = GraphQLInputObjectType(name, lambda: _get_fields())
-            setattr(cls, IS_STRAWBERRY_INPUT, True)
-        else:
-            cls.field = GraphQLObjectType(name, lambda: _get_fields())
+    def wrap(cls):
+        return _process_type(cls, is_input=is_input, description=description)
 
-        return dataclass(cls, repr=False)
+    if cls is None:
+        return wrap
 
-    return wrap()
+    return wrap(cls)
 
 
 input = partial(type, is_input=True)


Test patch:
diff --git a/tests/test_enum.py b/tests/test_enum.py
--- a/tests/test_enum.py
+++ b/tests/test_enum.py
@@ -107,6 +107,20 @@ class Test(Enum):
     assert Test.field.name == "NewName"
     assert Test.field.description == "This is the description"
 
+    @strawberry.enum(name="Example")
+    class Test2(Enum):
+        """Example"""
+
+        A = 1
+        B = 2
+        C = 3
+
+    assert Test.field
+
+    assert type(Test2.field) == GraphQLEnumType
+    assert Test2.field.name == "Example"
+    assert Test2.field.description == "Example"
+
 
 def test_raises_error_when_using_enum_with_a_not_enum_class():
     with pytest.raises(NotAnEnum) as e:
diff --git a/tests/test_field.py b/tests/test_field.py
--- a/tests/test_field.py
+++ b/tests/test_field.py
@@ -1,6 +1,5 @@
 import pytest
 
-import dataclasses
 import strawberry
 from graphql import GraphQLField, GraphQLNonNull
 from strawberry.exceptions import (
@@ -12,8 +11,7 @@
 def test_field_as_function():
     field: str = strawberry.field()
 
-    assert type(field) == dataclasses.Field
-    assert field.metadata == {}
+    assert field.description is None
 
 
 def test_field_arguments():
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1,3 +1,5 @@
+from enum import Enum
+
 import strawberry
 from graphql import graphql_sync
 
@@ -128,3 +130,150 @@ def example(self, info, query_param: str) -> str:
     assert not result.errors
     assert result.data["helloWorld"] == "strawberry"
     assert result.data["example"] == "hi"
+
+
+def test_type_description():
+    @strawberry.type(description="Decorator argument description")
+    class TypeA:
+        a: str
+
+    @strawberry.type
+    class TypeB:
+        """Docstring description"""
+
+        a: str
+
+    @strawberry.type(description="Decorator description overrides docstring")
+    class TypeC:
+        """Docstring description"""
+
+        a: str
+
+    @strawberry.type
+    class Query:
+        a: TypeA
+        b: TypeB
+        c: TypeC
+
+    schema = strawberry.Schema(query=Query)
+
+    query = """{
+        __schema {
+            types {
+                name
+                description
+            }
+        }
+    }"""
+
+    result = graphql_sync(schema, query)
+
+    assert not result.errors
+
+    assert {
+        "name": "TypeA",
+        "description": "Decorator argument description",
+    } in result.data["__schema"]["types"]
+
+    assert {"name": "TypeB", "description": "Docstring description"} in result.data[
+        "__schema"
+    ]["types"]
+
+    assert {
+        "name": "TypeC",
+        "description": "Decorator description overrides docstring",
+    } in result.data["__schema"]["types"]
+
+
+def test_field_description():
+    @strawberry.type
+    class Query:
+        a: str = strawberry.field(description="Example")
+
+        @strawberry.field
+        def b(self, info, id: int) -> str:
+            return "I'm a resolver"
+
+        @strawberry.field(description="Example C")
+        def c(self, info, id: int) -> str:
+            return "I'm a resolver"
+
+        @strawberry.field
+        def d(self, info, id: int) -> str:
+            """Example D"""
+            return "I'm a resolver"
+
+        @strawberry.field(description="Inline description")
+        def e(self, info, id: int) -> str:
+            """Doc string description"""
+            return "I'm a resolver"
+
+    schema = strawberry.Schema(query=Query)
+
+    query = """{
+        __type(name: "Query") {
+            fields {
+                name
+                description
+            }
+        }
+    }"""
+
+    result = graphql_sync(schema, query)
+
+    assert not result.errors
+
+    assert result.data["__type"]["fields"] == [
+        {"name": "a", "description": "Example"},
+        {"name": "b", "description": None},
+        {"name": "c", "description": "Example C"},
+        {"name": "d", "description": "Example D"},
+        {"name": "e", "description": "Inline description"},
+    ]
+
+
+def test_enum_description():
+    @strawberry.enum(description="We love ice-creams")
+    class IceCreamFlavour(Enum):
+        VANILLA = "vanilla"
+        STRAWBERRY = "strawberry"
+        CHOCOLATE = "chocolate"
+
+    @strawberry.enum
+    class PizzaType(Enum):
+        """We also love pizza"""
+
+        MARGHERITA = "margherita"
+
+    @strawberry.type
+    class Query:
+        favorite_ice_cream: IceCreamFlavour = IceCreamFlavour.STRAWBERRY
+        pizza: PizzaType = PizzaType.MARGHERITA
+
+    schema = strawberry.Schema(query=Query)
+
+    query = """{
+        iceCreamFlavour: __type(name: "IceCreamFlavour") {
+            description
+            enumValues {
+                name
+                description
+            }
+        }
+        pizzas: __type(name: "PizzaType") {
+            description
+        }
+    }"""
+
+    result = graphql_sync(schema, query)
+
+    assert not result.errors
+
+    assert result.data["iceCreamFlavour"]["description"] == "We love ice-creams"
+    assert result.data["iceCreamFlavour"]["enumValues"] == [
+        {"name": "VANILLA", "description": None},
+        {"name": "STRAWBERRY", "description": None},
+        {"name": "CHOCOLATE", "description": None},
+    ]
+
+    assert result.data["pizzas"]["description"] == "We also love pizza"