Merge branch 'debug' of github.com:joerhodes3/graphene-django into debug

This commit is contained in:
Joe Rhodes 2020-04-21 09:49:58 -04:00
commit c272fd7cd6
14 changed files with 278 additions and 33 deletions

4
.github/stale.yml vendored
View File

@ -1,7 +1,7 @@
# Number of days of inactivity before an issue becomes stale # Number of days of inactivity before an issue becomes stale
daysUntilStale: 90 daysUntilStale: 120
# Number of days of inactivity before a stale issue is closed # Number of days of inactivity before a stale issue is closed
daysUntilClose: 14 daysUntilClose: 30
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- pinned - pinned

View File

@ -29,6 +29,20 @@ you're ready to use Relay with Graphene GraphQL implementation.
The schema file is sorted to create a reproducible canonical representation. The schema file is sorted to create a reproducible canonical representation.
GraphQL SDL Representation
--------------------------
The schema can also be exported as a GraphQL SDL file by changing the file
extension :
.. code:: bash
./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.graphql
When exporting the schema as a ``.graphql`` file the ``--indent`` option is
ignored.
Advanced Usage Advanced Usage
-------------- --------------

View File

@ -140,3 +140,33 @@ Default: ``False``
# 'messages': ['This field is required.'], # 'messages': ['This field is required.'],
# } # }
# ] # ]
``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING``
--------------------------------------
Set to ``True`` to use the new naming format for the auto generated Enum types from Django choice fields. The new format looks like this: ``{app_label}{object_name}{field_name}Choices``
Default: ``False``
``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME``
--------------------------------------
Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type.
If set to a function then the ``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING`` setting is ignored.
Default: ``None``
.. code:: python
# myapp.utils
def enum_naming(field):
if isinstance(field.model, User):
return f"CustomUserEnum{field.name.title()}"
return f"CustomEnum{field.name.title()}"
GRAPHENE = {
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
}

View File

@ -1,6 +1,6 @@
from .types import DjangoObjectType from .types import DjangoObjectType
from .fields import DjangoConnectionField from .fields import DjangoConnectionField
__version__ = "2.8.2" __version__ = "2.9.0"
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"] __all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]

View File

@ -1,6 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from django.db import models from django.db import models
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.module_loading import import_string
from graphene import ( from graphene import (
ID, ID,
@ -22,6 +23,7 @@ from graphene.types.json import JSONString
from graphene.utils.str_converters import to_camel_case, to_const from graphene.utils.str_converters import to_camel_case, to_const
from graphql import assert_valid_name from graphql import assert_valid_name
from .settings import graphene_settings
from .compat import ArrayField, HStoreField, JSONField, RangeField from .compat import ArrayField, HStoreField, JSONField, RangeField
from .fields import DjangoListField, DjangoConnectionField from .fields import DjangoListField, DjangoConnectionField
from .utils import import_single_dispatch from .utils import import_single_dispatch
@ -68,6 +70,31 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
return Enum(name, list(named_choices), type=EnumWithDescriptionsType) return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
def generate_enum_name(django_model_meta, field):
if graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME:
# Try and import custom function
custom_func = import_string(
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME
)
name = custom_func(field)
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING is True:
name = "{app_label}{object_name}{field_name}Choices".format(
app_label=to_camel_case(django_model_meta.app_label.title()),
object_name=django_model_meta.object_name,
field_name=to_camel_case(field.name.title()),
)
else:
name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name))
return name
def convert_choice_field_to_enum(field, name=None):
if name is None:
name = generate_enum_name(field.model._meta, field)
choices = field.choices
return convert_choices_to_named_enum_with_descriptions(name, choices)
def convert_django_field_with_choices( def convert_django_field_with_choices(
field, registry=None, convert_choices_to_enum=True field, registry=None, convert_choices_to_enum=True
): ):
@ -77,9 +104,7 @@ def convert_django_field_with_choices(
return converted return converted
choices = getattr(field, "choices", None) choices = getattr(field, "choices", None)
if choices and convert_choices_to_enum: if choices and convert_choices_to_enum:
meta = field.model._meta enum = convert_choice_field_to_enum(field)
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
enum = convert_choices_to_named_enum_with_descriptions(name, choices)
required = not (field.blank or field.null) required = not (field.blank or field.null)
converted = enum(description=field.help_text, required=required) converted = enum(description=field.help_text, required=required)
else: else:

View File

@ -1,5 +1,6 @@
from functools import partial from functools import partial
import six
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from graphql_relay.connection.arrayconnection import connection_from_list_slice from graphql_relay.connection.arrayconnection import connection_from_list_slice
from promise import Promise from promise import Promise
@ -19,19 +20,23 @@ class DjangoListField(Field):
if isinstance(_type, NonNull): if isinstance(_type, NonNull):
_type = _type.of_type _type = _type.of_type
assert issubclass(
_type, DjangoObjectType
), "DjangoListField only accepts DjangoObjectType types"
# Django would never return a Set of None vvvvvvv # Django would never return a Set of None vvvvvvv
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs) super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
assert issubclass(
self._underlying_type, DjangoObjectType
), "DjangoListField only accepts DjangoObjectType types"
@property
def _underlying_type(self):
_type = self._type
while hasattr(_type, "of_type"):
_type = _type.of_type
return _type
@property @property
def model(self): def model(self):
_type = self.type.of_type return self._underlying_type._meta.model
if isinstance(_type, NonNull):
_type = _type.of_type
return _type._meta.model
@staticmethod @staticmethod
def list_resolver(django_object_type, resolver, root, info, **args): def list_resolver(django_object_type, resolver, root, info, **args):

View File

@ -1,3 +1,4 @@
import os
import importlib import importlib
import json import json
import functools import functools
@ -5,6 +6,7 @@ import functools
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.utils import autoreload from django.utils import autoreload
from graphql import print_schema
from graphene_django.settings import graphene_settings from graphene_django.settings import graphene_settings
@ -44,24 +46,40 @@ class CommandArguments(BaseCommand):
class Command(CommandArguments): class Command(CommandArguments):
help = "Dump Graphene schema JSON to file" help = "Dump Graphene schema as a JSON or GraphQL file"
can_import_settings = True can_import_settings = True
def save_file(self, out, schema_dict, indent): def save_json_file(self, out, schema_dict, indent):
with open(out, "w") as outfile: with open(out, "w") as outfile:
json.dump(schema_dict, outfile, indent=indent, sort_keys=True) json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
def save_graphql_file(self, out, schema):
with open(out, "w") as outfile:
outfile.write(print_schema(schema))
def get_schema(self, schema, out, indent): def get_schema(self, schema, out, indent):
schema_dict = {"data": schema.introspect()} schema_dict = {"data": schema.introspect()}
if out == "-": if out == "-":
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True)) self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
else: else:
self.save_file(out, schema_dict, indent) # Determine format
_, file_extension = os.path.splitext(out)
if file_extension == ".graphql":
self.save_graphql_file(out, schema)
elif file_extension == ".json":
self.save_json_file(out, schema_dict, indent)
else:
raise CommandError(
'Unrecognised file format "{}"'.format(file_extension)
)
style = getattr(self, "style", None) style = getattr(self, "style", None)
success = getattr(style, "SUCCESS", lambda x: x) success = getattr(style, "SUCCESS", lambda x: x)
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out)) self.stdout.write(
success("Successfully dumped GraphQL schema to {}".format(out))
)
def handle(self, *args, **options): def handle(self, *args, **options):
options_schema = options.get("schema") options_schema = options.get("schema")

View File

@ -36,7 +36,10 @@ DEFAULTS = {
# Max items returned in ConnectionFields / FilterConnectionFields # Max items returned in ConnectionFields / FilterConnectionFields
"RELAY_CONNECTION_MAX_LIMIT": 100, "RELAY_CONNECTION_MAX_LIMIT": 100,
"CAMELCASE_ERRORS": False, "CAMELCASE_ERRORS": False,
"SOURCE": None "SOURCE": None,
# Set to True to enable v3 naming convention for choice field Enum's
"DJANGO_CHOICE_FIELD_ENUM_V3_NAMING": False,
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
} }
if settings.DEBUG: if settings.DEBUG:

View File

@ -14,11 +14,11 @@
} }
</style> </style>
<link rel="stylesheet" href={% static "graphiql/css/graphiql.css" %}> <link rel="stylesheet" href="{% static 'graphiql/css/graphiql.css' %}">
<script src={%static "graphiql/js/fetch.min.js" %}></script> <script src="{% static 'graphiql/js/fetch.min.js' %}"></script>
<script src={%static "graphiql/js/react.production.min.js" %}></script> <script src="{% static 'graphiql/js/react.production.min.js' %}"></script>
<script src={%static "graphiql/js/react-dom.production.min.js" %}></script> <script src="{% static 'graphiql/js/react-dom.production.min.js' %}"></script>
<script src={%static "graphiql/js/graphiql.min.js" %}></script> <script src="{% static 'graphiql/js/graphiql.min.js' %}"></script>
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<meta name="Authorization" content="{% block additional_headers %}{% endblock %}"> <meta name="Authorization" content="{% block additional_headers %}{% endblock %}">

View File

@ -1,17 +1,21 @@
from textwrap import dedent
from django.core import management from django.core import management
from mock import patch, mock_open from mock import mock_open, patch
from six import StringIO from six import StringIO
from graphene import ObjectType, Schema, String
@patch("graphene_django.management.commands.graphql_schema.Command.save_file")
def test_generate_file_on_call_graphql_schema(savefile_mock, settings): @patch("graphene_django.management.commands.graphql_schema.Command.save_json_file")
def test_generate_json_file_on_call_graphql_schema(savefile_mock, settings):
out = StringIO() out = StringIO()
management.call_command("graphql_schema", schema="", stdout=out) management.call_command("graphql_schema", schema="", stdout=out)
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue() assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
@patch("json.dump") @patch("json.dump")
def test_files_are_canonical(dump_mock): def test_json_files_are_canonical(dump_mock):
open_mock = mock_open() open_mock = mock_open()
with patch("graphene_django.management.commands.graphql_schema.open", open_mock): with patch("graphene_django.management.commands.graphql_schema.open", open_mock):
management.call_command("graphql_schema", schema="") management.call_command("graphql_schema", schema="")
@ -25,3 +29,34 @@ def test_files_are_canonical(dump_mock):
assert ( assert (
dump_mock.call_args[1]["indent"] > 0 dump_mock.call_args[1]["indent"] > 0
), "output should be pretty-printed by default" ), "output should be pretty-printed by default"
def test_generate_graphql_file_on_call_graphql_schema():
class Query(ObjectType):
hi = String()
mock_schema = Schema(query=Query)
open_mock = mock_open()
with patch("graphene_django.management.commands.graphql_schema.open", open_mock):
management.call_command(
"graphql_schema", schema=mock_schema, out="schema.graphql"
)
open_mock.assert_called_once()
handle = open_mock()
assert handle.write.called_once()
schema_output = handle.write.call_args[0][0]
assert schema_output == dedent(
"""\
schema {
query: Query
}
type Query {
hi: String
}
"""
)

View File

@ -1,4 +1,5 @@
import pytest import pytest
from collections import namedtuple
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from graphene import NonNull from graphene import NonNull
@ -10,9 +11,14 @@ from graphene.types.datetime import DateTime, Date, Time
from graphene.types.json import JSONString from graphene.types.json import JSONString
from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType
from ..converter import convert_django_field, convert_django_field_with_choices from ..converter import (
convert_django_field,
convert_django_field_with_choices,
generate_enum_name,
)
from ..registry import Registry from ..registry import Registry
from ..types import DjangoObjectType from ..types import DjangoObjectType
from ..settings import graphene_settings
from .models import Article, Film, FilmDetails, Reporter from .models import Article, Film, FilmDetails, Reporter
@ -325,3 +331,25 @@ def test_should_postgres_range_convert_list():
assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type, graphene.NonNull)
assert isinstance(field.type.of_type, graphene.List) assert isinstance(field.type.of_type, graphene.List)
assert field.type.of_type.of_type == graphene.Int assert field.type.of_type.of_type == graphene.Int
def test_generate_enum_name():
MockDjangoModelMeta = namedtuple("DjangoMeta", ["app_label", "object_name"])
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
# Simple case
field = graphene.Field(graphene.String, name="type")
model_meta = MockDjangoModelMeta(app_label="users", object_name="User")
assert generate_enum_name(model_meta, field) == "UsersUserTypeChoices"
# More complicated multiple work case
field = graphene.Field(graphene.String, name="fizz_buzz")
model_meta = MockDjangoModelMeta(
app_label="some_long_app_name", object_name="SomeObject"
)
assert (
generate_enum_name(model_meta, field)
== "SomeLongAppNameSomeObjectFizzBuzzChoices"
)
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False

View File

@ -19,6 +19,12 @@ class TestDjangoListField:
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
list_field = DjangoListField(TestType) list_field = DjangoListField(TestType)
def test_only_import_paths(self):
list_field = DjangoListField("graphene_django.tests.schema.Human")
from .schema import Human
assert list_field._type.of_type.of_type is Human
def test_non_null_type(self): def test_non_null_type(self):
class Reporter(DjangoObjectType): class Reporter(DjangoObjectType):
class Meta: class Meta:

View File

@ -9,7 +9,9 @@ from graphene import Connection, Field, Interface, ObjectType, Schema, String
from graphene.relay import Node from graphene.relay import Node
from .. import registry from .. import registry
from ..settings import graphene_settings
from ..types import DjangoObjectType, DjangoObjectTypeOptions from ..types import DjangoObjectType, DjangoObjectTypeOptions
from ..converter import convert_choice_field_to_enum
from .models import Article as ArticleModel from .models import Article as ArticleModel
from .models import Reporter as ReporterModel from .models import Reporter as ReporterModel
@ -386,6 +388,10 @@ def test_django_objecttype_exclude_fields_exist_on_model():
assert len(record) == 0 assert len(record) == 0
def custom_enum_name(field):
return "CustomEnum{}".format(field.name.title())
class TestDjangoObjectType: class TestDjangoObjectType:
@pytest.fixture @pytest.fixture
def PetModel(self): def PetModel(self):
@ -492,3 +498,78 @@ class TestDjangoObjectType:
} }
""" """
) )
def test_django_objecttype_convert_choices_enum_naming_collisions(self, PetModel):
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
class PetModelKind(DjangoObjectType):
class Meta:
model = PetModel
fields = ["id", "kind"]
class Query(ObjectType):
pet = Field(PetModelKind)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type PetModelKind {
id: ID!
kind: TestsPetModelKindChoices!
}
type Query {
pet: PetModelKind
}
enum TestsPetModelKindChoices {
CAT
DOG
}
"""
)
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False
def test_django_objecttype_choices_custom_enum_name(self, PetModel):
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = (
"graphene_django.tests.test_types.custom_enum_name"
)
class PetModelKind(DjangoObjectType):
class Meta:
model = PetModel
fields = ["id", "kind"]
class Query(ObjectType):
pet = Field(PetModelKind)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
enum CustomEnumKind {
CAT
DOG
}
type PetModelKind {
id: ID!
kind: CustomEnumKind!
}
type Query {
pet: PetModelKind
}
"""
)
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = None

View File

@ -26,10 +26,10 @@ tests_require = [
dev_requires = [ dev_requires = [
"black==19.3b0", "black==19.10b0",
"flake8==3.7.7", "flake8==3.7.9",
"flake8-black==0.1.0", "flake8-black==0.1.1",
"flake8-bugbear==19.3.0", "flake8-bugbear==20.1.4",
] + tests_require ] + tests_require
setup( setup(