From f9303dab720336af5826b41497f8e71377aaac9a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 21 Jun 2016 23:04:12 -0700 Subject: [PATCH] Improved Django integration --- .../cookbook/cookbook/ingredients/schema.py | 6 +- .../examples/cookbook/cookbook/schema.py | 5 +- .../examples/cookbook/cookbook/urls.py | 2 +- graphene-django/graphene_django/converter.py | 16 ++--- .../graphene_django/debug/sql/types.py | 2 +- .../graphene_django/debug/tests/test_query.py | 14 ++-- .../graphene_django/debug/types.py | 5 +- graphene-django/graphene_django/fields.py | 9 ++- .../graphene_django/filter/__init__.py | 2 +- .../graphene_django/filter/fields.py | 3 +- .../graphene_django/filter/filterset.py | 3 +- .../graphene_django/filter/tests/filters.py | 2 +- .../filter/tests/test_fields.py | 64 +++++++++++-------- .../graphene_django/filter/utils.py | 8 +-- .../graphene_django/tests/test_converter.py | 2 +- graphene-django/graphene_django/types.py | 20 ++++-- graphene/relay/connection.py | 14 +++- graphene/types/argument.py | 3 +- 18 files changed, 108 insertions(+), 72 deletions(-) diff --git a/graphene-django/examples/cookbook/cookbook/ingredients/schema.py b/graphene-django/examples/cookbook/cookbook/ingredients/schema.py index 28d8555a..7af52624 100644 --- a/graphene-django/examples/cookbook/cookbook/ingredients/schema.py +++ b/graphene-django/examples/cookbook/cookbook/ingredients/schema.py @@ -1,5 +1,5 @@ from cookbook.ingredients.models import Category, Ingredient -from graphene import ObjectType, relay +from graphene import ObjectType, Field from graphene_django.filter import DjangoFilterConnectionField from graphene_django.types import DjangoNode, DjangoObjectType @@ -29,8 +29,8 @@ class IngredientNode(DjangoNode, DjangoObjectType): class Query(ObjectType): - category = relay.NodeField(CategoryNode) + category = Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) - ingredient = relay.NodeField(IngredientNode) + ingredient = Field(IngredientNode) all_ingredients = DjangoFilterConnectionField(IngredientNode) diff --git a/graphene-django/examples/cookbook/cookbook/schema.py b/graphene-django/examples/cookbook/cookbook/schema.py index ba6f02cf..ff2b2fe5 100644 --- a/graphene-django/examples/cookbook/cookbook/schema.py +++ b/graphene-django/examples/cookbook/cookbook/schema.py @@ -1,8 +1,9 @@ -import cookbook.ingredients.schema import graphene +import cookbook.ingredients.schema +# print cookbook.ingredients.schema.Query._meta.graphql_type.get_fields()['allIngredients'].args class Query(cookbook.ingredients.schema.Query): pass -schema = graphene.Schema(name='Cookbook Schema', query=Query) +schema = graphene.Schema(query=Query) diff --git a/graphene-django/examples/cookbook/cookbook/urls.py b/graphene-django/examples/cookbook/cookbook/urls.py index e8bc0aa5..8e096e35 100644 --- a/graphene-django/examples/cookbook/cookbook/urls.py +++ b/graphene-django/examples/cookbook/cookbook/urls.py @@ -3,7 +3,7 @@ from django.contrib import admin from django.views.decorators.csrf import csrf_exempt from cookbook.schema import schema -from graphene.contrib.django.views import GraphQLView +from graphene_django.views import GraphQLView urlpatterns = [ url(r'^admin/', admin.site.urls), diff --git a/graphene-django/graphene_django/converter.py b/graphene-django/graphene_django/converter.py index f5f64d39..2078639b 100644 --- a/graphene-django/graphene_django/converter.py +++ b/graphene-django/graphene_django/converter.py @@ -4,13 +4,14 @@ from django.utils.encoding import force_text from graphene import Enum, List, ID, Boolean, Float, Int, String, Field, NonNull from graphene.types.json import JSONString from graphene.types.datetime import DateTime +from graphene.types.json import JSONString from graphene.utils.str_converters import to_const -from graphene.relay import Node, ConnectionField -# from ...core.types.custom_scalars import DateTime, JSONString +from graphene.relay import Node + from .compat import (ArrayField, HStoreField, JSONField, RangeField, RelatedObject, UUIDField) from .utils import get_related_model, import_single_dispatch -from .fields import DjangoConnectionField +from .fields import get_connection_field singledispatch = import_single_dispatch() @@ -30,8 +31,7 @@ def convert_django_field_with_choices(field, registry=None): meta = field.model._meta name = '{}{}'.format(meta.object_name, field.name.capitalize()) graphql_choices = list(convert_choices(choices)) - from collections import OrderedDict - enum = Enum(name, OrderedDict(graphql_choices)) + enum = Enum(name, list(graphql_choices)) return enum(description=field.help_text) return convert_django_field(field, registry) @@ -106,7 +106,7 @@ def convert_field_to_list_or_connection(field, registry=None): return if issubclass(_type, Node): - return DjangoConnectionField(_type) + return get_connection_field(_type) return Field(List(_type)) @@ -116,8 +116,8 @@ def convert_relatedfield_to_djangomodel(field, registry=None): model = field.model _type = registry.get_type_for_model(model) if issubclass(_type, Node): - return DjangoConnectionField(_type) - return Field(List(_type)) + return get_connection_field(_type) + return List(_type) @convert_django_field.register(models.OneToOneField) diff --git a/graphene-django/graphene_django/debug/sql/types.py b/graphene-django/graphene_django/debug/sql/types.py index 43d2c73a..6be6074c 100644 --- a/graphene-django/graphene_django/debug/sql/types.py +++ b/graphene-django/graphene_django/debug/sql/types.py @@ -1,4 +1,4 @@ -from .....core import Boolean, Float, ObjectType, String +from graphene import Boolean, Float, ObjectType, String class DjangoDebugBaseSQL(ObjectType): diff --git a/graphene-django/graphene_django/debug/tests/test_query.py b/graphene-django/graphene_django/debug/tests/test_query.py index 50976ad5..4ecc54e5 100644 --- a/graphene-django/graphene_django/debug/tests/test_query.py +++ b/graphene-django/graphene_django/debug/tests/test_query.py @@ -1,8 +1,8 @@ import pytest import graphene -from graphene.contrib.django import DjangoConnectionField, DjangoNode -from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED +from graphene_django import DjangoConnectionField, DjangoNode, DjangoObjectType +from graphene_django.utils import DJANGO_FILTER_INSTALLED from ...tests.models import Reporter from ..middleware import DjangoDebugMiddleware @@ -23,7 +23,7 @@ def test_should_query_field(): r2 = Reporter(last_name='Griffin') r2.save() - class ReporterType(DjangoNode): + class ReporterType(DjangoNode, DjangoObjectType): class Meta: model = Reporter @@ -69,7 +69,7 @@ def test_should_query_list(): r2 = Reporter(last_name='Griffin') r2.save() - class ReporterType(DjangoNode): + class ReporterType(DjangoNode, DjangoObjectType): class Meta: model = Reporter @@ -117,7 +117,7 @@ def test_should_query_connection(): r2 = Reporter(last_name='Griffin') r2.save() - class ReporterType(DjangoNode): + class ReporterType(DjangoNode, DjangoObjectType): class Meta: model = Reporter @@ -166,14 +166,14 @@ def test_should_query_connection(): @pytest.mark.skipif(not DJANGO_FILTER_INSTALLED, reason="requires django-filter") def test_should_query_connectionfilter(): - from graphene.contrib.django.filter import DjangoFilterConnectionField + from ...filter import DjangoFilterConnectionField r1 = Reporter(last_name='ABA') r1.save() r2 = Reporter(last_name='Griffin') r2.save() - class ReporterType(DjangoNode): + class ReporterType(DjangoNode, DjangoObjectType): class Meta: model = Reporter diff --git a/graphene-django/graphene_django/debug/types.py b/graphene-django/graphene_django/debug/types.py index c6a498f4..a75839c1 100644 --- a/graphene-django/graphene_django/debug/types.py +++ b/graphene-django/graphene_django/debug/types.py @@ -1,7 +1,6 @@ -from ....core.classtypes.objecttype import ObjectType -from ....core.types import Field +from graphene import ObjectType, List from .sql.types import DjangoDebugBaseSQL class DjangoDebug(ObjectType): - sql = Field(DjangoDebugBaseSQL.List()) + sql = List(DjangoDebugBaseSQL) diff --git a/graphene-django/graphene_django/fields.py b/graphene-django/graphene_django/fields.py index 087bd7c6..89770473 100644 --- a/graphene-django/graphene_django/fields.py +++ b/graphene-django/graphene_django/fields.py @@ -1,7 +1,7 @@ from django.db.models.query import QuerySet from graphene.relay import ConnectionField from graphql_relay.connection.arrayconnection import connection_from_list_slice -from .utils import maybe_queryset +from .utils import maybe_queryset, DJANGO_FILTER_INSTALLED class DjangoConnectionField(ConnectionField): @@ -39,3 +39,10 @@ class DjangoConnectionField(ConnectionField): connection_type=self.connection, edge_type=self.connection.Edge, ) + + +def get_connection_field(*args, **kwargs): + if DJANGO_FILTER_INSTALLED: + from .filter.fields import DjangoFilterConnectionField + return DjangoFilterConnectionField(*args, **kwargs) + return ConnectionField(*args, **kwargs) diff --git a/graphene-django/graphene_django/filter/__init__.py b/graphene-django/graphene_django/filter/__init__.py index 4f8b0579..71616b6d 100644 --- a/graphene-django/graphene_django/filter/__init__.py +++ b/graphene-django/graphene_django/filter/__init__.py @@ -1,5 +1,5 @@ import warnings -from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED +from ..utils import DJANGO_FILTER_INSTALLED if not DJANGO_FILTER_INSTALLED: warnings.warn( diff --git a/graphene-django/graphene_django/filter/fields.py b/graphene-django/graphene_django/filter/fields.py index d8457fa8..789c0f63 100644 --- a/graphene-django/graphene_django/filter/fields.py +++ b/graphene-django/graphene_django/filter/fields.py @@ -1,6 +1,7 @@ from ..fields import DjangoConnectionField from .utils import get_filtering_args_from_filterset, get_filterset_class +from graphene.types.argument import to_arguments class DjangoFilterConnectionField(DjangoConnectionField): @@ -18,7 +19,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): self.filterset_class = get_filterset_class(filterset_class, **meta) self.filtering_args = get_filtering_args_from_filterset(self.filterset_class, type) kwargs.setdefault('args', {}) - kwargs['args'].update(**self.filtering_args) + kwargs['args'].update(to_arguments(self.filtering_args)) super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs) def get_queryset(self, qs, args, info): diff --git a/graphene-django/graphene_django/filter/filterset.py b/graphene-django/graphene_django/filter/filterset.py index 6b9c8ac9..7aa40310 100644 --- a/graphene-django/graphene_django/filter/filterset.py +++ b/graphene-django/graphene_django/filter/filterset.py @@ -5,8 +5,7 @@ from django.utils.text import capfirst from django_filters import Filter, MultipleChoiceFilter from django_filters.filterset import FilterSet, FilterSetMetaclass -from graphene.contrib.django.forms import (GlobalIDFormField, - GlobalIDMultipleChoiceField) +from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphql_relay.node.node import from_global_id diff --git a/graphene-django/graphene_django/filter/tests/filters.py b/graphene-django/graphene_django/filter/tests/filters.py index bccd72d5..1a816691 100644 --- a/graphene-django/graphene_django/filter/tests/filters.py +++ b/graphene-django/graphene_django/filter/tests/filters.py @@ -1,6 +1,6 @@ import django_filters -from graphene.contrib.django.tests.models import Article, Pet, Reporter +from graphene_django.tests.models import Article, Pet, Reporter class ArticleFilter(django_filters.FilterSet): diff --git a/graphene-django/graphene_django/filter/tests/test_fields.py b/graphene-django/graphene_django/filter/tests/test_fields.py index 5b2875b2..d071dd9e 100644 --- a/graphene-django/graphene_django/filter/tests/test_fields.py +++ b/graphene-django/graphene_django/filter/tests/test_fields.py @@ -2,51 +2,58 @@ from datetime import datetime import pytest -from graphene import ObjectType, Schema -from graphene.contrib.django import DjangoNode -from graphene.contrib.django.forms import (GlobalIDFormField, - GlobalIDMultipleChoiceField) -from graphene.contrib.django.tests.models import Article, Pet, Reporter -from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED -from graphene.relay import NodeField +from graphene import ObjectType, Schema, Field +from graphene_django import DjangoNode, DjangoObjectType +from graphene_django.forms import (GlobalIDFormField, + GlobalIDMultipleChoiceField) +from graphene_django.tests.models import Article, Pet, Reporter +from graphene_django.utils import DJANGO_FILTER_INSTALLED pytestmark = [] if DJANGO_FILTER_INSTALLED: import django_filters - from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField, - GlobalIDMultipleChoiceFilter) - from graphene.contrib.django.filter.tests.filters import ArticleFilter, PetFilter + from graphene_django.filter import (GlobalIDFilter, DjangoFilterConnectionField, + GlobalIDMultipleChoiceFilter) + from graphene_django.filter.tests.filters import ArticleFilter, PetFilter else: pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed')) pytestmark.append(pytest.mark.django_db) -class ArticleNode(DjangoNode): +class ArticleNode(DjangoNode, DjangoObjectType): class Meta: model = Article -class ReporterNode(DjangoNode): +class ReporterNode(DjangoNode, DjangoObjectType): class Meta: model = Reporter -class PetNode(DjangoNode): +class PetNode(DjangoNode, DjangoObjectType): class Meta: model = Pet -schema = Schema() +# schema = Schema() + + +def get_args(field): + if isinstance(field.args, list): + return {arg.name: arg for arg in field.args} + else: + return field.args def assert_arguments(field, *arguments): ignore = ('after', 'before', 'first', 'last', 'orderBy') + args = get_args(field) actual = [ name - for name in schema.T(field.arguments) + for name in args if name not in ignore and not name.startswith('_') ] assert set(arguments) == set(actual), \ @@ -57,12 +64,14 @@ def assert_arguments(field, *arguments): def assert_orderable(field): - assert 'orderBy' in schema.T(field.arguments), \ + args = get_args(field) + assert 'orderBy' in args, \ 'Field cannot be ordered' def assert_not_orderable(field): - assert 'orderBy' not in schema.T(field.arguments), \ + args = get_args(field) + assert 'orderBy' not in args, \ 'Field can be ordered' @@ -122,7 +131,7 @@ def test_filter_shortcut_filterset_extra_meta(): def test_filter_filterset_information_on_meta(): - class ReporterFilterNode(DjangoNode): + class ReporterFilterNode(DjangoNode, DjangoObjectType): class Meta: model = Reporter @@ -135,14 +144,14 @@ def test_filter_filterset_information_on_meta(): def test_filter_filterset_information_on_meta_related(): - class ReporterFilterNode(DjangoNode): + class ReporterFilterNode(DjangoNode, DjangoObjectType): class Meta: model = Reporter filter_fields = ['first_name', 'articles'] filter_order_by = True - class ArticleFilterNode(DjangoNode): + class ArticleFilterNode(DjangoNode, DjangoObjectType): class Meta: model = Article @@ -152,25 +161,24 @@ def test_filter_filterset_information_on_meta_related(): class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_articles = DjangoFilterConnectionField(ArticleFilterNode) - reporter = NodeField(ReporterFilterNode) - article = NodeField(ArticleFilterNode) + reporter = Field(ReporterFilterNode) + article = Field(ArticleFilterNode) schema = Schema(query=Query) - schema.schema # Trigger the schema loading - articles_field = schema.get_type('ReporterFilterNode')._meta.fields_map['articles'] + articles_field = ReporterFilterNode._meta.graphql_type.get_fields()['articles'] assert_arguments(articles_field, 'headline', 'reporter') assert_orderable(articles_field) def test_filter_filterset_related_results(): - class ReporterFilterNode(DjangoNode): + class ReporterFilterNode(DjangoNode, DjangoObjectType): class Meta: model = Reporter filter_fields = ['first_name', 'articles'] filter_order_by = True - class ArticleFilterNode(DjangoNode): + class ArticleFilterNode(DjangoNode, DjangoObjectType): class Meta: model = Article @@ -180,8 +188,8 @@ def test_filter_filterset_related_results(): class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_articles = DjangoFilterConnectionField(ArticleFilterNode) - reporter = NodeField(ReporterFilterNode) - article = NodeField(ArticleFilterNode) + reporter = Field(ReporterFilterNode) + article = Field(ArticleFilterNode) r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com') r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com') diff --git a/graphene-django/graphene_django/filter/utils.py b/graphene-django/graphene_django/filter/utils.py index 5071ddc4..86a34f27 100644 --- a/graphene-django/graphene_django/filter/utils.py +++ b/graphene-django/graphene_django/filter/utils.py @@ -1,6 +1,6 @@ import six -from ....core.types import Argument, String +from graphene import Argument, String from .filterset import custom_filterset_factory, setup_filterset @@ -9,16 +9,16 @@ def get_filtering_args_from_filterset(filterset_class, type): a Graphene Field. These arguments will be available to filter against in the GraphQL """ - from graphene.contrib.django.form_converter import convert_form_field + from ..form_converter import convert_form_field args = {} for name, filter_field in six.iteritems(filterset_class.base_filters): - field_type = Argument(convert_form_field(filter_field.field)) + field_type = convert_form_field(filter_field.field) args[name] = field_type # Also add the 'order_by' field if filterset_class._meta.order_by: - args[filterset_class.order_by_field] = Argument(String()) + args[filterset_class.order_by_field] = String() return args diff --git a/graphene-django/graphene_django/tests/test_converter.py b/graphene-django/graphene_django/tests/test_converter.py index 28f464da..771dc55b 100644 --- a/graphene-django/graphene_django/tests/test_converter.py +++ b/graphene-django/graphene_django/tests/test_converter.py @@ -248,4 +248,4 @@ def test_should_postgres_range_convert_list(): from django.contrib.postgres.fields import IntegerRangeField field = assert_conversion(IntegerRangeField, graphene.List) assert isinstance(field.type, graphene.List) - # assert isinstance(field.type.of_type, graphene.Int) + assert field.type.of_type == get_graphql_type(graphene.Int) diff --git a/graphene-django/graphene_django/types.py b/graphene-django/graphene_django/types.py index 7f08ae3a..aa8e13d4 100644 --- a/graphene-django/graphene_django/types.py +++ b/graphene-django/graphene_django/types.py @@ -8,7 +8,7 @@ from graphene.relay import Node from graphene.relay.node import NodeMeta from .converter import convert_django_field_with_choices from graphene.types.options import Options -from .utils import get_model_fields, is_valid_django_model +from .utils import get_model_fields, is_valid_django_model, DJANGO_FILTER_INSTALLED from .registry import Registry, get_global_registry from graphene.utils.is_base_type import is_base_type from graphene.utils.copy_fields import copy_fields @@ -49,8 +49,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): if not is_base_type(bases, DjangoObjectTypeMeta): return super_new(cls, name, bases, attrs) - options = Options( - attrs.pop('Meta', None), + defaults = dict( name=None, description=None, model=None, @@ -59,6 +58,19 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): interfaces=(), registry=None ) + if DJANGO_FILTER_INSTALLED: + # In case Django filter is available, then + # we allow more attributes in Meta + defaults = dict( + defaults, + filter_fields=(), + filter_order_by=(), + ) + + options = Options( + attrs.pop('Meta', None), + **defaults + ) if not options.registry: options.registry = get_global_registry() assert isinstance(options.registry, Registry), 'The attribute registry in {}.Meta needs to be an instance of Registry, received "{}".'.format(name, options.registry) @@ -77,7 +89,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): fields=partial(cls._construct_fields, fields, options), interfaces=tuple(get_interfaces(interfaces + base_interfaces)) ) - options.get_fields = lambda: {} + options.get_fields = partial(cls._construct_fields, fields, options) if issubclass(cls, DjangoObjectType): options.registry.register(cls) diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index e88d1b82..3a5776ea 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -1,5 +1,6 @@ import re -from collections import Iterable +import copy +from collections import Iterable, OrderedDict import six @@ -72,8 +73,15 @@ class Connection(six.with_metaclass(ConnectionMeta, ObjectType)): class IterableConnectionField(Field): - def __init__(self, type, args={}, *other_args, **kwargs): - super(IterableConnectionField, self).__init__(type, args=connection_args, *other_args, **kwargs) + def __init__(self, type, *other_args, **kwargs): + args = kwargs.pop('args', {}) + if not args: + args = connection_args + else: + args = copy.copy(args) + args.update(connection_args) + + super(IterableConnectionField, self).__init__(type, args=args, *other_args, **kwargs) @property def type(self): diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 072b83a9..ad4b20a4 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -6,6 +6,7 @@ from graphql.type.definition import GraphQLArgument, GraphQLArgumentDefinition from graphql.utils.assert_valid_name import assert_valid_name from ..utils.orderedtype import OrderedType +from ..utils.str_converters import to_camel_case class Argument(GraphQLArgument, OrderedType): @@ -67,7 +68,7 @@ def to_arguments(*args, **extra): raise ValueError('Unknown argument "{}".'.format(default_name)) arg = Argument.copy_from(arg) - arg.name = arg.name or default_name + arg.name = arg.name or default_name and to_camel_case(default_name) assert arg.name, 'All arguments must have a name.' assert arg.name not in arguments_names, 'More than one Argument have same name "{}".'.format(arg.name) arguments.append(arg)