Merge remote-tracking branch 'up/master' into enum_conversion_fixes

This commit is contained in:
Jason Kraus 2019-06-10 09:30:01 -07:00
commit 71ba7fb82b
20 changed files with 384 additions and 149 deletions

2
.gitignore vendored
View File

@ -78,3 +78,5 @@ Session.vim
*~
# auto-generated tag files
tags
.tox/
.pytest_cache/

View File

@ -1,62 +1,60 @@
language: python
sudo: required
dist: xenial
python:
- 2.7
- 3.4
- 3.5
- 3.6
- 3.7
install:
- |
if [ "$TEST_TYPE" = build ]; then
pip install -e .[test]
pip install psycopg2==2.8.2 # Required for Django postgres fields testing
pip install django==$DJANGO_VERSION
python setup.py develop
elif [ "$TEST_TYPE" = lint ]; then
pip install flake8==3.7.7
fi
script:
- |
if [ "$TEST_TYPE" = lint ]; then
echo "Checking Python code lint."
flake8 graphene_django
exit
elif [ "$TEST_TYPE" = build ]; then
py.test --cov=graphene_django graphene_django examples
fi
after_success:
- |
if [ "$TEST_TYPE" = build ]; then
coveralls
fi
env:
matrix:
- TEST_TYPE=build DJANGO_VERSION=1.11
- DJANGO=1.11
- DJANGO=2.1
- DJANGO=2.2
- DJANGO=master
install:
- TOX_ENV=py${TRAVIS_PYTHON_VERSION}-django${DJANGO}
- pip install tox
- tox -e $TOX_ENV --notest
script:
- tox -e $TOX_ENV
after_success:
- tox -e $TOX_ENV -- pip install coveralls
- tox -e $TOX_ENV -- coveralls $COVERALLS_OPTION
matrix:
fast_finish: true
include:
- python: '3.4'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.5'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.5'
env: TEST_TYPE=build DJANGO_VERSION=2.1
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.1
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.2
- python: '3.7'
env: TEST_TYPE=build DJANGO_VERSION=2.2
- python: '2.7'
env: TEST_TYPE=lint
- python: '3.6'
env: TEST_TYPE=lint
- python: '3.7'
env: TEST_TYPE=lint
- python: 3.5
script: tox -e lint
exclude:
- python: 2.7
env: DJANGO=2.1
- python: 2.7
env: DJANGO=2.2
- python: 2.7
env: DJANGO=master
- python: 3.4
env: DJANGO=2.1
- python: 3.4
env: DJANGO=2.2
- python: 3.4
env: DJANGO=master
- python: 3.5
env: DJANGO=master
- python: 3.7
env: DJANGO=1.10
- python: 3.7
env: DJANGO=1.11
allow_failures:
- python: 3.7
- env: DJANGO=master
deploy:
provider: pypi
user: syrusakbary

View File

@ -100,7 +100,7 @@ features of ``django-filter``. This is done by transparently creating a
``filter_fields``.
However, you may find this to be insufficient. In these cases you can
create your own ``Filterset`` as follows:
create your own ``FilterSet``. You can pass it directly as follows:
.. code:: python
@ -127,6 +127,33 @@ create your own ``Filterset`` as follows:
all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter)
You can also specify the ``FilterSet`` class using the ``filerset_class``
parameter when defining your ``DjangoObjectType``, however, this can't be used
in unison with the ``filter_fields`` parameter:
.. code:: python
class AnimalFilter(django_filters.FilterSet):
# Do case-insensitive lookups on 'name'
name = django_filters.CharFilter(lookup_expr=['iexact'])
class Meta:
# Assume you have an Animal model defined with the following fields
model = Animal
fields = ['name', 'genus', 'is_domesticated']
class AnimalNode(DjangoObjectType):
class Meta:
model = Animal
filterset_class = AnimalFilter
interfaces = (relay.Node, )
class Query(ObjectType):
animal = relay.Node.Field(AnimalNode)
all_animals = DjangoFilterConnectionField(AnimalNode)
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
in a ``django_filters.FilterSet`` instance. You can use this to customize your
filters to be context-dependent. We could modify the ``AnimalFilter`` above to

View File

@ -35,6 +35,8 @@ Advanced Usage
The ``--indent`` option can be used to specify the number of indentation spaces to
be used in the output. Defaults to `None` which displays all data on a single line.
The ``--watch`` option can be used to run ``./manage.py graphql_schema`` in watch mode, where it will automatically output a new schema every time there are file changes in your project
To simplify the command to ``./manage.py graphql_schema``, you can
specify the parameters in your settings.py:

View File

@ -199,7 +199,9 @@ You can use relay with mutations. A Relay mutation must inherit from
.. code:: python
import graphene import relay, DjangoObjectType
import graphene
from graphene import relay
from graphene_django import DjangoObjectType
from graphql_relay import from_global_id
from .queries import QuestionType
@ -214,7 +216,7 @@ You can use relay with mutations. A Relay mutation must inherit from
@classmethod
def mutate_and_get_payload(cls, root, info, text, id):
question = Question.objects.get(pk=from_global_id(id))
question = Question.objects.get(pk=from_global_id(id)[1])
question.text = text
question.save()
return QuestionMutation(question=question)

View File

@ -1,5 +1,5 @@
graphene
graphene-django
graphql-core>=2.1rc1
django==1.11.19
django==1.11.20
django-filter>=2

View File

@ -180,19 +180,22 @@ def convert_field_to_list_or_connection(field, registry=None):
if not _type:
return
description = field.help_text if isinstance(field, models.ManyToManyField) else field.field.help_text
# If there is a connection, we should transform the field
# into a DjangoConnectionField
if _type._meta.connection:
# Use a DjangoFilterConnectionField if there are
# defined filter_fields in the DjangoObjectType Meta
if _type._meta.filter_fields:
# defined filter_fields or a filterset_class in the
# DjangoObjectType Meta
if _type._meta.filter_fields or _type._meta.filterset_class:
from .filter.fields import DjangoFilterConnectionField
return DjangoFilterConnectionField(_type)
return DjangoFilterConnectionField(_type, description=description)
return DjangoConnectionField(_type)
return DjangoConnectionField(_type, description=description)
return DjangoListField(_type)
return DjangoListField(_type, description=description)
return Dynamic(dynamic_type)

View File

@ -50,9 +50,7 @@ def test_should_query_field():
"""
expected = {
"reporter": {"lastName": "ABA"},
"_debug": {
"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]
},
"_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]},
}
schema = graphene.Schema(query=Query)
result = schema.execute(

View File

@ -40,8 +40,10 @@ class DjangoFilterConnectionField(DjangoConnectionField):
if self._extra_filter_meta:
meta.update(self._extra_filter_meta)
filterset_class = self._provided_filterset_class or (
self.node_type._meta.filterset_class)
self._filterset_class = get_filterset_class(
self._provided_filterset_class, **meta
filterset_class, **meta
)
return self._filterset_class

View File

@ -227,6 +227,73 @@ def test_filter_filterset_information_on_meta_related():
assert_not_orderable(articles_field)
def test_filter_filterset_class_filter_fields_exception():
with pytest.raises(Exception):
class ReporterFilter(FilterSet):
class Meta:
model = Reporter
fields = ["first_name", "articles"]
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
filterset_class = ReporterFilter
filter_fields = ["first_name", "articles"]
def test_filter_filterset_class_information_on_meta():
class ReporterFilter(FilterSet):
class Meta:
model = Reporter
fields = ["first_name", "articles"]
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
filterset_class = ReporterFilter
field = DjangoFilterConnectionField(ReporterFilterNode)
assert_arguments(field, "first_name", "articles")
assert_not_orderable(field)
def test_filter_filterset_class_information_on_meta_related():
class ReporterFilter(FilterSet):
class Meta:
model = Reporter
fields = ["first_name", "articles"]
class ArticleFilter(FilterSet):
class Meta:
model = Article
fields = ["headline", "reporter"]
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
filterset_class = ReporterFilter
class ArticleFilterNode(DjangoObjectType):
class Meta:
model = Article
interfaces = (Node,)
filterset_class = ArticleFilter
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
reporter = Field(ReporterFilterNode)
article = Field(ArticleFilterNode)
schema = Schema(query=Query)
articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
assert_arguments(articles_field, "headline", "reporter")
assert_not_orderable(articles_field)
def test_filter_filterset_related_results():
class ReporterFilterNode(DjangoObjectType):
class Meta:

View File

@ -1,7 +1,9 @@
import importlib
import json
import functools
from django.core.management.base import BaseCommand, CommandError
from django.utils import autoreload
from graphene_django.settings import graphene_settings
@ -32,6 +34,14 @@ class CommandArguments(BaseCommand):
help="Output file indent (default: None)",
)
parser.add_argument(
"--watch",
dest="watch",
default=False,
action="store_true",
help="Updates the schema on file changes (default: False)",
)
class Command(CommandArguments):
help = "Dump Graphene schema JSON to file"
@ -41,6 +51,18 @@ class Command(CommandArguments):
with open(out, "w") as outfile:
json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
def get_schema(self, schema, out, indent):
schema_dict = {"data": schema.introspect()}
if out == "-":
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
else:
self.save_file(out, schema_dict, indent)
style = getattr(self, "style", None)
success = getattr(style, "SUCCESS", lambda x: x)
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
def handle(self, *args, **options):
options_schema = options.get("schema")
@ -63,13 +85,10 @@ class Command(CommandArguments):
)
indent = options.get("indent")
schema_dict = {"data": schema.introspect()}
if out == "-":
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
watch = options.get("watch")
if watch:
autoreload.run_with_reloader(
functools.partial(self.get_schema, schema, out, indent)
)
else:
self.save_file(out, schema_dict, indent)
style = getattr(self, "style", None)
success = getattr(style, "SUCCESS", lambda x: x)
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
self.get_schema(schema, out, indent)

View File

@ -4,3 +4,8 @@ from django.db import models
class MyFakeModel(models.Model):
cool_name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
class MyFakeModelWithPassword(models.Model):
cool_name = models.CharField(max_length=50)
password = models.CharField(max_length=50)

View File

@ -27,6 +27,8 @@ def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=Fals
name
in exclude_fields # or
# name in already_created_fields
) or (
field.write_only and not is_input # don't show write_only fields in Query
)
if is_not_in_only or is_excluded:
@ -138,6 +140,7 @@ class SerializerMutation(ClientIDMutation):
kwargs = {}
for f, field in serializer.fields.items():
if not field.write_only:
kwargs[f] = field.get_attribute(obj)
return cls(errors=None, **kwargs)

View File

@ -7,7 +7,7 @@ from py.test import mark
from rest_framework import serializers
from ...types import DjangoObjectType
from ..models import MyFakeModel
from ..models import MyFakeModel, MyFakeModelWithPassword
from ..mutation import SerializerMutation
@ -86,6 +86,47 @@ def test_exclude_fields():
assert "created" not in MyMutation.Input._meta.fields
@mark.django_db
def test_write_only_field():
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = MyFakeModelWithPassword
fields = ["cool_name", "password"]
class MyMutation(SerializerMutation):
class Meta:
serializer_class = WriteOnlyFieldModelSerializer
result = MyMutation.mutate_and_get_payload(
None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
)
assert hasattr(result, "cool_name")
assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible"
@mark.django_db
def test_write_only_field_using_extra_kwargs():
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyFakeModelWithPassword
fields = ["cool_name", "password"]
extra_kwargs = {"password": {"write_only": True}}
class MyMutation(SerializerMutation):
class Meta:
serializer_class = WriteOnlyFieldModelSerializer
result = MyMutation.mutate_and_get_payload(
None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
)
assert hasattr(result, "cool_name")
assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible"
def test_nested_model():
class MyFakeModelGrapheneType(DjangoObjectType):
class Meta:

View File

@ -66,6 +66,11 @@ class Reporter(models.Model):
self.__class__ = CNNReporter
class CNNReporterManager(models.Manager):
def get_queryset(self):
return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2)
class CNNReporter(Reporter):
"""
This class is a proxy model for Reporter, used for testing
@ -75,6 +80,8 @@ class CNNReporter(Reporter):
class Meta:
proxy = True
objects = CNNReporterManager()
class Article(models.Model):
headline = models.CharField(max_length=100)

View File

@ -1,3 +1,4 @@
import base64
import datetime
import pytest
@ -7,6 +8,7 @@ from py.test import raises
from django.db.models import Q
from graphql_relay import to_global_id
import graphene
from graphene.relay import Node
@ -226,6 +228,62 @@ def test_should_node():
assert result.data == expected
def test_should_query_onetoone_fields():
film = Film(id=1)
film_details = FilmDetails(id=1, film=film)
class FilmNode(DjangoObjectType):
class Meta:
model = Film
interfaces = (Node,)
class FilmDetailsNode(DjangoObjectType):
class Meta:
model = FilmDetails
interfaces = (Node,)
class Query(graphene.ObjectType):
film = graphene.Field(FilmNode)
film_details = graphene.Field(FilmDetailsNode)
def resolve_film(root, info):
return film
def resolve_film_details(root, info):
return film_details
query = """
query FilmQuery {
filmDetails {
id
film {
id
}
}
film {
id
details {
id
}
}
}
"""
expected = {
"filmDetails": {
"id": "RmlsbURldGFpbHNOb2RlOjE=",
"film": {"id": "RmlsbU5vZGU6MQ=="},
},
"film": {
"id": "RmlsbU5vZGU6MQ==",
"details": {"id": "RmlsbURldGFpbHNOb2RlOjE="},
},
}
schema = graphene.Schema(query=Query)
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_should_query_connectionfields():
class ReporterType(DjangoObjectType):
class Meta:
@ -895,8 +953,7 @@ def test_should_handle_inherited_choices():
def test_proxy_model_support():
"""
This test asserts that we can query for all Reporters,
even if some are of a proxy model type at runtime.
This test asserts that we can query for all Reporters and proxied Reporters.
"""
class ReporterType(DjangoObjectType):
@ -905,11 +962,17 @@ def test_proxy_model_support():
interfaces = (Node,)
use_connection = True
reporter_1 = Reporter.objects.create(
class CNNReporterType(DjangoObjectType):
class Meta:
model = CNNReporter
interfaces = (Node,)
use_connection = True
reporter = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
reporter_2 = CNNReporter.objects.create(
cnn_reporter = CNNReporter.objects.create(
first_name="Some",
last_name="Guy",
email="someguy@cnn.com",
@ -919,6 +982,7 @@ def test_proxy_model_support():
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
cnn_reporters = DjangoConnectionField(CNNReporterType)
schema = graphene.Schema(query=Query)
query = """
@ -930,14 +994,26 @@ def test_proxy_model_support():
}
}
}
cnnReporters {
edges {
node {
id
}
}
}
}
"""
expected = {
"allReporters": {
"edges": [
{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
{"node": {"id": to_global_id("ReporterType", reporter.id)}},
{"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}},
]
},
"cnnReporters": {
"edges": [
{"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}}
]
}
}
@ -947,68 +1023,6 @@ def test_proxy_model_support():
assert result.data == expected
def test_proxy_model_fails():
"""
This test asserts that if you try to query for a proxy model,
that query will fail with:
GraphQLError('Expected value of type "CNNReporterType" but got:
CNNReporter.',)
This is because a proxy model has the identical model definition
to its superclass, and defines its behavior at runtime, rather than
at the database level. Currently, filtering objects of the proxy models'
type isn't supported. It would require a field on the model that would
represent the type, and it doesn't seem like there is a clear way to
enforce this pattern across all projects
"""
class CNNReporterType(DjangoObjectType):
class Meta:
model = CNNReporter
interfaces = (Node,)
use_connection = True
reporter_1 = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
reporter_2 = CNNReporter.objects.create(
first_name="Some",
last_name="Guy",
email="someguy@cnn.com",
a_choice=1,
reporter_type=2, # set this guy to be CNN
)
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(CNNReporterType)
schema = graphene.Schema(query=Query)
query = """
query ProxyModelQuery {
allReporters {
edges {
node {
id
}
}
}
}
"""
expected = {
"allReporters": {
"edges": [
{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
]
}
}
result = schema.execute(query)
assert result.errors
def test_should_resolve_get_queryset_connectionfields():
reporter_1 = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1

View File

@ -45,6 +45,7 @@ class DjangoObjectTypeOptions(ObjectTypeOptions):
connection = None # type: Type[Connection]
filter_fields = ()
filterset_class = None
class DjangoObjectType(ObjectType):
@ -57,6 +58,7 @@ class DjangoObjectType(ObjectType):
only_fields=(),
exclude_fields=(),
filter_fields=None,
filterset_class=None,
connection=None,
connection_class=None,
use_connection=None,
@ -76,8 +78,14 @@ class DjangoObjectType(ObjectType):
'Registry, received "{}".'
).format(cls.__name__, registry)
if not DJANGO_FILTER_INSTALLED and filter_fields:
raise Exception("Can only set filter_fields if Django-Filter is installed")
if filter_fields and filterset_class:
raise Exception("Can't set both filter_fields and filterset_class")
if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class):
raise Exception((
"Can only set filter_fields or filterset_class if "
"Django-Filter is installed"
))
django_fields = yank_fields_from_attrs(
construct_fields(model, registry, only_fields, exclude_fields), _as=Field
@ -108,6 +116,7 @@ class DjangoObjectType(ObjectType):
_meta.model = model
_meta.registry = registry
_meta.filter_fields = filter_fields
_meta.filterset_class = filterset_class
_meta.fields = django_fields
_meta.connection = connection
@ -131,7 +140,11 @@ class DjangoObjectType(ObjectType):
if not is_valid_django_model(type(root)):
raise Exception(('Received incompatible instance "{}".').format(root))
if cls._meta.model._meta.proxy:
model = root._meta.model
else:
model = root._meta.model._meta.concrete_model
return model == cls._meta.model
@classmethod

View File

@ -22,7 +22,7 @@ class GraphQLTestCase(TestCase):
"Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase."
)
cls._client = Client(cls.GRAPHQL_SCHEMA)
cls._client = Client()
def query(self, query, op_name=None, input_data=None):
"""

View File

@ -18,7 +18,8 @@ def get_reverse_fields(model, local_field_names):
if name in local_field_names:
continue
related = getattr(attr, "rel", None)
# "rel" for FK and M2M relations and "related" for O2O Relations
related = getattr(attr, "rel", None) or getattr(attr, "related", None)
if isinstance(related, models.ManyToOneRel):
yield (name, related)
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:

31
tox.ini Normal file
View File

@ -0,0 +1,31 @@
[tox]
envlist = py{2.7,3.4,3.5,3.6,3.7,pypy,pypy3}-django{1.10,1.11,2.0,2.1,2.2,master},lint
[testenv]
passenv = *
usedevelop = True
setenv =
DJANGO_SETTINGS_MODULE=django_test_settings
basepython =
py2.7: python2.7
py3.4: python3.4
py3.5: python3.5
py3.6: python3.6
py3.7: python3.7
pypypy: pypy
pypypy3: pypy3
deps =
-e.[test]
psycopg2
django1.10: Django>=1.10,<1.11
django1.11: Django>=1.11,<1.12
django2.0: Django>=2.0
django2.1: Django>=2.1
djangomaster: https://github.com/django/django/archive/master.zip
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
[testenv:lint]
basepython = python
deps =
prospector
commands = prospector graphene_django -0