mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-07 22:03:12 +03:00
Merge branch with 'master'
This commit is contained in:
commit
08684973ed
22
Makefile
22
Makefile
|
@ -1,11 +1,29 @@
|
|||
.PHONY: dev-setup ## Install development dependencies
|
||||
dev-setup:
|
||||
pip install -e ".[dev]"
|
||||
|
||||
.PHONY: install-dev
|
||||
install-dev: dev-setup # Alias install-dev -> dev-setup
|
||||
|
||||
.PHONY: tests
|
||||
tests:
|
||||
py.test graphene_django --cov=graphene_django -vv
|
||||
|
||||
format:
|
||||
black --exclude "/migrations/" graphene_django examples
|
||||
.PHONY: test
|
||||
test: tests # Alias test -> tests
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
black --exclude "/migrations/" graphene_django examples setup.py
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
flake8 graphene_django examples
|
||||
|
||||
.PHONY: docs ## Generate docs
|
||||
docs: dev-setup
|
||||
cd docs && make install && make html
|
||||
|
||||
.PHONY: docs-live ## Generate docs with live reloading
|
||||
docs-live: dev-setup
|
||||
cd docs && make install && make livehtml
|
||||
|
|
|
@ -38,12 +38,12 @@ GRAPHENE = {
|
|||
We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries.
|
||||
|
||||
```python
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from graphene_django.views import GraphQLView
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
|
||||
path('graphql', GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
```
|
||||
|
||||
|
@ -100,4 +100,4 @@ To learn more check out the following [examples](examples/):
|
|||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
|
|
@ -48,12 +48,20 @@ help:
|
|||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
.PHONY: install ## to install all documentation related requirements
|
||||
install:
|
||||
pip install -r requirements.txt
|
||||
|
||||
.PHONY: html
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
.PHONY: livehtml ## to build and serve live-reloading documentation
|
||||
livehtml:
|
||||
sphinx-autobuild -b html --watch ../graphene_django $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
|
||||
.PHONY: dirhtml
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
|
|
0
docs/_static/.gitkeep
vendored
Normal file
0
docs/_static/.gitkeep
vendored
Normal file
|
@ -154,7 +154,8 @@ Adding Login Required
|
|||
To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.:
|
||||
|
||||
.. code:: python
|
||||
#views.py
|
||||
|
||||
# views.py
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from graphene_django.views import GraphQLView
|
||||
|
|
|
@ -151,7 +151,7 @@ customize the look up with the ``lookup_field`` attribute on the ``SerializerMut
|
|||
.. code:: python
|
||||
|
||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||
from .serializers imoprt MyModelSerializer
|
||||
from .serializers import MyModelSerializer
|
||||
|
||||
|
||||
class AwesomeModelMutation(SerializerMutation):
|
||||
|
@ -168,7 +168,7 @@ Use the method ``get_serializer_kwargs`` to override how updates are applied.
|
|||
.. code:: python
|
||||
|
||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||
from .serializers imoprt MyModelSerializer
|
||||
from .serializers import MyModelSerializer
|
||||
|
||||
|
||||
class AwesomeModelMutation(SerializerMutation):
|
||||
|
@ -199,7 +199,7 @@ You can use relay with mutations. A Relay mutation must inherit from
|
|||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
import graphene
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphql_relay import from_global_id
|
||||
|
|
110
docs/queries.rst
110
docs/queries.rst
|
@ -41,14 +41,18 @@ Full example
|
|||
return Question.objects.get(pk=question_id)
|
||||
|
||||
|
||||
Fields
|
||||
------
|
||||
Specifying which fields to include
|
||||
----------------------------------
|
||||
|
||||
By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL.
|
||||
If you don't want to do this you can change this by setting either ``only_fields`` and ``exclude_fields``.
|
||||
If you only want a subset of fields to be present, you can do so using
|
||||
``fields`` or ``exclude``. It is strongly recommended that you explicitly set
|
||||
all fields that should be exposed using the fields attribute.
|
||||
This will make it less likely to result in unintentionally exposing data when
|
||||
your models change.
|
||||
|
||||
only_fields
|
||||
~~~~~~~~~~~
|
||||
``fields``
|
||||
~~~~~~~~~~
|
||||
|
||||
Show **only** these fields on the model:
|
||||
|
||||
|
@ -57,24 +61,35 @@ Show **only** these fields on the model:
|
|||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
only_fields = ('question_text')
|
||||
fields = ('id', 'question_text')
|
||||
|
||||
You can also set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used.
|
||||
|
||||
exclude_fields
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Show all fields **except** those in ``exclude_fields``:
|
||||
For example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
exclude_fields = ('question_text')
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
Customised fields
|
||||
~~~~~~~~~~~~~~~~~
|
||||
``exclude``
|
||||
~~~~~~~~~~~
|
||||
|
||||
Show all fields **except** those in ``exclude``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
exclude = ('question_text',)
|
||||
|
||||
|
||||
Customising fields
|
||||
------------------
|
||||
|
||||
You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver:
|
||||
|
||||
|
@ -84,7 +99,7 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
|
|||
|
||||
class Meta:
|
||||
model = Question
|
||||
exclude_fields = ('question_text')
|
||||
fields = ('id', 'question_text')
|
||||
|
||||
extra_field = graphene.String()
|
||||
|
||||
|
@ -92,6 +107,71 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
|
|||
return 'hello!'
|
||||
|
||||
|
||||
Choices to Enum conversion
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default Graphene-Django will convert any Django fields that have `choices`_
|
||||
defined into a GraphQL enum type.
|
||||
|
||||
.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices
|
||||
|
||||
For example the following ``Model`` and ``DjangoObjectType``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class PetModel(models.Model):
|
||||
kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))
|
||||
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
|
||||
Results in the following GraphQL schema definition:
|
||||
|
||||
.. code::
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: PetModelKind!
|
||||
}
|
||||
|
||||
enum PetModelKind {
|
||||
CAT
|
||||
DOG
|
||||
}
|
||||
|
||||
You can disable this automatic conversion by setting
|
||||
``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType``
|
||||
``Meta`` class.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = False
|
||||
|
||||
.. code::
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: String!
|
||||
}
|
||||
|
||||
You can also set ``convert_choices_to_enum`` to a list of fields that should be
|
||||
automatically converted into enums:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = ['kind']
|
||||
|
||||
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
|
||||
``False``.
|
||||
|
||||
|
||||
Related models
|
||||
--------------
|
||||
|
||||
|
@ -113,7 +193,7 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C
|
|||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
only_fields = ('category',)
|
||||
fields = ('category',)
|
||||
|
||||
Then all query-able related models must be defined as DjangoObjectType subclass,
|
||||
or they will fail to show if you are trying to query those relation fields. You only
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
sphinx
|
||||
Sphinx==1.5.3
|
||||
sphinx-autobuild==0.7.1
|
||||
# Docs template
|
||||
http://graphene-python.org/sphinx_graphene_theme.zip
|
||||
|
|
|
@ -30,7 +30,7 @@ Default: ``None``
|
|||
|
||||
|
||||
``SCHEMA_OUTPUT``
|
||||
----------
|
||||
-----------------
|
||||
|
||||
The name of the file where the GraphQL schema output will go.
|
||||
|
||||
|
@ -44,7 +44,7 @@ Default: ``schema.json``
|
|||
|
||||
|
||||
``SCHEMA_INDENT``
|
||||
----------
|
||||
-----------------
|
||||
|
||||
The indentation level of the schema output.
|
||||
|
||||
|
@ -58,7 +58,7 @@ Default: ``2``
|
|||
|
||||
|
||||
``MIDDLEWARE``
|
||||
----------
|
||||
--------------
|
||||
|
||||
A tuple of middleware that will be executed for each GraphQL query.
|
||||
|
||||
|
@ -76,7 +76,7 @@ Default: ``()``
|
|||
|
||||
|
||||
``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST``
|
||||
----------
|
||||
------------------------------------------
|
||||
|
||||
Enforces relay queries to have the ``first`` or ``last`` argument.
|
||||
|
||||
|
@ -90,7 +90,7 @@ Default: ``False``
|
|||
|
||||
|
||||
``RELAY_CONNECTION_MAX_LIMIT``
|
||||
----------
|
||||
------------------------------
|
||||
|
||||
The maximum size of objects that can be requested through a relay connection.
|
||||
|
||||
|
@ -101,3 +101,42 @@ Default: ``100``
|
|||
GRAPHENE = {
|
||||
'RELAY_CONNECTION_MAX_LIMIT': 100,
|
||||
}
|
||||
|
||||
|
||||
``CAMELCASE_ERRORS``
|
||||
------------------------------------
|
||||
|
||||
When set to ``True`` field names in the ``errors`` object will be camel case.
|
||||
By default they will be snake case.
|
||||
|
||||
Default: ``False``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'CAMELCASE_ERRORS': False,
|
||||
}
|
||||
|
||||
# result = schema.execute(...)
|
||||
print(result.errors)
|
||||
# [
|
||||
# {
|
||||
# 'field': 'test_field',
|
||||
# 'messages': ['This field is required.'],
|
||||
# }
|
||||
# ]
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'CAMELCASE_ERRORS': True,
|
||||
}
|
||||
|
||||
# result = schema.execute(...)
|
||||
print(result.errors)
|
||||
# [
|
||||
# {
|
||||
# 'field': 'testField',
|
||||
# 'messages': ['This field is required.'],
|
||||
# }
|
||||
# ]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
graphene
|
||||
graphene-django
|
||||
graphql-core>=2.1rc1
|
||||
django==2.1.6
|
||||
django==2.2.4
|
||||
|
|
|
@ -11,7 +11,9 @@ class Category(models.Model):
|
|||
class Ingredient(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
notes = models.TextField(null=True, blank=True)
|
||||
category = models.ForeignKey(Category, related_name="ingredients")
|
||||
category = models.ForeignKey(
|
||||
Category, related_name="ingredients", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -10,8 +10,10 @@ class Recipe(models.Model):
|
|||
|
||||
|
||||
class RecipeIngredient(models.Model):
|
||||
recipe = models.ForeignKey(Recipe, related_name="amounts")
|
||||
ingredient = models.ForeignKey(Ingredient, related_name="used_by")
|
||||
recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
|
||||
ingredient = models.ForeignKey(
|
||||
Ingredient, related_name="used_by", on_delete=models.CASCADE
|
||||
)
|
||||
amount = models.FloatField()
|
||||
unit = models.CharField(
|
||||
max_length=20,
|
||||
|
|
|
@ -43,13 +43,12 @@ INSTALLED_APPS = [
|
|||
"cookbook.recipes.apps.RecipesConfig",
|
||||
]
|
||||
|
||||
MIDDLEWARE_CLASSES = [
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.auth.middleware.SessionAuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
graphene
|
||||
graphene-django
|
||||
graphql-core>=2.1rc1
|
||||
django==1.11.20
|
||||
django==2.2.4
|
||||
django-filter>=2
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from .types import DjangoObjectType
|
||||
from .fields import DjangoConnectionField
|
||||
|
||||
__version__ = "2.3.0"
|
||||
__version__ = "2.5.0"
|
||||
|
||||
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
|
||||
|
|
|
@ -52,13 +52,15 @@ def get_choices(choices):
|
|||
yield name, value, description
|
||||
|
||||
|
||||
def convert_django_field_with_choices(field, registry=None):
|
||||
def convert_django_field_with_choices(
|
||||
field, registry=None, convert_choices_to_enum=True
|
||||
):
|
||||
if registry is not None:
|
||||
converted = registry.get_converted_field(field)
|
||||
if converted:
|
||||
return converted
|
||||
choices = getattr(field, "choices", None)
|
||||
if choices:
|
||||
if choices and convert_choices_to_enum:
|
||||
meta = field.model._meta
|
||||
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
|
||||
choices = list(get_choices(choices))
|
||||
|
@ -71,7 +73,8 @@ def convert_django_field_with_choices(field, registry=None):
|
|||
return named_choices_descriptions[self.name]
|
||||
|
||||
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
||||
converted = enum(description=field.help_text, required=not field.null)
|
||||
required = not (field.blank or field.null)
|
||||
converted = enum(description=field.help_text, required=required)
|
||||
else:
|
||||
converted = convert_django_field(field, registry)
|
||||
if registry is not None:
|
||||
|
@ -192,11 +195,17 @@ def convert_field_to_list_or_connection(field, registry=None):
|
|||
if _type._meta.filter_fields or _type._meta.filterset_class:
|
||||
from .filter.fields import DjangoFilterConnectionField
|
||||
|
||||
return DjangoFilterConnectionField(_type, description=description)
|
||||
return DjangoFilterConnectionField(
|
||||
_type, required=True, description=description
|
||||
)
|
||||
|
||||
return DjangoConnectionField(_type, description=description)
|
||||
return DjangoConnectionField(_type, required=True, description=description)
|
||||
|
||||
return DjangoListField(_type, description=description)
|
||||
return DjangoListField(
|
||||
_type,
|
||||
required=True, # A Set is always returned, never None.
|
||||
description=description,
|
||||
)
|
||||
|
||||
return Dynamic(dynamic_type)
|
||||
|
||||
|
|
|
@ -16,14 +16,18 @@ class DjangoDebugContext(object):
|
|||
def get_debug_promise(self):
|
||||
if not self.debug_promise:
|
||||
self.debug_promise = Promise.all(self.promises)
|
||||
self.promises = []
|
||||
return self.debug_promise.then(self.on_resolve_all_promises)
|
||||
|
||||
def on_resolve_all_promises(self, values):
|
||||
if self.promises:
|
||||
self.debug_promise = None
|
||||
return self.get_debug_promise()
|
||||
self.disable_instrumentation()
|
||||
return self.object
|
||||
|
||||
def add_promise(self, promise):
|
||||
if self.debug_promise and not self.debug_promise.is_fulfilled:
|
||||
if self.debug_promise:
|
||||
self.promises.append(promise)
|
||||
|
||||
def enable_instrumentation(self):
|
||||
|
|
|
@ -60,6 +60,73 @@ def test_should_query_field():
|
|||
assert result.data == expected
|
||||
|
||||
|
||||
def test_should_query_nested_field():
|
||||
r1 = Reporter(last_name="ABA")
|
||||
r1.save()
|
||||
r2 = Reporter(last_name="Griffin")
|
||||
r2.save()
|
||||
r2.pets.add(r1)
|
||||
r1.pets.add(r2)
|
||||
|
||||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (Node,)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
reporter = graphene.Field(ReporterType)
|
||||
debug = graphene.Field(DjangoDebug, name="__debug")
|
||||
|
||||
def resolve_reporter(self, info, **args):
|
||||
return Reporter.objects.first()
|
||||
|
||||
query = """
|
||||
query ReporterQuery {
|
||||
reporter {
|
||||
lastName
|
||||
pets { edges { node {
|
||||
lastName
|
||||
pets { edges { node { lastName } } }
|
||||
} } }
|
||||
}
|
||||
__debug {
|
||||
sql {
|
||||
rawSql
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
expected = {
|
||||
"reporter": {
|
||||
"lastName": "ABA",
|
||||
"pets": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"lastName": "Griffin",
|
||||
"pets": {"edges": [{"node": {"lastName": "ABA"}}]},
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
schema = graphene.Schema(query=Query)
|
||||
result = schema.execute(
|
||||
query, context_value=context(), middleware=[DjangoDebugMiddleware()]
|
||||
)
|
||||
assert not result.errors
|
||||
query = str(Reporter.objects.order_by("pk")[:1].query)
|
||||
assert result.data["__debug"]["sql"][0]["rawSql"] == query
|
||||
assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"]
|
||||
assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"]
|
||||
assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"]
|
||||
assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"]
|
||||
assert len(result.data["__debug"]["sql"]) == 5
|
||||
|
||||
assert result.data["reporter"] == expected["reporter"]
|
||||
|
||||
|
||||
def test_should_query_list():
|
||||
r1 = Reporter(last_name="ABA")
|
||||
r1.save()
|
||||
|
|
|
@ -15,7 +15,8 @@ from .utils import maybe_queryset
|
|||
|
||||
class DjangoListField(Field):
|
||||
def __init__(self, _type, *args, **kwargs):
|
||||
super(DjangoListField, self).__init__(List(_type), *args, **kwargs)
|
||||
# Django would never return a Set of None vvvvvvv
|
||||
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
|
@ -100,7 +101,7 @@ class DjangoConnectionField(ConnectionField):
|
|||
iterable = default_manager
|
||||
iterable = maybe_queryset(iterable)
|
||||
if isinstance(iterable, QuerySet):
|
||||
if iterable is not default_manager:
|
||||
if iterable.model.objects is not default_manager:
|
||||
default_queryset = maybe_queryset(default_manager)
|
||||
iterable = cls.merge_querysets(default_queryset, iterable)
|
||||
_len = iterable.count()
|
||||
|
|
|
@ -111,7 +111,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
|||
return partial(
|
||||
self.connection_resolver,
|
||||
parent_resolver,
|
||||
self.type,
|
||||
self.connection_type,
|
||||
self.get_manager(),
|
||||
self.max_limit,
|
||||
self.enforce_first_or_last,
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
from datetime import datetime
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from django.db.models import TextField, Value
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String
|
||||
from graphene.relay import Node
|
||||
|
||||
from graphene_django import 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
|
||||
|
||||
# for annotation test
|
||||
from django.db.models import TextField, Value
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
pytestmark = []
|
||||
|
||||
if DJANGO_FILTER_INSTALLED:
|
||||
|
@ -184,7 +182,7 @@ def test_filter_shortcut_filterset_context():
|
|||
}
|
||||
"""
|
||||
schema = Schema(query=Query)
|
||||
result = schema.execute(query, context_value=context())
|
||||
result = schema.execute(query, context=context())
|
||||
assert not result.errors
|
||||
|
||||
assert len(result.data["contextArticles"]["edges"]) == 1
|
||||
|
@ -322,12 +320,14 @@ def test_filter_filterset_related_results():
|
|||
pub_date=datetime.now(),
|
||||
pub_date_time=datetime.now(),
|
||||
reporter=r1,
|
||||
editor=r1,
|
||||
)
|
||||
Article.objects.create(
|
||||
headline="a2",
|
||||
pub_date=datetime.now(),
|
||||
pub_date_time=datetime.now(),
|
||||
reporter=r2,
|
||||
editor=r2,
|
||||
)
|
||||
|
||||
query = """
|
||||
|
@ -451,7 +451,7 @@ def test_global_id_multiple_field_explicit_reverse():
|
|||
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
|
||||
|
||||
|
||||
def test_filter_filterset_related_results():
|
||||
def test_filter_filterset_related_results_with_filter():
|
||||
class ReporterFilterNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
|
@ -461,15 +461,15 @@ def test_filter_filterset_related_results():
|
|||
class Query(ObjectType):
|
||||
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
|
||||
|
||||
r1 = Reporter.objects.create(
|
||||
Reporter.objects.create(
|
||||
first_name="A test user", last_name="Last Name", email="test1@test.com"
|
||||
)
|
||||
r2 = Reporter.objects.create(
|
||||
Reporter.objects.create(
|
||||
first_name="Other test user",
|
||||
last_name="Other Last Name",
|
||||
email="test2@test.com",
|
||||
)
|
||||
r3 = Reporter.objects.create(
|
||||
Reporter.objects.create(
|
||||
first_name="Random", last_name="RandomLast", email="random@test.com"
|
||||
)
|
||||
|
||||
|
@ -637,7 +637,7 @@ def test_should_query_filter_node_double_limit_raises():
|
|||
Reporter.objects.create(
|
||||
first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2
|
||||
)
|
||||
r = Reporter.objects.create(
|
||||
Reporter.objects.create(
|
||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||
)
|
||||
|
||||
|
@ -683,7 +683,7 @@ def test_order_by_is_perserved():
|
|||
return reporters
|
||||
|
||||
Reporter.objects.create(first_name="b")
|
||||
r = Reporter.objects.create(first_name="a")
|
||||
Reporter.objects.create(first_name="a")
|
||||
|
||||
schema = Schema(query=Query)
|
||||
query = """
|
||||
|
@ -768,36 +768,154 @@ def test_annotation_is_perserved():
|
|||
assert result.data == expected
|
||||
|
||||
|
||||
def test_filter_with_union():
|
||||
class ReporterType(DjangoObjectType):
|
||||
def test_integer_field_filter_type():
|
||||
class PetType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
model = Pet
|
||||
interfaces = (Node,)
|
||||
filter_fields = ("first_name",)
|
||||
filter_fields = {"age": ["exact"]}
|
||||
fields = ("age",)
|
||||
|
||||
class Query(ObjectType):
|
||||
all_reporters = DjangoFilterConnectionField(ReporterType)
|
||||
|
||||
@classmethod
|
||||
def resolve_all_reporters(cls, root, info, **kwargs):
|
||||
ret = Reporter.objects.none() | Reporter.objects.filter(first_name="John")
|
||||
|
||||
Reporter.objects.create(first_name="John", last_name="Doe")
|
||||
pets = DjangoFilterConnectionField(PetType)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
query = """
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
hasNextPage: Boolean!
|
||||
hasPreviousPage: Boolean!
|
||||
startCursor: String
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
type PetType implements Node {
|
||||
age: Int!
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type PetTypeConnection {
|
||||
pageInfo: PageInfo!
|
||||
edges: [PetTypeEdge]!
|
||||
}
|
||||
|
||||
type PetTypeEdge {
|
||||
node: PetType
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_filter_filterset_based_on_mixin():
|
||||
class ArticleFilterMixin(FilterSet):
|
||||
@classmethod
|
||||
def get_filters(cls):
|
||||
filters = super(FilterSet, cls).get_filters()
|
||||
filters.update(
|
||||
{
|
||||
"viewer__email__in": django_filters.CharFilter(
|
||||
method="filter_email_in", field_name="reporter__email__in"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return filters
|
||||
|
||||
def filter_email_in(cls, queryset, name, value):
|
||||
return queryset.filter(**{name: [value]})
|
||||
|
||||
class NewArticleFilter(ArticleFilterMixin, ArticleFilter):
|
||||
pass
|
||||
|
||||
class NewReporterNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (Node,)
|
||||
|
||||
class NewArticleFilterNode(DjangoObjectType):
|
||||
viewer = Field(NewReporterNode)
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
interfaces = (Node,)
|
||||
filterset_class = NewArticleFilter
|
||||
|
||||
def resolve_viewer(self, info):
|
||||
return self.reporter
|
||||
|
||||
class Query(ObjectType):
|
||||
all_articles = DjangoFilterConnectionField(NewArticleFilterNode)
|
||||
|
||||
reporter_1 = Reporter.objects.create(
|
||||
first_name="John", last_name="Doe", email="john@doe.com"
|
||||
)
|
||||
|
||||
article_1 = Article.objects.create(
|
||||
headline="Hello",
|
||||
reporter=reporter_1,
|
||||
editor=reporter_1,
|
||||
pub_date=datetime.now(),
|
||||
pub_date_time=datetime.now(),
|
||||
)
|
||||
|
||||
reporter_2 = Reporter.objects.create(
|
||||
first_name="Adam", last_name="Doe", email="adam@doe.com"
|
||||
)
|
||||
|
||||
article_2 = Article.objects.create(
|
||||
headline="Good Bye",
|
||||
reporter=reporter_2,
|
||||
editor=reporter_2,
|
||||
pub_date=datetime.now(),
|
||||
pub_date_time=datetime.now(),
|
||||
)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
query = (
|
||||
"""
|
||||
query NodeFilteringQuery {
|
||||
allReporters(firstName: "abc") {
|
||||
allArticles(viewer_Email_In: "%s") {
|
||||
edges {
|
||||
node {
|
||||
firstName
|
||||
headline
|
||||
viewer {
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
expected = {"allReporters": {"edges": []}}
|
||||
% reporter_1.email
|
||||
)
|
||||
|
||||
expected = {
|
||||
"allArticles": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"headline": article_1.headline,
|
||||
"viewer": {"email": reporter_1.email},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
result = schema.execute(query)
|
||||
|
||||
|
|
|
@ -11,8 +11,29 @@ def get_filtering_args_from_filterset(filterset_class, type):
|
|||
from ..forms.converter import convert_form_field
|
||||
|
||||
args = {}
|
||||
model = filterset_class._meta.model
|
||||
for name, filter_field in six.iteritems(filterset_class.base_filters):
|
||||
field_type = convert_form_field(filter_field.field).Argument()
|
||||
form_field = None
|
||||
|
||||
if name in filterset_class.declared_filters:
|
||||
form_field = filter_field.field
|
||||
else:
|
||||
field_name = name.split("__", 1)[0]
|
||||
|
||||
if hasattr(model, field_name):
|
||||
model_field = model._meta.get_field(field_name)
|
||||
|
||||
if hasattr(model_field, "formfield"):
|
||||
form_field = model_field.formfield(
|
||||
required=filter_field.extra.get("required", False)
|
||||
)
|
||||
|
||||
# Fallback to field defined on filter if we can't get it from the
|
||||
# model field
|
||||
if not form_field:
|
||||
form_field = filter_field.field
|
||||
|
||||
field_type = convert_form_field(form_field).Argument()
|
||||
field_type.description = filter_field.label
|
||||
args[name] = field_type
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@ from graphene.types.mutation import MutationOptions
|
|||
from graphene.types.utils import yank_fields_from_attrs
|
||||
from graphene_django.registry import get_global_registry
|
||||
|
||||
from .converter import convert_form_field
|
||||
from ..types import ErrorType
|
||||
from .converter import convert_form_field
|
||||
|
||||
|
||||
def fields_for_form(form, only_fields, exclude_fields):
|
||||
|
@ -45,10 +45,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
|
|||
if form.is_valid():
|
||||
return cls.perform_mutate(form, info)
|
||||
else:
|
||||
errors = [
|
||||
ErrorType(field=key, messages=value)
|
||||
for key, value in form.errors.items()
|
||||
]
|
||||
errors = ErrorType.from_errors(form.errors)
|
||||
|
||||
return cls(errors=errors)
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@ from django import forms
|
|||
from django.test import TestCase
|
||||
from py.test import raises
|
||||
|
||||
from graphene_django.tests.models import Pet, Film, FilmDetails
|
||||
from graphene_django.tests.models import Film, FilmDetails, Pet
|
||||
|
||||
from ...settings import graphene_settings
|
||||
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
|
||||
|
||||
|
||||
|
@ -41,6 +43,22 @@ def test_has_input_fields():
|
|||
assert "text" in MyMutation.Input._meta.fields
|
||||
|
||||
|
||||
def test_mutation_error_camelcased():
|
||||
class ExtraPetForm(PetForm):
|
||||
test_field = forms.CharField(required=True)
|
||||
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = ExtraPetForm
|
||||
|
||||
result = PetMutation.mutate_and_get_payload(None, None)
|
||||
assert {f.field for f in result.errors} == {"name", "age", "test_field"}
|
||||
graphene_settings.CAMELCASE_ERRORS = True
|
||||
result = PetMutation.mutate_and_get_payload(None, None)
|
||||
assert {f.field for f in result.errors} == {"name", "age", "testField"}
|
||||
graphene_settings.CAMELCASE_ERRORS = False
|
||||
|
||||
|
||||
class ModelFormMutationTests(TestCase):
|
||||
def test_default_meta_fields(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import graphene
|
||||
|
||||
|
||||
class ErrorType(graphene.ObjectType):
|
||||
field = graphene.String()
|
||||
messages = graphene.List(graphene.String)
|
||||
from ..types import ErrorType # noqa Import ErrorType for backwards compatability
|
||||
|
|
|
@ -3,13 +3,13 @@ from collections import OrderedDict
|
|||
from django.shortcuts import get_object_or_404
|
||||
|
||||
import graphene
|
||||
from graphene.relay.mutation import ClientIDMutation
|
||||
from graphene.types import Field, InputField
|
||||
from graphene.types.mutation import MutationOptions
|
||||
from graphene.relay.mutation import ClientIDMutation
|
||||
from graphene.types.objecttype import yank_fields_from_attrs
|
||||
|
||||
from .serializer_converter import convert_serializer_field
|
||||
from ..types import ErrorType
|
||||
from .serializer_converter import convert_serializer_field
|
||||
|
||||
|
||||
class SerializerMutationOptions(MutationOptions):
|
||||
|
@ -52,7 +52,7 @@ class SerializerMutation(ClientIDMutation):
|
|||
lookup_field=None,
|
||||
serializer_class=None,
|
||||
model_class=None,
|
||||
model_operations=["create", "update"],
|
||||
model_operations=("create", "update"),
|
||||
only_fields=(),
|
||||
exclude_fields=(),
|
||||
**options
|
||||
|
@ -127,10 +127,7 @@ class SerializerMutation(ClientIDMutation):
|
|||
if serializer.is_valid():
|
||||
return cls.perform_mutate(serializer, info)
|
||||
else:
|
||||
errors = [
|
||||
ErrorType(field=key, messages=value)
|
||||
for key, value in serializer.errors.items()
|
||||
]
|
||||
errors = ErrorType.from_errors(serializer.errors)
|
||||
|
||||
return cls(errors=errors)
|
||||
|
||||
|
|
|
@ -57,18 +57,27 @@ def convert_serializer_field(field, is_input=True):
|
|||
|
||||
|
||||
def convert_serializer_to_input_type(serializer_class):
|
||||
cached_type = convert_serializer_to_input_type.cache.get(
|
||||
serializer_class.__name__, None
|
||||
)
|
||||
if cached_type:
|
||||
return cached_type
|
||||
serializer = serializer_class()
|
||||
|
||||
items = {
|
||||
name: convert_serializer_field(field)
|
||||
for name, field in serializer.fields.items()
|
||||
}
|
||||
|
||||
return type(
|
||||
ret_type = type(
|
||||
"{}Input".format(serializer.__class__.__name__),
|
||||
(graphene.InputObjectType,),
|
||||
items,
|
||||
)
|
||||
convert_serializer_to_input_type.cache[serializer_class.__name__] = ret_type
|
||||
return ret_type
|
||||
|
||||
|
||||
convert_serializer_to_input_type.cache = {}
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.Field)
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import graphene
|
||||
import pytest
|
||||
from django.db import models
|
||||
from graphene import Schema
|
||||
from rest_framework import serializers
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
class MyFakeChildModel(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class MyFakeParentModel(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
child1 = models.OneToOneField(
|
||||
MyFakeChildModel, related_name="parent1", on_delete=models.CASCADE
|
||||
)
|
||||
child2 = models.OneToOneField(
|
||||
MyFakeChildModel, related_name="parent2", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
|
||||
class ParentType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = MyFakeParentModel
|
||||
interfaces = (graphene.relay.Node,)
|
||||
|
||||
|
||||
class ChildType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = MyFakeChildModel
|
||||
interfaces = (graphene.relay.Node,)
|
||||
|
||||
|
||||
class MyModelChildSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = MyFakeChildModel
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class MyModelParentSerializer(serializers.ModelSerializer):
|
||||
child1 = MyModelChildSerializer()
|
||||
child2 = MyModelChildSerializer()
|
||||
|
||||
class Meta:
|
||||
model = MyFakeParentModel
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class MyParentModelMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = MyModelParentSerializer
|
||||
|
||||
|
||||
class Mutation(graphene.ObjectType):
|
||||
createParentWithChild = MyParentModelMutation.Field()
|
||||
|
||||
|
||||
def test_create_schema():
|
||||
schema = Schema(mutation=Mutation, types=[ParentType, ChildType])
|
||||
assert schema
|
|
@ -1,11 +1,12 @@
|
|||
import datetime
|
||||
|
||||
from py.test import mark, raises
|
||||
from rest_framework import serializers
|
||||
|
||||
from graphene import Field, ResolveInfo
|
||||
from graphene.types.inputobjecttype import InputObjectType
|
||||
from py.test import raises
|
||||
from py.test import mark
|
||||
from rest_framework import serializers
|
||||
|
||||
from ...settings import graphene_settings
|
||||
from ...types import DjangoObjectType
|
||||
from ..models import MyFakeModel, MyFakeModelWithPassword
|
||||
from ..mutation import SerializerMutation
|
||||
|
@ -213,6 +214,13 @@ def test_model_mutate_and_get_payload_error():
|
|||
assert len(result.errors) > 0
|
||||
|
||||
|
||||
def test_mutation_error_camelcased():
|
||||
graphene_settings.CAMELCASE_ERRORS = True
|
||||
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
|
||||
assert result.errors[0].field == "coolName"
|
||||
graphene_settings.CAMELCASE_ERRORS = False
|
||||
|
||||
|
||||
def test_invalid_serializer_operations():
|
||||
with raises(Exception) as exc:
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ DEFAULTS = {
|
|||
"RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False,
|
||||
# Max items returned in ConnectionFields / FilterConnectionFields
|
||||
"RELAY_CONNECTION_MAX_LIMIT": 100,
|
||||
"CAMELCASE_ERRORS": False,
|
||||
}
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
|
@ -38,7 +38,7 @@ class Reporter(models.Model):
|
|||
last_name = models.CharField(max_length=30)
|
||||
email = models.EmailField()
|
||||
pets = models.ManyToManyField("self")
|
||||
a_choice = models.CharField(max_length=30, choices=CHOICES)
|
||||
a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True)
|
||||
objects = models.Manager()
|
||||
doe_objects = DoeReporterManager()
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from graphene import NonNull
|
||||
from py.test import raises
|
||||
|
||||
import graphene
|
||||
|
@ -196,6 +197,23 @@ def test_field_with_choices_collision():
|
|||
convert_django_field_with_choices(field)
|
||||
|
||||
|
||||
def test_field_with_choices_convert_enum_false():
|
||||
field = models.CharField(
|
||||
help_text="Language", choices=(("es", "Spanish"), ("en", "English"))
|
||||
)
|
||||
|
||||
class TranslatedModel(models.Model):
|
||||
language = field
|
||||
|
||||
class Meta:
|
||||
app_label = "test"
|
||||
|
||||
graphene_type = convert_django_field_with_choices(
|
||||
field, convert_choices_to_enum=False
|
||||
)
|
||||
assert isinstance(graphene_type, graphene.String)
|
||||
|
||||
|
||||
def test_should_float_convert_float():
|
||||
assert_conversion(models.FloatField, graphene.Float)
|
||||
|
||||
|
@ -217,8 +235,12 @@ def test_should_manytomany_convert_connectionorlist_list():
|
|||
assert isinstance(graphene_field, graphene.Dynamic)
|
||||
dynamic_field = graphene_field.get_type()
|
||||
assert isinstance(dynamic_field, graphene.Field)
|
||||
assert isinstance(dynamic_field.type, graphene.List)
|
||||
assert dynamic_field.type.of_type == A
|
||||
# A NonNull List of NonNull A ([A!]!)
|
||||
# https://github.com/graphql-python/graphene-django/issues/448
|
||||
assert isinstance(dynamic_field.type, NonNull)
|
||||
assert isinstance(dynamic_field.type.of_type, graphene.List)
|
||||
assert isinstance(dynamic_field.type.of_type.of_type, NonNull)
|
||||
assert dynamic_field.type.of_type.of_type.of_type == A
|
||||
|
||||
|
||||
def test_should_manytomany_convert_connectionorlist_connection():
|
||||
|
@ -233,7 +255,7 @@ def test_should_manytomany_convert_connectionorlist_connection():
|
|||
assert isinstance(graphene_field, graphene.Dynamic)
|
||||
dynamic_field = graphene_field.get_type()
|
||||
assert isinstance(dynamic_field, ConnectionField)
|
||||
assert dynamic_field.type == A._meta.connection
|
||||
assert dynamic_field.type.of_type == A._meta.connection
|
||||
|
||||
|
||||
def test_should_manytoone_convert_connectionorlist():
|
||||
|
@ -245,8 +267,11 @@ def test_should_manytoone_convert_connectionorlist():
|
|||
assert isinstance(graphene_field, graphene.Dynamic)
|
||||
dynamic_field = graphene_field.get_type()
|
||||
assert isinstance(dynamic_field, graphene.Field)
|
||||
assert isinstance(dynamic_field.type, graphene.List)
|
||||
assert dynamic_field.type.of_type == A
|
||||
# a NonNull List of NonNull A ([A!]!)
|
||||
assert isinstance(dynamic_field.type, NonNull)
|
||||
assert isinstance(dynamic_field.type.of_type, graphene.List)
|
||||
assert isinstance(dynamic_field.type.of_type.of_type, NonNull)
|
||||
assert dynamic_field.type.of_type.of_type.of_type == A
|
||||
|
||||
|
||||
def test_should_onetoone_reverse_convert_model():
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import base64
|
||||
import datetime
|
||||
|
||||
import graphene
|
||||
import pytest
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from graphene.relay import Node
|
||||
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
|
||||
|
||||
from ..utils import DJANGO_FILTER_INSTALLED
|
||||
from ..compat import MissingType, JSONField
|
||||
from ..compat import JSONField, MissingType
|
||||
from ..fields import DjangoConnectionField
|
||||
from ..types import DjangoObjectType
|
||||
from ..settings import graphene_settings
|
||||
from .models import Article, CNNReporter, Reporter, Film, FilmDetails
|
||||
from ..types import DjangoObjectType
|
||||
from ..utils import DJANGO_FILTER_INSTALLED
|
||||
from .models import Article, CNNReporter, Film, FilmDetails, Reporter
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
@ -28,7 +27,7 @@ def test_should_query_only_fields():
|
|||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
only_fields = ("articles",)
|
||||
fields = ("articles",)
|
||||
|
||||
schema = graphene.Schema(query=ReporterType)
|
||||
query = """
|
||||
|
@ -44,7 +43,7 @@ def test_should_query_simplelazy_objects():
|
|||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
only_fields = ("id",)
|
||||
fields = ("id",)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
reporter = graphene.Field(ReporterType)
|
||||
|
@ -289,7 +288,7 @@ def test_should_query_connectionfields():
|
|||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (Node,)
|
||||
only_fields = ("articles",)
|
||||
fields = ("articles",)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
all_reporters = DjangoConnectionField(ReporterType)
|
||||
|
@ -329,7 +328,7 @@ def test_should_keep_annotations():
|
|||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (Node,)
|
||||
only_fields = ("articles",)
|
||||
fields = ("articles",)
|
||||
|
||||
class ArticleType(DjangoObjectType):
|
||||
class Meta:
|
||||
|
@ -1236,3 +1235,54 @@ def test_should_resolve_get_queryset_connectionfields():
|
|||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
assert result.data == expected
|
||||
|
||||
|
||||
def test_should_preserve_prefetch_related(django_assert_num_queries):
|
||||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (graphene.relay.Node,)
|
||||
|
||||
class FilmType(DjangoObjectType):
|
||||
reporters = DjangoConnectionField(ReporterType)
|
||||
|
||||
class Meta:
|
||||
model = Film
|
||||
interfaces = (graphene.relay.Node,)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
films = DjangoConnectionField(FilmType)
|
||||
|
||||
def resolve_films(root, info, **kwargs):
|
||||
qs = Film.objects.prefetch_related("reporters")
|
||||
return qs
|
||||
|
||||
r1 = Reporter.objects.create(first_name="Dave", last_name="Smith")
|
||||
r2 = Reporter.objects.create(first_name="Jane", last_name="Doe")
|
||||
|
||||
f1 = Film.objects.create()
|
||||
f1.reporters.set([r1, r2])
|
||||
f2 = Film.objects.create()
|
||||
f2.reporters.set([r2])
|
||||
|
||||
query = """
|
||||
query {
|
||||
films {
|
||||
edges {
|
||||
node {
|
||||
reporters {
|
||||
edges {
|
||||
node {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
schema = graphene.Schema(query=Query)
|
||||
with django_assert_num_queries(3) as captured:
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
|
|
|
@ -48,6 +48,6 @@ def test_should_map_only_few_fields():
|
|||
class Reporter2(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
only_fields = ("id", "email")
|
||||
fields = ("id", "email")
|
||||
|
||||
assert list(Reporter2._meta.fields.keys()) == ["id", "email"]
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
from collections import OrderedDict, defaultdict
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from django.db import models
|
||||
from mock import patch
|
||||
|
||||
from graphene import Interface, ObjectType, Schema, Connection, String
|
||||
from graphene import Connection, Field, Interface, ObjectType, Schema, String
|
||||
from graphene.relay import Node
|
||||
|
||||
from .. import registry
|
||||
|
@ -165,10 +170,10 @@ type Reporter {
|
|||
firstName: String!
|
||||
lastName: String!
|
||||
email: String!
|
||||
pets: [Reporter]
|
||||
aChoice: ReporterAChoice!
|
||||
pets: [Reporter!]!
|
||||
aChoice: ReporterAChoice
|
||||
reporterType: ReporterReporterType
|
||||
articles(before: String, after: String, first: Int, last: Int): ArticleConnection
|
||||
articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
|
||||
}
|
||||
|
||||
enum ReporterAChoice {
|
||||
|
@ -206,21 +211,216 @@ def with_local_registry(func):
|
|||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_only_fields():
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
only_fields = ("id", "email", "films")
|
||||
with pytest.warns(PendingDeprecationWarning):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
only_fields = ("id", "email", "films")
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert fields == ["id", "email", "films"]
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude_fields():
|
||||
def test_django_objecttype_fields():
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude_fields = "email"
|
||||
fields = ("id", "email", "films")
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert fields == ["id", "email", "films"]
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_only_fields_and_fields():
|
||||
with pytest.raises(Exception):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
only_fields = ("id", "email", "films")
|
||||
fields = ("id", "email", "films")
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_all_fields():
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
fields = "__all__"
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert len(fields) == len(ReporterModel._meta.get_fields())
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude_fields():
|
||||
with pytest.warns(PendingDeprecationWarning):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude_fields = ["email"]
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert "email" not in fields
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude():
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude = ["email"]
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert "email" not in fields
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude_fields_and_exclude():
|
||||
with pytest.raises(Exception):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude = ["email"]
|
||||
exclude_fields = ["email"]
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude_and_only():
|
||||
with pytest.raises(AssertionError):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude = ["email"]
|
||||
fields = ["id"]
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_fields_exclude_type_checking():
|
||||
with pytest.raises(TypeError):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
fields = "foo"
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
|
||||
class Reporter2(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
fields = "foo"
|
||||
|
||||
|
||||
class TestDjangoObjectType:
|
||||
@pytest.fixture
|
||||
def PetModel(self):
|
||||
class PetModel(models.Model):
|
||||
kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog")))
|
||||
cuteness = models.IntegerField(
|
||||
choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!"))
|
||||
)
|
||||
|
||||
yield PetModel
|
||||
|
||||
# Clear Django model cache so we don't get warnings when creating the
|
||||
# model multiple times
|
||||
PetModel._meta.apps.all_models = defaultdict(OrderedDict)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_false(self, PetModel):
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = False
|
||||
|
||||
class Query(ObjectType):
|
||||
pet = Field(Pet)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: String!
|
||||
cuteness: Int!
|
||||
}
|
||||
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = ["kind"]
|
||||
|
||||
class Query(ObjectType):
|
||||
pet = Field(Pet)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: PetModelKind!
|
||||
cuteness: Int!
|
||||
}
|
||||
|
||||
enum PetModelKind {
|
||||
CAT
|
||||
DOG
|
||||
}
|
||||
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = []
|
||||
|
||||
class Query(ObjectType):
|
||||
pet = Field(Pet)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: String!
|
||||
cuteness: Int!
|
||||
}
|
||||
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from ..utils import get_model_fields
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from ..utils import camelize, get_model_fields
|
||||
from .models import Film, Reporter
|
||||
|
||||
|
||||
|
@ -10,3 +12,21 @@ def test_get_model_fields_no_duplication():
|
|||
film_fields = get_model_fields(Film)
|
||||
film_name_set = set([field[0] for field in film_fields])
|
||||
assert len(film_fields) == len(film_name_set)
|
||||
|
||||
|
||||
def test_camelize():
|
||||
assert camelize({}) == {}
|
||||
assert camelize("value_a") == "value_a"
|
||||
assert camelize({"value_a": "value_b"}) == {"valueA": "value_b"}
|
||||
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
|
||||
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
|
||||
assert camelize({"nested_field": {"value_a": ["error"], "value_b": ["error"]}}) == {
|
||||
"nestedField": {"valueA": ["error"], "valueB": ["error"]}
|
||||
}
|
||||
assert camelize({"value_a": gettext_lazy("value_b")}) == {"valueA": "value_b"}
|
||||
assert camelize({"value_a": [gettext_lazy("value_b")]}) == {"valueA": ["value_b"]}
|
||||
assert camelize(gettext_lazy("value_a")) == "value_a"
|
||||
assert camelize({gettext_lazy("value_a"): gettext_lazy("value_b")}) == {
|
||||
"valueA": "value_b"
|
||||
}
|
||||
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import six
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
import six
|
||||
from django.db.models import Model
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
|
||||
import graphene
|
||||
from graphene import Field
|
||||
from graphene.relay import Connection, Node
|
||||
|
@ -11,14 +13,24 @@ from graphene.types.utils import yank_fields_from_attrs
|
|||
|
||||
from .converter import convert_django_field_with_choices
|
||||
from .registry import Registry, get_global_registry
|
||||
from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model
|
||||
|
||||
from .settings import graphene_settings
|
||||
from .utils import (
|
||||
DJANGO_FILTER_INSTALLED,
|
||||
camelize,
|
||||
get_model_fields,
|
||||
is_valid_django_model,
|
||||
)
|
||||
|
||||
if six.PY3:
|
||||
from typing import Type
|
||||
|
||||
|
||||
def construct_fields(model, registry, only_fields, exclude_fields):
|
||||
ALL_FIELDS = "__all__"
|
||||
|
||||
|
||||
def construct_fields(
|
||||
model, registry, only_fields, exclude_fields, convert_choices_to_enum
|
||||
):
|
||||
_model_fields = get_model_fields(model)
|
||||
|
||||
fields = OrderedDict()
|
||||
|
@ -33,7 +45,18 @@ def construct_fields(model, registry, only_fields, exclude_fields):
|
|||
# in there. Or when we exclude this field in exclude_fields.
|
||||
# Or when there is no back reference.
|
||||
continue
|
||||
converted = convert_django_field_with_choices(field, registry)
|
||||
|
||||
_convert_choices_to_enum = convert_choices_to_enum
|
||||
if not isinstance(_convert_choices_to_enum, bool):
|
||||
# then `convert_choices_to_enum` is a list of field names to convert
|
||||
if name in _convert_choices_to_enum:
|
||||
_convert_choices_to_enum = True
|
||||
else:
|
||||
_convert_choices_to_enum = False
|
||||
|
||||
converted = convert_django_field_with_choices(
|
||||
field, registry, convert_choices_to_enum=_convert_choices_to_enum
|
||||
)
|
||||
fields[name] = converted
|
||||
|
||||
return fields
|
||||
|
@ -55,14 +78,17 @@ class DjangoObjectType(ObjectType):
|
|||
model=None,
|
||||
registry=None,
|
||||
skip_registry=False,
|
||||
only_fields=(),
|
||||
exclude_fields=(),
|
||||
only_fields=(), # deprecated in favour of `fields`
|
||||
fields=(),
|
||||
exclude_fields=(), # deprecated in favour of `exclude`
|
||||
exclude=(),
|
||||
filter_fields=None,
|
||||
filterset_class=None,
|
||||
connection=None,
|
||||
connection_class=None,
|
||||
use_connection=None,
|
||||
interfaces=(),
|
||||
convert_choices_to_enum=True,
|
||||
_meta=None,
|
||||
**options
|
||||
):
|
||||
|
@ -89,8 +115,49 @@ class DjangoObjectType(ObjectType):
|
|||
)
|
||||
)
|
||||
|
||||
assert not (fields and exclude), (
|
||||
"Cannot set both 'fields' and 'exclude' options on "
|
||||
"DjangoObjectType {class_name}.".format(class_name=cls.__name__)
|
||||
)
|
||||
|
||||
# Alias only_fields -> fields
|
||||
if only_fields and fields:
|
||||
raise Exception("Can't set both only_fields and fields")
|
||||
if only_fields:
|
||||
warnings.warn(
|
||||
"Defining `only_fields` is deprecated in favour of `fields`.",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fields = only_fields
|
||||
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
|
||||
raise TypeError(
|
||||
'The `fields` option must be a list or tuple or "__all__". '
|
||||
"Got %s." % type(fields).__name__
|
||||
)
|
||||
|
||||
if fields == ALL_FIELDS:
|
||||
fields = None
|
||||
|
||||
# Alias exclude_fields -> exclude
|
||||
if exclude_fields and exclude:
|
||||
raise Exception("Can't set both exclude_fields and exclude")
|
||||
if exclude_fields:
|
||||
warnings.warn(
|
||||
"Defining `exclude_fields` is deprecated in favour of `exclude`.",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
exclude = exclude_fields
|
||||
if exclude and not isinstance(exclude, (list, tuple)):
|
||||
raise TypeError(
|
||||
"The `exclude` option must be a list or tuple. Got %s."
|
||||
% type(exclude).__name__
|
||||
)
|
||||
|
||||
django_fields = yank_fields_from_attrs(
|
||||
construct_fields(model, registry, only_fields, exclude_fields), _as=Field
|
||||
construct_fields(model, registry, fields, exclude, convert_choices_to_enum),
|
||||
_as=Field,
|
||||
)
|
||||
|
||||
if use_connection is None and interfaces:
|
||||
|
@ -165,3 +232,8 @@ class DjangoObjectType(ObjectType):
|
|||
class ErrorType(ObjectType):
|
||||
field = graphene.String(required=True)
|
||||
messages = graphene.List(graphene.NonNull(graphene.String), required=True)
|
||||
|
||||
@classmethod
|
||||
def from_errors(cls, errors):
|
||||
data = camelize(errors) if graphene_settings.CAMELCASE_ERRORS else errors
|
||||
return [cls(field=key, messages=value) for key, value in data.items()]
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
from .testing import GraphQLTestCase
|
||||
from .utils import (
|
||||
DJANGO_FILTER_INSTALLED,
|
||||
get_reverse_fields,
|
||||
maybe_queryset,
|
||||
camelize,
|
||||
get_model_fields,
|
||||
is_valid_django_model,
|
||||
get_reverse_fields,
|
||||
import_single_dispatch,
|
||||
is_valid_django_model,
|
||||
maybe_queryset,
|
||||
)
|
||||
from .testing import GraphQLTestCase
|
||||
|
||||
__all__ = [
|
||||
"DJANGO_FILTER_INSTALLED",
|
||||
"get_reverse_fields",
|
||||
"maybe_queryset",
|
||||
"get_model_fields",
|
||||
"camelize",
|
||||
"is_valid_django_model",
|
||||
"import_single_dispatch",
|
||||
"GraphQLTestCase",
|
||||
|
|
|
@ -54,8 +54,8 @@ class GraphQLTestCase(TestCase):
|
|||
the call was fine.
|
||||
:resp HttpResponse: Response
|
||||
"""
|
||||
content = json.loads(resp.content)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
content = json.loads(resp.content)
|
||||
self.assertNotIn("errors", list(content.keys()))
|
||||
|
||||
def assertResponseHasErrors(self, resp):
|
||||
|
|
|
@ -2,7 +2,11 @@ import inspect
|
|||
|
||||
from django.db import models
|
||||
from django.db.models.manager import Manager
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import Promise
|
||||
|
||||
from graphene.utils.str_converters import to_camel_case
|
||||
|
||||
try:
|
||||
import django_filters # noqa
|
||||
|
@ -12,6 +16,28 @@ except ImportError:
|
|||
DJANGO_FILTER_INSTALLED = False
|
||||
|
||||
|
||||
def isiterable(value):
|
||||
try:
|
||||
iter(value)
|
||||
except TypeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _camelize_django_str(s):
|
||||
if isinstance(s, Promise):
|
||||
s = force_text(s)
|
||||
return to_camel_case(s) if isinstance(s, six.string_types) else s
|
||||
|
||||
|
||||
def camelize(data):
|
||||
if isinstance(data, dict):
|
||||
return {_camelize_django_str(k): camelize(v) for k, v in data.items()}
|
||||
if isiterable(data) and not isinstance(data, (six.string_types, Promise)):
|
||||
return [camelize(d) for d in data]
|
||||
return data
|
||||
|
||||
|
||||
def get_reverse_fields(model, local_field_names):
|
||||
for name, attr in model.__dict__.items():
|
||||
# Don't duplicate any local fields
|
||||
|
|
32
setup.cfg
32
setup.cfg
|
@ -5,11 +5,41 @@ test=pytest
|
|||
universal=1
|
||||
|
||||
[flake8]
|
||||
exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/*
|
||||
exclude = docs,graphene_django/debug/sql/*,migrations
|
||||
max-line-length = 120
|
||||
select =
|
||||
# Dictionary key repeated
|
||||
F601,
|
||||
# Ensure use of ==/!= to compare with str, bytes and int literals
|
||||
F632,
|
||||
# Redefinition of unused name
|
||||
F811,
|
||||
# Using an undefined variable
|
||||
F821,
|
||||
# Defining an undefined variable in __all__
|
||||
F822,
|
||||
# Using a variable before it is assigned
|
||||
F823,
|
||||
# Duplicate argument in function declaration
|
||||
F831,
|
||||
# Black would format this line
|
||||
BLK,
|
||||
# Do not use bare except
|
||||
B001,
|
||||
# Don't allow ++n. You probably meant n += 1
|
||||
B002,
|
||||
# Do not use mutable structures for argument defaults
|
||||
B006,
|
||||
# Do not perform calls in argument defaults
|
||||
B008
|
||||
|
||||
[coverage:run]
|
||||
omit = */tests/*
|
||||
|
||||
[isort]
|
||||
known_first_party=graphene,graphene_django
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
|
|
8
setup.py
8
setup.py
|
@ -28,6 +28,8 @@ tests_require = [
|
|||
dev_requires = [
|
||||
"black==19.3b0",
|
||||
"flake8==3.7.7",
|
||||
"flake8-black==0.1.0",
|
||||
"flake8-bugbear==19.3.0",
|
||||
] + tests_require
|
||||
|
||||
setup(
|
||||
|
@ -64,7 +66,11 @@ setup(
|
|||
setup_requires=["pytest-runner"],
|
||||
tests_require=tests_require,
|
||||
rest_framework_require=rest_framework_require,
|
||||
extras_require={"test": tests_require, "rest_framework": rest_framework_require, "dev": dev_requires},
|
||||
extras_require={
|
||||
"test": tests_require,
|
||||
"rest_framework": rest_framework_require,
|
||||
"dev": dev_requires,
|
||||
},
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
platforms="any",
|
||||
|
|
6
tox.ini
6
tox.ini
|
@ -28,12 +28,12 @@ commands = {posargs:py.test --cov=graphene_django graphene_django examples}
|
|||
|
||||
[testenv:black]
|
||||
basepython = python3.7
|
||||
deps = black
|
||||
deps = -e.[dev]
|
||||
commands =
|
||||
black --exclude "/migrations/" graphene_django examples --check
|
||||
black --exclude "/migrations/" graphene_django examples setup.py --check
|
||||
|
||||
[testenv:flake8]
|
||||
basepython = python3.7
|
||||
deps = flake8
|
||||
deps = -e.[dev]
|
||||
commands =
|
||||
flake8 graphene_django examples
|
||||
|
|
Loading…
Reference in New Issue
Block a user