Repository: graphql-python/graphene-django

Issue:
Schema management command broken in 3.0.0b1
In version `3.0.0b1` (#705), schema generation via the management command fails with the following exception if the schema contains translatable descriptions (taken from django form field `help_text`s).

```
Traceback (most recent call last):
  File "./manage.py", line 21, in <module>
    main()
  File "./manage.py", line 17, in main
    execute_from_command_line(sys.argv)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/django/core/management/__init__.py", line 395, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/django/core/management/base.py", line 328, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/django/core/management/base.py", line 369, in execute
    output = self.handle(*args, **options)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphene_django/management/commands/graphql_schema.py", line 113, in handle
    self.get_schema(schema, out, indent)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphene_django/management/commands/graphql_schema.py", line 70, in get_schema
    self.save_graphql_file(out, schema)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphene_django/management/commands/graphql_schema.py", line 59, in save_graphql_file
    outfile.write(print_schema(schema.graphql_schema))
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphql/utilities/print_schema.py", line 39, in print_schema
    return print_filtered_schema(
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphql/utilities/print_schema.py", line 61, in print_filtered_schema
    "\n\n".join(
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphql/utilities/print_schema.py", line 65, in <genexpr>
    (print_type(type_) for type_ in types),
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphql/utilities/print_schema.py", line 128, in print_type
    return print_object(type_)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphql/utilities/print_schema.py", line 162, in print_object
    + print_fields(type_)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphql/utilities/print_schema.py", line 198, in print_fields
    fields = [
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphql/utilities/print_schema.py", line 199, in <listcomp>
    print_description(field, "  ", not i)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphql/utilities/print_schema.py", line 282, in print_description
    block_string = print_block_string(description, "", prefer_multiple_lines)
  File "/home/username/.virtualenvs/project/lib/python3.8/site-packages/graphql/language/block_string.py", line 91, in print_block_string
    result += value.replace("\n", "\n" + indentation) if indentation else value
TypeError: can only concatenate str (not "__proxy__") to str
```

Schema generation fails when descriptions originally taken of django form fields are processed. Some of them are translatable and thus instances of `django.utils.functional.__proxy__` ([django docs](https://docs.djangoproject.com/en/dev/ref/unicode/#translated-strings)). However, the involved graphql core utility functions expect descriptions to be strings and thus fail to concatenate such descriptions with other strings.

An easy fix would be to cast the `value` argument of the `print_block_string` in `graphql/utilities/print_schema.py` to `str`. However, there might be a cleaner way that allows us to fix the issue from within the `graphene-django` package.


Discussion:

Resolving patch:
diff --git a/graphene_django/converter.py b/graphene_django/converter.py
--- a/graphene_django/converter.py
+++ b/graphene_django/converter.py
@@ -108,7 +108,9 @@ def convert_django_field_with_choices(
     if choices and convert_choices_to_enum:
         enum = convert_choice_field_to_enum(field)
         required = not (field.blank or field.null)
-        converted = enum(description=field.help_text, required=required)
+        converted = enum(
+            description=get_django_field_description(field), required=required
+        )
     else:
         converted = convert_django_field(field, registry)
     if registry is not None:
@@ -116,6 +118,10 @@ def convert_django_field_with_choices(
     return converted
 
 
+def get_django_field_description(field):
+    return None if field.help_text is None else str(field.help_text)
+
+
 @singledispatch
 def convert_django_field(field, registry=None):
     raise Exception(
@@ -132,17 +138,21 @@ def convert_django_field(field, registry=None):
 @convert_django_field.register(models.FileField)
 @convert_django_field.register(models.FilePathField)
 def convert_field_to_string(field, registry=None):
-    return String(description=field.help_text, required=not field.null)
+    return String(
+        description=get_django_field_description(field), required=not field.null
+    )
 
 
 @convert_django_field.register(models.AutoField)
 def convert_field_to_id(field, registry=None):
-    return ID(description=field.help_text, required=not field.null)
+    return ID(description=get_django_field_description(field), required=not field.null)
 
 
 @convert_django_field.register(models.UUIDField)
 def convert_field_to_uuid(field, registry=None):
-    return UUID(description=field.help_text, required=not field.null)
+    return UUID(
+        description=get_django_field_description(field), required=not field.null
+    )
 
 
 @convert_django_field.register(models.PositiveIntegerField)
@@ -151,35 +161,45 @@ def convert_field_to_uuid(field, registry=None):
 @convert_django_field.register(models.BigIntegerField)
 @convert_django_field.register(models.IntegerField)
 def convert_field_to_int(field, registry=None):
-    return Int(description=field.help_text, required=not field.null)
+    return Int(description=get_django_field_description(field), required=not field.null)
 
 
 @convert_django_field.register(models.NullBooleanField)
 @convert_django_field.register(models.BooleanField)
 def convert_field_to_boolean(field, registry=None):
-    return Boolean(description=field.help_text, required=not field.null)
+    return Boolean(
+        description=get_django_field_description(field), required=not field.null
+    )
 
 
 @convert_django_field.register(models.DecimalField)
 @convert_django_field.register(models.FloatField)
 @convert_django_field.register(models.DurationField)
 def convert_field_to_float(field, registry=None):
-    return Float(description=field.help_text, required=not field.null)
+    return Float(
+        description=get_django_field_description(field), required=not field.null
+    )
 
 
 @convert_django_field.register(models.DateTimeField)
 def convert_datetime_to_string(field, registry=None):
-    return DateTime(description=field.help_text, required=not field.null)
+    return DateTime(
+        description=get_django_field_description(field), required=not field.null
+    )
 
 
 @convert_django_field.register(models.DateField)
 def convert_date_to_string(field, registry=None):
-    return Date(description=field.help_text, required=not field.null)
+    return Date(
+        description=get_django_field_description(field), required=not field.null
+    )
 
 
 @convert_django_field.register(models.TimeField)
 def convert_time_to_string(field, registry=None):
-    return Time(description=field.help_text, required=not field.null)
+    return Time(
+        description=get_django_field_description(field), required=not field.null
+    )
 
 
 @convert_django_field.register(models.OneToOneRel)
@@ -250,7 +270,11 @@ def dynamic_type():
         if not _type:
             return
 
-        return Field(_type, description=field.help_text, required=not field.null)
+        return Field(
+            _type,
+            description=get_django_field_description(field),
+            required=not field.null,
+        )
 
     return Dynamic(dynamic_type)
 
@@ -260,13 +284,19 @@ def convert_postgres_array_to_list(field, registry=None):
     base_type = convert_django_field(field.base_field)
     if not isinstance(base_type, (List, NonNull)):
         base_type = type(base_type)
-    return List(base_type, description=field.help_text, required=not field.null)
+    return List(
+        base_type,
+        description=get_django_field_description(field),
+        required=not field.null,
+    )
 
 
 @convert_django_field.register(HStoreField)
 @convert_django_field.register(JSONField)
 def convert_postgres_field_to_string(field, registry=None):
-    return JSONString(description=field.help_text, required=not field.null)
+    return JSONString(
+        description=get_django_field_description(field), required=not field.null
+    )
 
 
 @convert_django_field.register(RangeField)
@@ -274,7 +304,11 @@ def convert_postgres_range_to_string(field, registry=None):
     inner_type = convert_django_field(field.base_field)
     if not isinstance(inner_type, (List, NonNull)):
         inner_type = type(inner_type)
-    return List(inner_type, description=field.help_text, required=not field.null)
+    return List(
+        inner_type,
+        description=get_django_field_description(field),
+        required=not field.null,
+    )
 
 
 # Register Django lazy()-wrapped values as GraphQL description/help_text.
diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py
--- a/graphene_django/filter/utils.py
+++ b/graphene_django/filter/utils.py
@@ -30,7 +30,9 @@ def get_filtering_args_from_filterset(filterset_class, type):
             form_field = filter_field.field
 
         field_type = convert_form_field(form_field).Argument()
-        field_type.description = filter_field.label
+        field_type.description = (
+            None if filter_field.label is None else str(filter_field.label)
+        )
         args[name] = field_type
 
     return args
diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py
--- a/graphene_django/forms/converter.py
+++ b/graphene_django/forms/converter.py
@@ -8,6 +8,10 @@
 from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
 
 
+def get_form_field_description(field):
+    return None if field.help_text is None else str(field.help_text)
+
+
 @singledispatch
 def convert_form_field(field):
     raise ImproperlyConfigured(
@@ -25,39 +29,45 @@ def convert_form_field(field):
 @convert_form_field.register(forms.RegexField)
 @convert_form_field.register(forms.Field)
 def convert_form_field_to_string(field):
-    return String(description=field.help_text, required=field.required)
+    return String(
+        description=get_form_field_description(field), required=field.required
+    )
 
 
 @convert_form_field.register(forms.UUIDField)
 def convert_form_field_to_uuid(field):
-    return UUID(description=field.help_text, required=field.required)
+    return UUID(description=get_form_field_description(field), required=field.required)
 
 
 @convert_form_field.register(forms.IntegerField)
 @convert_form_field.register(forms.NumberInput)
 def convert_form_field_to_int(field):
-    return Int(description=field.help_text, required=field.required)
+    return Int(description=get_form_field_description(field), required=field.required)
 
 
 @convert_form_field.register(forms.BooleanField)
 def convert_form_field_to_boolean(field):
-    return Boolean(description=field.help_text, required=field.required)
+    return Boolean(
+        description=get_form_field_description(field), required=field.required
+    )
 
 
 @convert_form_field.register(forms.NullBooleanField)
 def convert_form_field_to_nullboolean(field):
-    return Boolean(description=field.help_text)
+    return Boolean(description=get_form_field_description(field))
 
 
 @convert_form_field.register(forms.DecimalField)
 @convert_form_field.register(forms.FloatField)
 def convert_form_field_to_float(field):
-    return Float(description=field.help_text, required=field.required)
+    return Float(description=get_form_field_description(field), required=field.required)
 
 
 @convert_form_field.register(forms.MultipleChoiceField)
 def convert_form_field_to_string_list(field):
-    return List(String, description=field.help_text, required=field.required)
+    return List(
+        String, description=get_form_field_description(field), required=field.required
+    )
 
 
 @convert_form_field.register(forms.ModelMultipleChoiceField)
@@ -68,17 +78,19 @@ def convert_form_field_to_id_list(field):
 
 @convert_form_field.register(forms.DateField)
 def convert_form_field_to_date(field):
-    return Date(description=field.help_text, required=field.required)
+    return Date(description=get_form_field_description(field), required=field.required)
 
 
 @convert_form_field.register(forms.DateTimeField)
 def convert_form_field_to_datetime(field):
-    return DateTime(description=field.help_text, required=field.required)
+    return DateTime(
+        description=get_form_field_description(field), required=field.required
+    )
 
 
 @convert_form_field.register(forms.TimeField)
 def convert_form_field_to_time(field):
-    return Time(description=field.help_text, required=field.required)
+    return Time(description=get_form_field_description(field), required=field.required)
 
 
 @convert_form_field.register(forms.ModelChoiceField)


Test patch: