Skip to content

Commit b18fbb5

Browse files
committed
Merge pull request #79 from syrusakbary/feature/django
Improved Django integration
2 parents c733b38 + 112006c commit b18fbb5

20 files changed

+118
-49
lines changed

graphene/contrib/django/converter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def convert_field_to_float(field):
5858

5959
@convert_django_field.register(models.ManyToManyField)
6060
@convert_django_field.register(models.ManyToOneRel)
61+
@convert_django_field.register(models.ManyToManyRel)
6162
def convert_field_to_list_or_connection(field):
6263
from .fields import DjangoModelField, ConnectionOrListField
6364
model_field = DjangoModelField(get_related_model(field))

graphene/contrib/django/fields.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from ...core.types.definitions import List
55
from ...relay import ConnectionField
66
from ...relay.utils import is_node
7-
from .utils import get_type_for_model, maybe_queryset
7+
from .utils import DJANGO_FILTER_INSTALLED, get_type_for_model, maybe_queryset
88

99

1010
class DjangoConnectionField(ConnectionField):
@@ -37,7 +37,8 @@ def from_list(self, connection_type, resolved, args, info):
3737
class ConnectionOrListField(Field):
3838

3939
def internal_type(self, schema):
40-
from .filter.fields import DjangoFilterConnectionField
40+
if DJANGO_FILTER_INSTALLED:
41+
from .filter.fields import DjangoFilterConnectionField
4142

4243
model_field = self.type
4344
field_object_type = model_field.get_object_type(schema)
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
import warnings
12
from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED
23

34
if not DJANGO_FILTER_INSTALLED:
4-
raise Exception(
5+
warnings.warn(
56
"Use of django filtering requires the django-filter package "
6-
"be installed. You can do so using `pip install django-filter`"
7+
"be installed. You can do so using `pip install django-filter`", ImportWarning
78
)
9+
else:
10+
from .fields import DjangoFilterConnectionField
11+
from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter
812

9-
from .fields import DjangoFilterConnectionField
10-
from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter
11-
12-
__all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet',
13-
'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter']
13+
__all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet',
14+
'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter']

graphene/contrib/django/filter/filterset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from django.conf import settings
33
from django.db import models
44
from django.utils.text import capfirst
5-
from django_filters import Filter, MultipleChoiceFilter
6-
from django_filters.filterset import FilterSet, FilterSetMetaclass
75
from graphql_relay.node.node import from_global_id
86

7+
from django_filters import Filter, MultipleChoiceFilter
8+
from django_filters.filterset import FilterSet, FilterSetMetaclass
99
from graphene.contrib.django.forms import (GlobalIDFormField,
1010
GlobalIDMultipleChoiceField)
1111

graphene/contrib/django/filter/tests/filters.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import django_filters
2-
32
from graphene.contrib.django.tests.models import Article, Pet, Reporter
43

54

graphene/contrib/django/filter/tests/test_fields.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ def assert_not_orderable(field):
6969
def test_filter_explicit_filterset_arguments():
7070
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter)
7171
assert_arguments(field,
72-
'headline', 'headlineIcontains',
73-
'pubDate', 'pubDateGt', 'pubDateLt',
72+
'headline', 'headline_Icontains',
73+
'pubDate', 'pubDate_Gt', 'pubDate_Lt',
7474
'reporter',
7575
)
7676

@@ -89,7 +89,7 @@ def test_filter_shortcut_filterset_arguments_dict():
8989
'reporter': ['exact'],
9090
})
9191
assert_arguments(field,
92-
'headline', 'headlineIcontains',
92+
'headline', 'headline_Icontains',
9393
'reporter',
9494
)
9595

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,72 @@
11
import importlib
22
import json
3+
from distutils.version import StrictVersion
4+
from optparse import make_option
35

6+
from django import get_version as get_django_version
47
from django.core.management.base import BaseCommand, CommandError
58

9+
LT_DJANGO_1_8 = StrictVersion(get_django_version()) < StrictVersion('1.8')
610

7-
class Command(BaseCommand):
11+
if LT_DJANGO_1_8:
12+
class CommandArguments(BaseCommand):
13+
option_list = BaseCommand.option_list + (
14+
make_option(
15+
'--schema',
16+
type=str,
17+
dest='schema',
18+
default='',
19+
help='Django app containing schema to dump, e.g. myproject.core.schema',
20+
),
21+
make_option(
22+
'--out',
23+
type=str,
24+
dest='out',
25+
default='',
26+
help='Output file (default: schema.json)'
27+
),
28+
)
29+
else:
30+
class CommandArguments(BaseCommand):
31+
32+
def add_arguments(self, parser):
33+
from django.conf import settings
34+
parser.add_argument(
35+
'--schema',
36+
type=str,
37+
dest='schema',
38+
default=getattr(settings, 'GRAPHENE_SCHEMA', ''),
39+
help='Django app containing schema to dump, e.g. myproject.core.schema')
40+
41+
parser.add_argument(
42+
'--out',
43+
type=str,
44+
dest='out',
45+
default=getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json'),
46+
help='Output file (default: schema.json)')
47+
48+
49+
class Command(CommandArguments):
850
help = 'Dump Graphene schema JSON to file'
951
can_import_settings = True
1052

11-
def add_arguments(self, parser):
12-
from django.conf import settings
13-
parser.add_argument(
14-
'--schema',
15-
type=str,
16-
dest='schema',
17-
default=getattr(settings, 'GRAPHENE_SCHEMA', ''),
18-
help='Django app containing schema to dump, e.g. myproject.core.schema')
19-
20-
parser.add_argument(
21-
'--out',
22-
type=str,
23-
dest='out',
24-
default=getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json'),
25-
help='Output file (default: schema.json)')
53+
def save_file(self, out, schema_dict):
54+
with open(out, 'w') as outfile:
55+
json.dump(schema_dict, outfile)
2656

2757
def handle(self, *args, **options):
28-
schema_module = options['schema']
29-
if schema_module == '':
58+
from django.conf import settings
59+
schema = options.get('schema') or getattr(settings, 'GRAPHENE_SCHEMA', '')
60+
out = options.get('out') or getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json')
61+
62+
if schema == '':
3063
raise CommandError('Specify schema on GRAPHENE_SCHEMA setting or by using --schema')
31-
i = importlib.import_module(schema_module)
64+
i = importlib.import_module(schema)
3265

3366
schema_dict = {'data': i.schema.introspect()}
67+
self.save_file(out, schema_dict)
3468

35-
with open(options['out'], 'w') as outfile:
36-
json.dump(schema_dict, outfile)
69+
style = getattr(self, 'style', None)
70+
SUCCESS = getattr(style, 'SUCCESS', lambda x: x)
3771

38-
self.stdout.write(self.style.SUCCESS('Successfully dumped GraphQL schema to %s' % options['out']))
72+
self.stdout.write(SUCCESS('Successfully dumped GraphQL schema to %s' % out))

graphene/contrib/django/tests/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ class Pet(models.Model):
77
name = models.CharField(max_length=30)
88

99

10+
class Film(models.Model):
11+
reporters = models.ManyToManyField('Reporter',
12+
related_name='films')
13+
14+
1015
class Reporter(models.Model):
1116
first_name = models.CharField(max_length=30)
1217
last_name = models.CharField(max_length=30)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.core import management
2+
from mock import patch
3+
from six import StringIO
4+
5+
6+
@patch('graphene.contrib.django.management.commands.graphql_schema.Command.save_file')
7+
def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
8+
settings.GRAPHENE_SCHEMA = 'graphene.contrib.django.tests.test_urls'
9+
out = StringIO()
10+
management.call_command('graphql_schema', schema='', stdout=out)
11+
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()

graphene/contrib/django/tests/test_schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from py.test import raises
2-
from tests.utils import assert_equal_lists
32

43
from graphene.contrib.django import DjangoObjectType
4+
from tests.utils import assert_equal_lists
55

66
from .models import Reporter
77

@@ -29,7 +29,7 @@ class Meta:
2929
model = Reporter
3030
assert_equal_lists(
3131
ReporterType2._meta.fields_map.keys(),
32-
['articles', 'first_name', 'last_name', 'email', 'pets', 'id']
32+
['articles', 'first_name', 'last_name', 'email', 'pets', 'id', 'films']
3333
)
3434

3535

graphene/contrib/django/tests/test_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from graphql.core.type import GraphQLObjectType
22
from mock import patch
3-
from tests.utils import assert_equal_lists
43

54
from graphene import Schema
65
from graphene.contrib.django.types import DjangoNode, DjangoObjectType
76
from graphene.core.fields import Field
87
from graphene.core.types.scalars import Int
98
from graphene.relay.fields import GlobalIDField
9+
from tests.utils import assert_equal_lists
1010

1111
from .models import Article, Reporter
1212

graphene/contrib/django/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
try:
1010
import django_filters # noqa
1111
DJANGO_FILTER_INSTALLED = True
12-
except ImportError:
12+
except (ImportError, AttributeError):
13+
# AtributeError raised if DjangoFilters installed with a incompatible Django Version
1314
DJANGO_FILTER_INSTALLED = False
1415

1516

@@ -35,6 +36,8 @@ def get_reverse_fields(model):
3536
yield new_related
3637
elif isinstance(related, models.ManyToOneRel):
3738
yield related
39+
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
40+
yield related
3841

3942

4043
class WrappedQueryset(LazyList):

graphene/core/tests/test_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from graphql.core import graphql
22
from py.test import raises
3-
from tests.utils import assert_equal_lists
43

54
from graphene import Interface, List, ObjectType, Schema, String
65
from graphene.core.fields import Field
76
from graphene.core.types.base import LazyType
7+
from tests.utils import assert_equal_lists
88

99
schema = Schema(name='My own schema')
1010

graphene/core/types/tests/test_argument.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def test_to_arguments_wrong_type():
4848

4949

5050
def test_snake_case_args():
51-
resolver = lambda instance, args, info: args['my_arg']['inner_arg']
51+
def resolver(instance, args, info):
52+
return args['my_arg']['inner_arg']
5253
r = snake_case_args(resolver)
5354
assert r(None, {'myArg': {'innerArg': 3}}, None) == 3

graphene/core/types/tests/test_base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ def test_orderedtype_different():
2525

2626
@patch('graphene.core.types.field.Field')
2727
def test_type_as_field_called(Field):
28-
resolver = lambda x: x
28+
def resolver(x):
29+
return x
30+
2931
a = MountedType(2, description='A', resolver=resolver)
3032
a.as_field()
3133
Field.assert_called_with(
@@ -45,7 +47,8 @@ def test_type_as_argument_called(Argument):
4547

4648

4749
def test_type_as_field():
48-
resolver = lambda x: x
50+
def resolver(x):
51+
return x
4952

5053
class MyObjectType(ObjectType):
5154
t = MountedType(description='A', resolver=resolver)

graphene/core/types/tests/test_field.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212

1313
def test_field_internal_type():
14-
resolver = lambda *args: 'RESOLVED'
14+
def resolver(*args):
15+
return 'RESOLVED'
1516

1617
field = Field(String(), description='My argument', resolver=resolver)
1718

@@ -132,7 +133,8 @@ class Query(ObjectType):
132133

133134

134135
def test_field_resolve_argument():
135-
resolver = lambda instance, args, info: args.get('first_name')
136+
def resolver(instance, args, info):
137+
return args.get('first_name')
136138

137139
field = Field(String(), first_name=String(), description='My argument', resolver=resolver)
138140

graphene/utils/str_converters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def to_camel_case(snake_str):
77
components = snake_str.split('_')
88
# We capitalize the first letter of each component except the first one
99
# with the 'title' method and join them together.
10-
return components[0] + "".join(x.title() for x in components[1:])
10+
return components[0] + "".join(x.title() if x else '_' for x in components[1:])
1111

1212

1313
# From this response in Stackoverflow

graphene/utils/tests/test_resolve_only_args.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33

44
def test_resolve_only_args():
5+
6+
def resolver(*args, **kwargs):
7+
return kwargs
8+
59
my_data = {'one': 1, 'two': 2}
6-
resolver = lambda *args, **kwargs: kwargs
10+
711
wrapped = resolve_only_args(resolver)
812
assert wrapped(None, my_data, None) == my_data

graphene/utils/tests/test_str_converter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
def test_snake_case():
55
assert to_snake_case('snakesOnAPlane') == 'snakes_on_a_plane'
66
assert to_snake_case('SnakesOnAPlane') == 'snakes_on_a_plane'
7+
assert to_snake_case('SnakesOnA_Plane') == 'snakes_on_a__plane'
78
assert to_snake_case('snakes_on_a_plane') == 'snakes_on_a_plane'
9+
assert to_snake_case('snakes_on_a__plane') == 'snakes_on_a__plane'
810
assert to_snake_case('IPhoneHysteria') == 'i_phone_hysteria'
911
assert to_snake_case('iPhoneHysteria') == 'i_phone_hysteria'
1012

1113

1214
def test_camel_case():
1315
assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane'
16+
assert to_camel_case('snakes_on_a__plane') == 'snakesOnA_Plane'
1417
assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria'

tests/django_settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
SECRET_KEY = 1
22

33
INSTALLED_APPS = [
4+
'graphene.contrib.django',
45
'graphene.contrib.django.tests',
56
'examples.starwars_django',
67
]

0 commit comments

Comments
 (0)