Merge branch 'main' into fix-m2m-filter-type

This commit is contained in:
Firas K 2023-01-09 12:00:06 +03:00 committed by GitHub
commit e1b111d3ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 1664 additions and 480 deletions

View File

@ -16,7 +16,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install tox
- name: Run lint 💅
- name: Run pre-commit 💅
run: tox
env:
TOXENV: flake8
TOXENV: pre-commit

View File

@ -8,9 +8,11 @@ jobs:
strategy:
max-parallel: 4
matrix:
django: ["2.2", "3.0", "3.1", "3.2"]
python-version: ["3.6", "3.7", "3.8", "3.9"]
django: ["3.2", "4.0", "4.1"]
python-version: ["3.8", "3.9", "3.10"]
include:
- django: "3.2"
python-version: "3.7"
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}

30
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,30 @@
default_language_version:
python: python3.9
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-merge-conflict
- id: check-json
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
exclude: ^docs/.*$
- id: pretty-format-json
args:
- --autofix
- id: trailing-whitespace
exclude: README.md
- repo: https://github.com/asottile/pyupgrade
rev: v3.2.0
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8

View File

@ -1,22 +1,21 @@
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}'
.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
.PHONY: tests ## Run unit tests
tests:
py.test graphene_django --cov=graphene_django -vv
.PHONY: test
test: tests # Alias test -> tests
.PHONY: format
.PHONY: format ## Format code
format:
black --exclude "/migrations/" graphene_django examples setup.py
black graphene_django examples setup.py
.PHONY: lint
.PHONY: lint ## Lint code
lint:
flake8 graphene_django examples

View File

@ -55,7 +55,7 @@ from graphene_django.views import GraphQLView
urlpatterns = [
# ...
path('graphql', GraphQLView.as_view(graphiql=True)),
path('graphql/', GraphQLView.as_view(graphiql=True)),
]
```

View File

@ -198,7 +198,7 @@ For Django 2.2 and above:
urlpatterns = [
# some other urls
path('graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
path('graphql/', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
]
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin

View File

@ -2,8 +2,8 @@ Filtering
=========
Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results.
See the `usage documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
`django-filter <https://django-filter.readthedocs.io/en/main/>`__ to provide filtering of results.
See the `usage documentation <https://django-filter.readthedocs.io/en/main/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``.
@ -26,7 +26,7 @@ After installing ``django-filter`` you'll need to add the application in the ``s
]
Note: The techniques below are demoed in the `cookbook example
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
app <https://github.com/graphql-python/graphene-django/tree/main/examples/cookbook>`__.
Filterable fields
-----------------
@ -34,7 +34,7 @@ Filterable fields
The ``filter_fields`` parameter is used to specify the fields which can
be filtered upon. The value specified here is passed directly to
``django-filter``, so see the `filtering
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
documentation <https://django-filter.readthedocs.io/en/main/guide/usage.html#the-filter>`__
for full details on the range of options available.
For example:
@ -192,7 +192,7 @@ in unison with the ``filter_fields`` parameter:
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>`__
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/main/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
pre-filter animals owned by the authenticated user (set in ``context.user``).

View File

@ -151,7 +151,7 @@ For example the following ``Model`` and ``DjangoObjectType``:
Results in the following GraphQL schema definition:
.. code::
.. code:: graphql
type Pet {
id: ID!
@ -178,7 +178,7 @@ You can disable this automatic conversion by setting
fields = ("id", "kind",)
convert_choices_to_enum = False
.. code::
.. code:: graphql
type Pet {
id: ID!
@ -313,7 +313,7 @@ Additionally, Resolvers will receive **any arguments declared in the field defin
bar=graphene.Int()
)
def resolve_question(root, info, foo, bar):
def resolve_question(root, info, foo=None, bar=None):
# If `foo` or `bar` are declared in the GraphQL query they will be here, else None.
return Question.objects.filter(foo=foo, bar=bar).first()
@ -418,7 +418,7 @@ the core graphene pages for more information on customizing the Relay experience
You can now execute queries like:
.. code:: python
.. code:: graphql
{
questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
@ -440,7 +440,7 @@ You can now execute queries like:
Which returns:
.. code:: python
.. code:: json
{
"data": {

View File

@ -1,32 +1,29 @@
import graphene
import graphene
from graphene_django.types import DjangoObjectType
from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient
from cookbook.ingredients.models import Category, Ingredient
class CategoryType(DjangoObjectType):
class CategoryType(DjangoObjectType):
class Meta:
model = Category
fields = '__all__'
fields = "__all__"
class IngredientType(DjangoObjectType):
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
fields = '__all__'
fields = "__all__"
class Query(object):
category = graphene.Field(CategoryType,
id=graphene.Int(),
name=graphene.String())
class Query:
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType)
ingredient = graphene.Field(IngredientType,
id=graphene.Int(),
name=graphene.String())
ingredient = graphene.Field(
IngredientType, id=graphene.Int(), name=graphene.String()
)
all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, info, **kwargs):
@ -36,8 +33,8 @@
return Ingredient.objects.all()
def resolve_category(self, info, **kwargs):
id = kwargs.get('id')
name = kwargs.get('name')
id = kwargs.get("id")
name = kwargs.get("name")
if id is not None:
return Category.objects.get(pk=id)
@ -48,8 +45,8 @@
return None
def resolve_ingredient(self, info, **kwargs):
id = kwargs.get('id')
name = kwargs.get('name')
id = kwargs.get("id")
name = kwargs.get("name")
if id is not None:
return Ingredient.objects.get(pk=id)

View File

@ -189,7 +189,7 @@ Default: ``None``
``GRAPHIQL_HEADER_EDITOR_ENABLED``
---------------------
----------------------------------
GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables.
@ -207,3 +207,36 @@ Default: ``True``
GRAPHENE = {
'GRAPHIQL_HEADER_EDITOR_ENABLED': True,
}
``TESTING_ENDPOINT``
--------------------
Define the graphql endpoint url used for the `GraphQLTestCase` class.
Default: ``/graphql``
.. code:: python
GRAPHENE = {
'TESTING_ENDPOINT': '/customEndpoint'
}
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
---------------------
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_.
.. _GraphiQLDocs: https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
Default: ``False``
.. code:: python
GRAPHENE = {
'GRAPHIQL_SHOULD_PERSIST_HEADERS': False,
}

View File

@ -6,7 +6,8 @@ Using unittest
If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`.
Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLTestCase`. The default endpoint is `GRAPHQL_URL = "/graphql/"`.
The default endpoint for testing is `/graphql`. You can override this in the `settings <https://docs.graphene-python.org/projects/django/en/latest/settings/#testing-endpoint>`__.
Usage:
@ -27,7 +28,7 @@ Usage:
}
}
''',
op_name='myModel'
operation_name='myModel'
)
content = json.loads(response.content)
@ -48,7 +49,7 @@ Usage:
}
}
''',
op_name='myModel',
operation_name='myModel',
variables={'id': 1}
)
@ -72,7 +73,7 @@ Usage:
}
}
''',
op_name='myMutation',
operation_name='myMutation',
input_data={'my_field': 'foo', 'other_field': 'bar'}
)
@ -107,7 +108,7 @@ Usage:
}
}
''',
op_name='myMutation',
operation_name='myMutation',
input_data={'my_field': 'foo', 'other_field': 'bar'}
)
@ -147,7 +148,7 @@ To use pytest define a simple fixture using the query helper below
}
}
''',
op_name='myModel'
operation_name='myModel'
)
content = json.loads(response.content)

View File

@ -35,6 +35,7 @@ Now sync your database for the first time:
.. code:: bash
cd ..
python manage.py migrate
Let's create a few simple models...
@ -77,6 +78,18 @@ Add ingredients as INSTALLED_APPS:
"cookbook.ingredients",
]
Make sure the app name in ``cookbook.ingredients.apps.IngredientsConfig`` is set to ``cookbook.ingredients``.
.. code:: python
# cookbook/ingredients/apps.py
from django.apps import AppConfig
class IngredientsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'cookbook.ingredients'
Don't forget to create & run migrations:

View File

@ -151,7 +151,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
interfaces = (relay.Node, )
class Query(graphene.ObjectType):
class Query(ObjectType):
category = relay.Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -14,7 +14,7 @@ whole Graphene repository:
```bash
# Get the example project code
git clone https://github.com/graphql-python/graphene-django.git
cd graphene-django/examples/cookbook
cd graphene-django/examples/cookbook-plain
```
It is good idea (but not required) to create a virtual environment

View File

@ -1 +1,52 @@
[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}]
[
{
"fields": {
"name": "Dairy"
},
"model": "ingredients.category",
"pk": 1
},
{
"fields": {
"name": "Meat"
},
"model": "ingredients.category",
"pk": 2
},
{
"fields": {
"category": 1,
"name": "Eggs",
"notes": "Good old eggs"
},
"model": "ingredients.ingredient",
"pk": 1
},
{
"fields": {
"category": 1,
"name": "Milk",
"notes": "Comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 2
},
{
"fields": {
"category": 2,
"name": "Beef",
"notes": "Much like milk, this comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 3
},
{
"fields": {
"category": 2,
"name": "Chicken",
"notes": "Definitely doesn't come from a cow"
},
"model": "ingredients.ingredient",
"pk": 4
}
]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:15
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
@ -10,24 +8,46 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='Category',
name="Category",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Ingredient',
name="Ingredient",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('notes', models.TextField()),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("notes", models.TextField()),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ingredients",
to="ingredients.Category",
),
),
],
),
]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 00:50
from __future__ import unicode_literals
from django.db import migrations, models
@ -8,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ingredients', '0001_initial'),
("ingredients", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name='ingredient',
name='notes',
model_name="ingredient",
name="notes",
field=models.TextField(blank=True, null=True),
),
]

View File

@ -6,12 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ingredients', '0002_auto_20161104_0050'),
("ingredients", "0002_auto_20161104_0050"),
]
operations = [
migrations.AlterModelOptions(
name='category',
options={'verbose_name_plural': 'Categories'},
name="category",
options={"verbose_name_plural": "Categories"},
),
]

View File

@ -16,7 +16,7 @@ class IngredientType(DjangoObjectType):
fields = "__all__"
class Query(object):
class Query:
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:20
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
@ -11,26 +9,62 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('ingredients', '0001_initial'),
("ingredients", "0001_initial"),
]
operations = [
migrations.CreateModel(
name='Recipe',
name="Recipe",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
('instructions', models.TextField()),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("instructions", models.TextField()),
],
),
migrations.CreateModel(
name='RecipeIngredient',
name="RecipeIngredient",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.FloatField()),
('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)),
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')),
('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("amount", models.FloatField()),
(
"unit",
models.CharField(
choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")],
max_length=20,
),
),
(
"ingredient",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="used_by",
to="ingredients.Ingredient",
),
),
(
"recipes",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="amounts",
to="recipes.Recipe",
),
),
],
),
]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 01:06
from __future__ import unicode_literals
from django.db import migrations, models
@ -8,18 +6,26 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipes', '0001_initial'),
("recipes", "0001_initial"),
]
operations = [
migrations.RenameField(
model_name='recipeingredient',
old_name='recipes',
new_name='recipe',
model_name="recipeingredient",
old_name="recipes",
new_name="recipe",
),
migrations.AlterField(
model_name='recipeingredient',
name='unit',
field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20),
model_name="recipeingredient",
name="unit",
field=models.CharField(
choices=[
(b"unit", b"Units"),
(b"kg", b"Kilograms"),
(b"l", b"Litres"),
(b"st", b"Shots"),
],
max_length=20,
),
),
]

View File

@ -6,13 +6,21 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipes', '0002_auto_20161104_0106'),
("recipes", "0002_auto_20161104_0106"),
]
operations = [
migrations.AlterField(
model_name='recipeingredient',
name='unit',
field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20),
model_name="recipeingredient",
name="unit",
field=models.CharField(
choices=[
("unit", "Units"),
("kg", "Kilograms"),
("l", "Litres"),
("st", "Shots"),
],
max_length=20,
),
),
]

View File

@ -16,7 +16,7 @@ class RecipeIngredientType(DjangoObjectType):
fields = "__all__"
class Query(object):
class Query:
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
all_recipes = graphene.List(RecipeType)

View File

@ -1,4 +1,4 @@
Cookbook Example Django Project
Cookbook Example (Relay) Django Project
===============================
This example project demos integration between Graphene and Django.
@ -60,5 +60,5 @@ Now you should be ready to start the server:
Now head on over to
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
and run some queries!
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-relay/#testing-our-graphql-schema)
for some example queries)

View File

@ -1 +1,52 @@
[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}]
[
{
"fields": {
"name": "Dairy"
},
"model": "ingredients.category",
"pk": 1
},
{
"fields": {
"name": "Meat"
},
"model": "ingredients.category",
"pk": 2
},
{
"fields": {
"category": 1,
"name": "Eggs",
"notes": "Good old eggs"
},
"model": "ingredients.ingredient",
"pk": 1
},
{
"fields": {
"category": 1,
"name": "Milk",
"notes": "Comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 2
},
{
"fields": {
"category": 2,
"name": "Beef",
"notes": "Much like milk, this comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 3
},
{
"fields": {
"category": 2,
"name": "Chicken",
"notes": "Definitely doesn't come from a cow"
},
"model": "ingredients.ingredient",
"pk": 4
}
]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:15
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
@ -10,24 +8,46 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='Category',
name="Category",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Ingredient',
name="Ingredient",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('notes', models.TextField()),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("notes", models.TextField()),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ingredients",
to="ingredients.Category",
),
),
],
),
]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 00:50
from __future__ import unicode_literals
from django.db import migrations, models
@ -8,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ingredients', '0001_initial'),
("ingredients", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name='ingredient',
name='notes',
model_name="ingredient",
name="notes",
field=models.TextField(blank=True, null=True),
),
]

View File

@ -28,7 +28,7 @@ class IngredientNode(DjangoObjectType):
}
class Query(object):
class Query:
category = Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:20
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
@ -11,26 +9,62 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('ingredients', '0001_initial'),
("ingredients", "0001_initial"),
]
operations = [
migrations.CreateModel(
name='Recipe',
name="Recipe",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
('instructions', models.TextField()),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("instructions", models.TextField()),
],
),
migrations.CreateModel(
name='RecipeIngredient',
name="RecipeIngredient",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.FloatField()),
('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)),
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')),
('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("amount", models.FloatField()),
(
"unit",
models.CharField(
choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")],
max_length=20,
),
),
(
"ingredient",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="used_by",
to="ingredients.Ingredient",
),
),
(
"recipes",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="amounts",
to="recipes.Recipe",
),
),
],
),
]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 01:06
from __future__ import unicode_literals
from django.db import migrations, models
@ -8,18 +6,26 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipes', '0001_initial'),
("recipes", "0001_initial"),
]
operations = [
migrations.RenameField(
model_name='recipeingredient',
old_name='recipes',
new_name='recipe',
model_name="recipeingredient",
old_name="recipes",
new_name="recipe",
),
migrations.AlterField(
model_name='recipeingredient',
name='unit',
field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20),
model_name="recipeingredient",
name="unit",
field=models.CharField(
choices=[
(b"unit", b"Units"),
(b"kg", b"Kilograms"),
(b"l", b"Litres"),
(b"st", b"Shots"),
],
max_length=20,
),
),
]

View File

@ -25,7 +25,7 @@ class RecipeIngredientNode(DjangoObjectType):
}
class Query(object):
class Query:
recipe = Node.Field(RecipeNode)
all_recipes = DjangoFilterConnectionField(RecipeNode)

View File

@ -1 +1,302 @@
[{"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=", "last_login": "2016-11-04T00:46:58Z", "is_superuser": true, "username": "admin", "first_name": "", "last_name": "", "email": "asdf@example.com", "is_staff": true, "is_active": true, "date_joined": "2016-11-03T18:24:40Z", "groups": [], "user_permissions": []}}, {"model": "recipes.recipe", "pk": 1, "fields": {"title": "Cheerios With a Shot of Vermouth", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 2, "fields": {"title": "Quail Eggs in Whipped Cream and MSG", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 3, "fields": {"title": "Deep Fried Skittles", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 4, "fields": {"title": "Newt ala Doritos", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 5, "fields": {"title": "Fruit Salad", "instructions": "Chop up and add together"}}, {"model": "recipes.recipeingredient", "pk": 1, "fields": {"recipes": 5, "ingredient": 9, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 2, "fields": {"recipes": 5, "ingredient": 10, "amount": 2.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 3, "fields": {"recipes": 5, "ingredient": 7, "amount": 3.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 4, "fields": {"recipes": 5, "ingredient": 8, "amount": 4.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 5, "fields": {"recipes": 4, "ingredient": 5, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 6, "fields": {"recipes": 4, "ingredient": 6, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 7, "fields": {"recipes": 3, "ingredient": 4, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 8, "fields": {"recipes": 2, "ingredient": 2, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 9, "fields": {"recipes": 2, "ingredient": 11, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 10, "fields": {"recipes": 2, "ingredient": 12, "amount": 3.0, "unit": "st"}}, {"model": "recipes.recipeingredient", "pk": 11, "fields": {"recipes": 1, "ingredient": 1, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 12, "fields": {"recipes": 1, "ingredient": 3, "amount": 1.0, "unit": "st"}}, {"model": "ingredients.category", "pk": 1, "fields": {"name": "fruit"}}, {"model": "ingredients.category", "pk": 3, "fields": {"name": "xkcd"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Cheerios", "notes": "this is a note", "category": 3}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Quail Eggs", "notes": "has more notes", "category": 3}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Vermouth", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Skittles", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 5, "fields": {"name": "Newt", "notes": "Braised and Confuesd", "category": 3}}, {"model": "ingredients.ingredient", "pk": 6, "fields": {"name": "Doritos", "notes": "Crushed", "category": 3}}, {"model": "ingredients.ingredient", "pk": 7, "fields": {"name": "Apple", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 8, "fields": {"name": "Orange", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 9, "fields": {"name": "Banana", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 10, "fields": {"name": "Grapes", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 11, "fields": {"name": "Whipped Cream", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 12, "fields": {"name": "MSG", "notes": "", "category": 3}}]
[
{
"fields": {
"date_joined": "2016-11-03T18:24:40Z",
"email": "asdf@example.com",
"first_name": "",
"groups": [],
"is_active": true,
"is_staff": true,
"is_superuser": true,
"last_login": "2016-11-04T00:46:58Z",
"last_name": "",
"password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=",
"user_permissions": [],
"username": "admin"
},
"model": "auth.user",
"pk": 1
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Cheerios With a Shot of Vermouth"
},
"model": "recipes.recipe",
"pk": 1
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Quail Eggs in Whipped Cream and MSG"
},
"model": "recipes.recipe",
"pk": 2
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Deep Fried Skittles"
},
"model": "recipes.recipe",
"pk": 3
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Newt ala Doritos"
},
"model": "recipes.recipe",
"pk": 4
},
{
"fields": {
"instructions": "Chop up and add together",
"title": "Fruit Salad"
},
"model": "recipes.recipe",
"pk": 5
},
{
"fields": {
"amount": 1.0,
"ingredient": 9,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 1
},
{
"fields": {
"amount": 2.0,
"ingredient": 10,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 2
},
{
"fields": {
"amount": 3.0,
"ingredient": 7,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 3
},
{
"fields": {
"amount": 4.0,
"ingredient": 8,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 4
},
{
"fields": {
"amount": 1.0,
"ingredient": 5,
"recipes": 4,
"unit": "kg"
},
"model": "recipes.recipeingredient",
"pk": 5
},
{
"fields": {
"amount": 2.0,
"ingredient": 6,
"recipes": 4,
"unit": "l"
},
"model": "recipes.recipeingredient",
"pk": 6
},
{
"fields": {
"amount": 1.0,
"ingredient": 4,
"recipes": 3,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 7
},
{
"fields": {
"amount": 1.0,
"ingredient": 2,
"recipes": 2,
"unit": "kg"
},
"model": "recipes.recipeingredient",
"pk": 8
},
{
"fields": {
"amount": 2.0,
"ingredient": 11,
"recipes": 2,
"unit": "l"
},
"model": "recipes.recipeingredient",
"pk": 9
},
{
"fields": {
"amount": 3.0,
"ingredient": 12,
"recipes": 2,
"unit": "st"
},
"model": "recipes.recipeingredient",
"pk": 10
},
{
"fields": {
"amount": 1.0,
"ingredient": 1,
"recipes": 1,
"unit": "kg"
},
"model": "recipes.recipeingredient",
"pk": 11
},
{
"fields": {
"amount": 1.0,
"ingredient": 3,
"recipes": 1,
"unit": "st"
},
"model": "recipes.recipeingredient",
"pk": 12
},
{
"fields": {
"name": "fruit"
},
"model": "ingredients.category",
"pk": 1
},
{
"fields": {
"name": "xkcd"
},
"model": "ingredients.category",
"pk": 3
},
{
"fields": {
"category": 3,
"name": "Cheerios",
"notes": "this is a note"
},
"model": "ingredients.ingredient",
"pk": 1
},
{
"fields": {
"category": 3,
"name": "Quail Eggs",
"notes": "has more notes"
},
"model": "ingredients.ingredient",
"pk": 2
},
{
"fields": {
"category": 3,
"name": "Vermouth",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 3
},
{
"fields": {
"category": 3,
"name": "Skittles",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 4
},
{
"fields": {
"category": 3,
"name": "Newt",
"notes": "Braised and Confuesd"
},
"model": "ingredients.ingredient",
"pk": 5
},
{
"fields": {
"category": 3,
"name": "Doritos",
"notes": "Crushed"
},
"model": "ingredients.ingredient",
"pk": 6
},
{
"fields": {
"category": 1,
"name": "Apple",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 7
},
{
"fields": {
"category": 1,
"name": "Orange",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 8
},
{
"fields": {
"category": 1,
"name": "Banana",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 9
},
{
"fields": {
"category": 1,
"name": "Grapes",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 10
},
{
"fields": {
"category": 3,
"name": "Whipped Cream",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 11
},
{
"fields": {
"category": 3,
"name": "MSG",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 12
}
]

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import
from django.db import models

View File

@ -1,7 +1,7 @@
from .fields import DjangoConnectionField, DjangoListField
from .types import DjangoObjectType
__version__ = "3.0.0b7"
__version__ = "3.0.0"
__all__ = [
"__version__",

View File

@ -1,4 +1,4 @@
class MissingType(object):
class MissingType:
def __init__(self, *args, **kwargs):
pass

View File

@ -24,8 +24,15 @@ from graphene import (
Decimal,
)
from graphene.types.json import JSONString
from graphene.types.scalars import BigInt
from graphene.utils.str_converters import to_camel_case
from graphql import GraphQLError, assert_valid_name
from graphql import GraphQLError
try:
from graphql import assert_name
except ImportError:
# Support for older versions of graphql
from graphql import assert_valid_name as assert_name
from graphql.pyutils import register_description
from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
@ -55,7 +62,7 @@ class BlankValueField(Field):
def convert_choice_name(name):
name = to_const(force_str(name))
try:
assert_valid_name(name)
assert_name(name)
except GraphQLError:
name = "A_%s" % name
return name
@ -67,8 +74,7 @@ def get_choices(choices):
choices = choices.items()
for value, help_text in choices:
if isinstance(help_text, (tuple, list)):
for choice in get_choices(help_text):
yield choice
yield from get_choices(help_text)
else:
name = convert_choice_name(value)
while name in converted_names:
@ -85,7 +91,7 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]: c[2] for c in choices}
class EnumWithDescriptionsType(object):
class EnumWithDescriptionsType:
@property
def description(self):
return str(named_choices_descriptions[self.name])
@ -102,7 +108,7 @@ def generate_enum_name(django_model_meta, field):
)
name = custom_func(field)
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True:
name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name))
name = to_camel_case(f"{django_model_meta.object_name}_{field.name}")
else:
name = "{app_label}{object_name}{field_name}Choices".format(
app_label=to_camel_case(django_model_meta.app_label.title()),
@ -148,7 +154,9 @@ def get_django_field_description(field):
@singledispatch
def convert_django_field(field, registry=None):
raise Exception(
"Don't know how to convert the Django field %s (%s)" % (field, field.__class__)
"Don't know how to convert the Django field {} ({})".format(
field, field.__class__
)
)
@ -186,10 +194,14 @@ def convert_field_to_uuid(field, registry=None):
)
@convert_django_field.register(models.BigIntegerField)
def convert_big_int_field(field, registry=None):
return BigInt(description=field.help_text, required=not field.null)
@convert_django_field.register(models.PositiveIntegerField)
@convert_django_field.register(models.PositiveSmallIntegerField)
@convert_django_field.register(models.SmallIntegerField)
@convert_django_field.register(models.BigIntegerField)
@convert_django_field.register(models.IntegerField)
def convert_field_to_int(field, registry=None):
return Int(description=get_django_field_description(field), required=not field.null)
@ -205,7 +217,9 @@ def convert_field_to_boolean(field, registry=None):
@convert_django_field.register(models.DecimalField)
def convert_field_to_decimal(field, registry=None):
return Decimal(description=field.help_text, required=not field.null)
return Decimal(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.FloatField)
@ -301,7 +315,26 @@ def convert_field_to_djangomodel(field, registry=None):
if not _type:
return
return Field(
class CustomField(Field):
def wrap_resolve(self, parent_resolver):
"""
Implements a custom resolver which go through the `get_node` method to ensure that
it goes through the `get_queryset` method of the DjangoObjectType.
"""
resolver = super().wrap_resolve(parent_resolver)
def custom_resolver(root, info, **args):
fk_obj = resolver(root, info, **args)
if not isinstance(fk_obj, model):
# In case the resolver is a custom one that overwrites
# the default Django resolver
# This happens, for example, when using custom awaitable resolvers.
return fk_obj
return _type.get_node(info, fk_obj.pk)
return custom_resolver
return CustomField(
_type,
description=get_django_field_description(field),
required=not field.null,

View File

@ -11,7 +11,7 @@ def wrap_exception(exception):
exc_type=force_str(type(exception)),
stack="".join(
traceback.format_exception(
etype=type(exception), value=exception, tb=exception.__traceback__
exception, value=exception, tb=exception.__traceback__
)
),
)

View File

@ -7,7 +7,7 @@ from .exception.formating import wrap_exception
from .types import DjangoDebug
class DjangoDebugContext(object):
class DjangoDebugContext:
def __init__(self):
self.debug_promise = None
self.promises = []
@ -46,7 +46,7 @@ class DjangoDebugContext(object):
unwrap_cursor(connection)
class DjangoDebugMiddleware(object):
class DjangoDebugMiddleware:
def resolve(self, next, root, info, **args):
context = info.context
django_debug = getattr(context, "django_debug", None)

View File

@ -1,5 +1,4 @@
# Code obtained from django-debug-toolbar sql panel tracking
from __future__ import absolute_import, unicode_literals
import json
from threading import local
@ -50,7 +49,7 @@ def unwrap_cursor(connection):
del connection._graphene_cursor
class ExceptionCursorWrapper(object):
class ExceptionCursorWrapper:
"""
Wraps a cursor and raises an exception on any operation.
Used in Templates panel.
@ -63,7 +62,7 @@ class ExceptionCursorWrapper(object):
raise SQLQueryTriggered()
class NormalCursorWrapper(object):
class NormalCursorWrapper:
"""
Wraps a cursor and logs queries.
"""
@ -85,7 +84,7 @@ class NormalCursorWrapper(object):
if not params:
return params
if isinstance(params, dict):
return dict((key, self._quote_expr(value)) for key, value in params.items())
return {key: self._quote_expr(value) for key, value in params.items()}
return list(map(self._quote_expr, params))
def _decode(self, param):

View File

@ -8,7 +8,7 @@ from ..middleware import DjangoDebugMiddleware
from ..types import DjangoDebug
class context(object):
class context:
pass

View File

@ -1,12 +1,14 @@
from functools import partial
from django.db.models.query import QuerySet
from graphql_relay.connection.arrayconnection import (
from graphql_relay import (
connection_from_array_slice,
cursor_to_offset,
get_offset_with_default,
offset_to_cursor,
)
from promise import Promise
from graphene import Int, NonNull
@ -26,7 +28,7 @@ class DjangoListField(Field):
_type = _type.of_type
# Django would never return a Set of None vvvvvvv
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
super().__init__(List(NonNull(_type)), *args, **kwargs)
assert issubclass(
self._underlying_type, DjangoObjectType
@ -61,13 +63,16 @@ class DjangoListField(Field):
return queryset
def wrap_resolve(self, parent_resolver):
resolver = super(DjangoListField, self).wrap_resolve(parent_resolver)
resolver = super().wrap_resolve(parent_resolver)
_type = self.type
if isinstance(_type, NonNull):
_type = _type.of_type
django_object_type = _type.of_type.of_type
return partial(
self.list_resolver, django_object_type, resolver, self.get_manager(),
self.list_resolver,
django_object_type,
resolver,
self.get_manager(),
)
@ -82,7 +87,7 @@ class DjangoConnectionField(ConnectionField):
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST,
)
kwargs.setdefault("offset", Int())
super(DjangoConnectionField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
@property
def type(self):
@ -144,36 +149,40 @@ class DjangoConnectionField(ConnectionField):
iterable = maybe_queryset(iterable)
if isinstance(iterable, QuerySet):
list_length = iterable.count()
array_length = iterable.count()
else:
list_length = len(iterable)
list_slice_length = (
min(max_limit, list_length) if max_limit is not None else list_length
)
array_length = len(iterable)
# If after is higher than list_length, connection_from_list_slice
# If after is higher than array_length, connection_from_array_slice
# would try to do a negative slicing which makes django throw an
# AssertionError
after = min(get_offset_with_default(args.get("after"), -1) + 1, list_length)
slice_start = min(
get_offset_with_default(args.get("after"), -1) + 1,
array_length,
)
array_slice_length = array_length - slice_start
if max_limit is not None and args.get("first", None) is None:
if args.get("last", None) is not None:
after = list_length - args["last"]
else:
# Impose the maximum limit via the `first` field if neither first or last are already provided
# (note that if any of them is provided they must be under max_limit otherwise an error is raised).
if (
max_limit is not None
and args.get("first", None) is None
and args.get("last", None) is None
):
args["first"] = max_limit
connection = connection_from_array_slice(
iterable[after:],
iterable[slice_start:],
args,
slice_start=after,
array_length=list_length,
array_slice_length=list_slice_length,
slice_start=slice_start,
array_length=array_length,
array_slice_length=array_slice_length,
connection_type=partial(connection_adapter, connection),
edge_type=connection.Edge,
page_info_type=page_info_adapter,
)
connection.iterable = iterable
connection.length = list_length
connection.length = array_length
return connection
@classmethod

View File

@ -30,7 +30,7 @@ def convert_enum(data):
class DjangoFilterConnectionField(DjangoConnectionField):
def __init__(
self,
type,
type_,
fields=None,
order_by=None,
extra_filter_meta=None,
@ -44,7 +44,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
self._filtering_args = None
self._extra_filter_meta = extra_filter_meta
self._base_args = None
super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs)
super().__init__(type_, *args, **kwargs)
@property
def args(self):
@ -90,9 +90,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
kwargs[k] = convert_enum(v)
return kwargs
qs = super(DjangoFilterConnectionField, cls).resolve_queryset(
connection, iterable, info, args
)
qs = super().resolve_queryset(connection, iterable, info, args)
filterset = filterset_class(
data=filter_kwargs(), queryset=qs, request=info.context

View File

@ -22,6 +22,6 @@ class ArrayFilter(TypedFilter):
return qs
if self.distinct:
qs = qs.distinct()
lookup = "%s__%s" % (self.field_name, self.lookup_expr)
lookup = f"{self.field_name}__{self.lookup_expr}"
qs = self.get_method(qs)(**{lookup: value})
return qs

View File

@ -13,11 +13,11 @@ class GlobalIDFilter(Filter):
field_class = GlobalIDFormField
def filter(self, qs, value):
""" Convert the filter value to a primary key before filtering """
"""Convert the filter value to a primary key before filtering"""
_id = None
if value is not None:
_, _id = from_global_id(value)
return super(GlobalIDFilter, self).filter(qs, _id)
return super().filter(qs, _id)
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
@ -25,4 +25,4 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
def filter(self, qs, value):
gids = [from_global_id(v)[1] for v in value]
return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids)
return super().filter(qs, gids)

View File

@ -23,4 +23,4 @@ class ListFilter(TypedFilter):
else:
return qs.none()
else:
return super(ListFilter, self).filter(qs, value)
return super().filter(qs, value)

View File

@ -12,7 +12,7 @@ class TypedFilter(Filter):
def __init__(self, input_type=None, *args, **kwargs):
self._input_type = input_type
super(TypedFilter, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
@property
def input_type(self):

View File

@ -18,8 +18,8 @@ GRAPHENE_FILTER_SET_OVERRIDES = {
class GrapheneFilterSetMixin(BaseFilterSet):
""" A django_filters.filterset.BaseFilterSet with default filter overrides
to handle global IDs """
"""A django_filters.filterset.BaseFilterSet with default filter overrides
to handle global IDs"""
FILTER_DEFAULTS = dict(
itertools.chain(
@ -29,20 +29,18 @@ class GrapheneFilterSetMixin(BaseFilterSet):
def setup_filterset(filterset_class):
""" Wrap a provided filterset in Graphene-specific functionality
"""
"""Wrap a provided filterset in Graphene-specific functionality"""
return type(
"Graphene{}".format(filterset_class.__name__),
f"Graphene{filterset_class.__name__}",
(filterset_class, GrapheneFilterSetMixin),
{},
)
def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta):
""" Create a filterset for the given model using the provided meta data
"""
"""Create a filterset for the given model using the provided meta data"""
meta.update({"model": model})
meta_class = type(str("Meta"), (object,), meta)
meta_class = type("Meta", (object,), meta)
filterset = type(
str("%sFilterSet" % model._meta.object_name),
(filterset_base_class, GrapheneFilterSetMixin),

View File

@ -1,4 +1,4 @@
from mock import MagicMock
from unittest.mock import MagicMock
import pytest
from django.db import models
@ -89,10 +89,10 @@ def Query(EventType):
def resolve_events(self, info, **kwargs):
events = [
Event(name="Live Show", tags=["concert", "music", "rock"],),
Event(name="Musical", tags=["movie", "music"],),
Event(name="Ballet", tags=["concert", "dance"],),
Event(name="Speech", tags=[],),
Event(name="Live Show", tags=["concert", "music", "rock"]),
Event(name="Musical", tags=["movie", "music"]),
Event(name="Ballet", tags=["concert", "dance"]),
Event(name="Speech", tags=[]),
]
STORE["events"] = events

View File

@ -120,10 +120,7 @@ def test_array_field_filter_schema_type(Query):
"randomField": "[Boolean!]",
}
filters_str = ", ".join(
[
f"{filter_field}: {gql_type} = null"
for filter_field, gql_type in filters.items()
]
[f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()]
)
assert (
f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str

View File

@ -54,13 +54,13 @@ def reporter_article_data():
first_name="Jane", last_name="Doe", email="janedoe@example.com", a_choice=2
)
Article.objects.create(
headline="Article Node 1", reporter=john, editor=john, lang="es",
headline="Article Node 1", reporter=john, editor=john, lang="es"
)
Article.objects.create(
headline="Article Node 2", reporter=john, editor=john, lang="en",
headline="Article Node 2", reporter=john, editor=john, lang="en"
)
Article.objects.create(
headline="Article Node 3", reporter=jane, editor=jane, lang="en",
headline="Article Node 3", reporter=jane, editor=jane, lang="en"
)
@ -80,7 +80,13 @@ def test_filter_enum_on_connection(schema, reporter_article_data):
}
"""
expected = {"allArticles": {"edges": [{"node": {"headline": "Article Node 1"}},]}}
expected = {
"allArticles": {
"edges": [
{"node": {"headline": "Article Node 1"}},
]
}
}
result = schema.execute(query)
assert not result.errors
@ -152,9 +158,6 @@ def test_filter_enum_field_schema_type(schema):
"reporter_AChoice_In": "[TestsReporterAChoiceChoices]",
}
filters_str = ", ".join(
[
f"{filter_field}: {gql_type} = null"
for filter_field, gql_type in filters.items()
]
[f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()]
)
assert f" allArticles({filters_str}): ArticleTypeConnection\n" in schema_str

View File

@ -5,7 +5,7 @@ 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 import Argument, Boolean, Decimal, Field, ObjectType, Schema, String
from graphene.relay import Node
from graphene_django import DjangoObjectType
from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField
@ -67,7 +67,7 @@ def assert_arguments(field, *arguments):
actual = [name for name in args if name not in ignore and not name.startswith("_")]
assert set(arguments) == set(
actual
), "Expected arguments ({}) did not match actual ({})".format(arguments, actual)
), f"Expected arguments ({arguments}) did not match actual ({actual})"
def assert_orderable(field):
@ -141,7 +141,7 @@ def test_filter_shortcut_filterset_context():
@property
def qs(self):
qs = super(ArticleContextFilter, self).qs
qs = super().qs
return qs.filter(reporter=self.request.reporter)
class Query(ObjectType):
@ -166,7 +166,7 @@ def test_filter_shortcut_filterset_context():
editor=r2,
)
class context(object):
class context:
reporter = r2
query = """
@ -401,7 +401,7 @@ def test_filterset_descriptions():
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter)
max_time = field.args["max_time"]
assert isinstance(max_time, Argument)
assert max_time.type == Float
assert max_time.type == Decimal
assert max_time.description == "The maximum time"
@ -1008,7 +1008,7 @@ def test_integer_field_filter_type():
assert str(schema) == dedent(
"""\
type Query {
pets(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection
pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
}
type PetTypeConnection {
@ -1056,8 +1056,7 @@ def test_integer_field_filter_type():
interface Node {
\"""The ID of the object\"""
id: ID!
}
"""
}"""
)
@ -1077,7 +1076,7 @@ def test_other_filter_types():
assert str(schema) == dedent(
"""\
type Query {
pets(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null, age_Isnull: Boolean = null, age_Lt: Int = null): PetTypeConnection
pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
}
type PetTypeConnection {
@ -1125,8 +1124,7 @@ def test_other_filter_types():
interface Node {
\"""The ID of the object\"""
id: ID!
}
"""
}"""
)
@ -1226,7 +1224,7 @@ def test_filter_filterset_based_on_mixin():
}
}
result = schema.execute(query, variable_values={"email": reporter_1.email},)
result = schema.execute(query, variable_values={"email": reporter_1.email})
assert not result.errors
assert result.data == expected
@ -1267,13 +1265,23 @@ def test_filter_string_contains():
result = schema.execute(query, variables={"filter": "Ja"})
assert not result.errors
assert result.data == {
"people": {"edges": [{"node": {"name": "Jack"}}, {"node": {"name": "Jane"}},]}
"people": {
"edges": [
{"node": {"name": "Jack"}},
{"node": {"name": "Jane"}},
]
}
}
result = schema.execute(query, variables={"filter": "o"})
assert not result.errors
assert result.data == {
"people": {"edges": [{"node": {"name": "Joe"}}, {"node": {"name": "Bob"}},]}
"people": {
"edges": [
{"node": {"name": "Joe"}},
{"node": {"name": "Bob"}},
]
}
}

View File

@ -349,19 +349,19 @@ def test_fk_id_in_filter(query):
schema = Schema(query=query)
query = """
query {
articles (reporter_In: [%s, %s]) {
edges {
node {
query {{
articles (reporter_In: [{}, {}]) {{
edges {{
node {{
headline
reporter {
reporter {{
lastName
}
}
}
}
}
""" % (
}}
}}
}}
}}
}}
""".format(
john_doe.id,
jean_bon.id,
)

View File

@ -98,20 +98,14 @@ def test_typed_filter_schema(schema):
)
for filter_field, gql_type in filters.items():
assert "{}: {} = null".format(filter_field, gql_type) in all_articles_filters
assert f"{filter_field}: {gql_type}" in all_articles_filters
def test_typed_filters_work(schema):
reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="")
Article.objects.create(
headline="A", reporter=reporter, editor=reporter, lang="es",
)
Article.objects.create(
headline="B", reporter=reporter, editor=reporter, lang="es",
)
Article.objects.create(
headline="C", reporter=reporter, editor=reporter, lang="en",
)
Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es")
Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es")
Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en")
query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }"

View File

@ -97,7 +97,9 @@ def get_filtering_args_from_filterset(filterset_class, type):
field_type = graphene.List(field_type)
args[name] = graphene.Argument(
field_type, description=filter_field.label, required=required,
field_type,
description=filter_field.label,
required=required,
)
return args

View File

@ -3,7 +3,19 @@ from functools import singledispatch
from django import forms
from django.core.exceptions import ImproperlyConfigured
from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time
from graphene import (
ID,
Boolean,
Decimal,
Float,
Int,
List,
String,
UUID,
Date,
DateTime,
Time,
)
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
@ -57,12 +69,18 @@ def convert_form_field_to_nullboolean(field):
return Boolean(description=get_form_field_description(field))
@convert_form_field.register(forms.DecimalField)
@convert_form_field.register(forms.FloatField)
def convert_form_field_to_float(field):
return Float(description=get_form_field_description(field), required=field.required)
@convert_form_field.register(forms.DecimalField)
def convert_form_field_to_decimal(field):
return Decimal(
description=get_form_field_description(field), required=field.required
)
@convert_form_field.register(forms.MultipleChoiceField)
def convert_form_field_to_string_list(field):
return List(

View File

@ -95,7 +95,7 @@ class DjangoFormMutation(BaseDjangoFormMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super(DjangoFormMutation, cls).__init_subclass_with_meta__(
super().__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options
)
@ -117,7 +117,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
class Meta:
abstract = True
errors = graphene.List(ErrorType)
errors = graphene.List(graphene.NonNull(ErrorType), required=True)
@classmethod
def __init_subclass_with_meta__(
@ -127,7 +127,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
return_field_name=None,
only_fields=(),
exclude_fields=(),
**options
**options,
):
if not form_class:
@ -147,7 +147,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
registry = get_global_registry()
model_type = registry.get_type_for_model(model)
if not model_type:
raise Exception("No type registered for model: {}".format(model.__name__))
raise Exception(f"No type registered for model: {model.__name__}")
if not return_field_name:
model_name = model.__name__
@ -163,7 +163,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super(DjangoModelFormMutation, cls).__init_subclass_with_meta__(
super().__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options
)

View File

@ -1,11 +1,12 @@
from django import forms
from py.test import raises
from pytest import raises
import graphene
from graphene import (
String,
Int,
Boolean,
Decimal,
Float,
ID,
UUID,
@ -97,8 +98,8 @@ def test_should_float_convert_float():
assert_conversion(forms.FloatField, Float)
def test_should_decimal_convert_float():
assert_conversion(forms.DecimalField, Float)
def test_should_decimal_convert_decimal():
assert_conversion(forms.DecimalField, Decimal)
def test_should_multiple_choice_convert_list():

View File

@ -1,7 +1,7 @@
import pytest
from django import forms
from django.core.exceptions import ValidationError
from py.test import raises
from pytest import raises
from graphene import Field, ObjectType, Schema, String
from graphene_django import DjangoObjectType

View File

@ -48,7 +48,7 @@ class CommandArguments(BaseCommand):
class Command(CommandArguments):
help = "Dump Graphene schema as a JSON or GraphQL file"
can_import_settings = True
requires_system_checks = False
requires_system_checks = []
def save_json_file(self, out, schema_dict, indent):
with open(out, "w") as outfile:
@ -73,16 +73,12 @@ class Command(CommandArguments):
elif file_extension == ".json":
self.save_json_file(out, schema_dict, indent)
else:
raise CommandError(
'Unrecognised file format "{}"'.format(file_extension)
)
raise CommandError(f'Unrecognised file format "{file_extension}"')
style = getattr(self, "style", None)
success = getattr(style, "SUCCESS", lambda x: x)
self.stdout.write(
success("Successfully dumped GraphQL schema to {}".format(out))
)
self.stdout.write(success(f"Successfully dumped GraphQL schema to {out}"))
def handle(self, *args, **options):
options_schema = options.get("schema")

View File

@ -1,4 +1,4 @@
class Registry(object):
class Registry:
def __init__(self):
self._registry = {}
self._field_registry = {}

View File

@ -114,7 +114,7 @@ class SerializerMutation(ClientIDMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super(SerializerMutation, cls).__init_subclass_with_meta__(
super().__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options
)

View File

@ -72,7 +72,7 @@ def convert_serializer_to_input_type(serializer_class):
for name, field in serializer.fields.items()
}
ret_type = type(
"{}Input".format(serializer.__class__.__name__),
f"{serializer.__class__.__name__}Input",
(graphene.InputObjectType,),
items,
)
@ -110,11 +110,15 @@ def convert_serializer_field_to_bool(field):
@get_graphene_type_from_serializer_field.register(serializers.FloatField)
@get_graphene_type_from_serializer_field.register(serializers.DecimalField)
def convert_serializer_field_to_float(field):
return graphene.Float
@get_graphene_type_from_serializer_field.register(serializers.DecimalField)
def convert_serializer_field_to_decimal(field):
return graphene.Decimal
@get_graphene_type_from_serializer_field.register(serializers.DateTimeField)
def convert_serializer_field_to_datetime_time(field):
return graphene.types.datetime.DateTime

View File

@ -3,7 +3,7 @@ import copy
import graphene
from django.db import models
from graphene import InputObjectType
from py.test import raises
from pytest import raises
from rest_framework import serializers
from ..serializer_converter import convert_serializer_field
@ -133,9 +133,9 @@ def test_should_float_convert_float():
assert_conversion(serializers.FloatField, graphene.Float)
def test_should_decimal_convert_float():
def test_should_decimal_convert_decimal():
assert_conversion(
serializers.DecimalField, graphene.Float, max_digits=4, decimal_places=2
serializers.DecimalField, graphene.Decimal, max_digits=4, decimal_places=2
)

View File

@ -1,6 +1,6 @@
import datetime
from py.test import raises
from pytest import raises
from rest_framework import serializers
from graphene import Field, ResolveInfo

View File

@ -11,7 +11,6 @@ This module provides the `graphene_settings` object, that is used to access
Graphene settings, checking for user settings first, then falling
back to the defaults.
"""
from __future__ import unicode_literals
from django.conf import settings
from django.test.signals import setting_changed
@ -41,7 +40,9 @@ DEFAULTS = {
# This sets headerEditorEnabled GraphiQL option, for details go to
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
"GRAPHIQL_HEADER_EDITOR_ENABLED": True,
"GRAPHIQL_SHOULD_PERSIST_HEADERS": False,
"ATOMIC_MUTATIONS": False,
"TESTING_ENDPOINT": "/graphql",
}
if settings.DEBUG:
@ -76,7 +77,7 @@ def import_from_string(val, setting_name):
module = importlib.import_module(module_path)
return getattr(module, class_name)
except (ImportError, AttributeError) as e:
msg = "Could not import '%s' for Graphene setting '%s'. %s: %s." % (
msg = "Could not import '{}' for Graphene setting '{}'. {}: {}.".format(
val,
setting_name,
e.__class__.__name__,
@ -85,7 +86,7 @@ def import_from_string(val, setting_name):
raise ImportError(msg)
class GrapheneSettings(object):
class GrapheneSettings:
"""
A settings object, that allows API settings to be accessed as properties.
For example:

View File

@ -10,14 +10,6 @@
history,
location,
) {
// Parse the cookie value for a CSRF token
var csrftoken;
var cookies = ("; " + document.cookie).split("; csrftoken=");
if (cookies.length == 2) {
csrftoken = cookies.pop().split(";").shift();
} else {
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
}
// Collect the URL parameters
var parameters = {};
@ -68,9 +60,19 @@
var headers = opts.headers || {};
headers['Accept'] = headers['Accept'] || 'application/json';
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
// Parse the cookie value for a CSRF token
var csrftoken;
var cookies = ("; " + document.cookie).split("; csrftoken=");
if (cookies.length == 2) {
csrftoken = cookies.pop().split(";").shift();
} else {
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
}
if (csrftoken) {
headers['X-CSRFToken'] = csrftoken
}
return fetch(fetchURL, {
method: "post",
headers: headers,
@ -176,6 +178,7 @@
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders,
query: parameters.query,
};
if (parameters.variables) {

View File

@ -46,6 +46,7 @@ add "&raw" to the end of the URL within a browser.
subscriptionPath: "{{subscription_path}}",
{% endif %}
graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }},
graphiqlShouldPersistHeaders: {{ graphiql_should_persist_headers|yesno:"true,false" }},
};
</script>
<script src="{% static 'graphene_django/graphiql.js' %}"></script>

View File

@ -8,8 +8,8 @@ import graphene
from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType
from py.test import raises
from py.test import mark
from pytest import raises
from pytest import mark
from rest_framework import serializers
from ...types import DjangoObjectType

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import
from django.db import models
from django.utils.translation import gettext_lazy as _
@ -13,6 +11,9 @@ class Person(models.Model):
class Pet(models.Model):
name = models.CharField(max_length=30)
age = models.PositiveIntegerField()
owner = models.ForeignKey(
"Person", on_delete=models.CASCADE, null=True, blank=True, related_name="pets"
)
class FilmDetails(models.Model):
@ -34,7 +35,7 @@ class Film(models.Model):
class DoeReporterManager(models.Manager):
def get_queryset(self):
return super(DoeReporterManager, self).get_queryset().filter(last_name="Doe")
return super().get_queryset().filter(last_name="Doe")
class Reporter(models.Model):
@ -54,7 +55,7 @@ class Reporter(models.Model):
)
def __str__(self): # __unicode__ on Python 2
return "%s %s" % (self.first_name, self.last_name)
return f"{self.first_name} {self.last_name}"
def __init__(self, *args, **kwargs):
"""
@ -64,7 +65,7 @@ class Reporter(models.Model):
when a CNNReporter is pulled from the database, it is still
of type Reporter. This was added to test proxy model support.
"""
super(Reporter, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
if self.reporter_type == 2: # quick and dirty way without enums
self.__class__ = CNNReporter
@ -74,7 +75,7 @@ class Reporter(models.Model):
class CNNReporterManager(models.Manager):
def get_queryset(self):
return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2)
return super().get_queryset().filter(reporter_type=2)
class CNNReporter(Reporter):

View File

@ -2,7 +2,7 @@ from textwrap import dedent
from django.core import management
from io import StringIO
from mock import mock_open, patch
from unittest.mock import mock_open, patch
from graphene import ObjectType, Schema, String
@ -53,6 +53,5 @@ def test_generate_graphql_file_on_call_graphql_schema():
"""\
type Query {
hi: String
}
"""
}"""
)

View File

@ -3,13 +3,14 @@ from collections import namedtuple
import pytest
from django.db import models
from django.utils.translation import gettext_lazy as _
from py.test import raises
from pytest import raises
import graphene
from graphene import NonNull
from graphene.relay import ConnectionField, Node
from graphene.types.datetime import Date, DateTime, Time
from graphene.types.json import JSONString
from graphene.types.scalars import BigInt
from ..compat import (
ArrayField,
@ -140,8 +141,8 @@ def test_should_small_integer_convert_int():
assert_conversion(models.SmallIntegerField, graphene.Int)
def test_should_big_integer_convert_int():
assert_conversion(models.BigIntegerField, graphene.Int)
def test_should_big_integer_convert_big_int():
assert_conversion(models.BigIntegerField, BigInt)
def test_should_integer_convert_int():

View File

@ -1,5 +1,5 @@
from django.core.exceptions import ValidationError
from py.test import raises
from pytest import raises
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField

View File

@ -0,0 +1,361 @@
import pytest
import graphene
from graphene.relay import Node
from graphql_relay import to_global_id
from ..fields import DjangoConnectionField
from ..types import DjangoObjectType
from .models import Article, Reporter
class TestShouldCallGetQuerySetOnForeignKey:
"""
Check that the get_queryset method is called in both forward and reversed direction
of a foreignkey on types.
(see issue #1111)
"""
@pytest.fixture(autouse=True)
def setup_schema(self):
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
@classmethod
def get_queryset(cls, queryset, info):
if info.context and info.context.get("admin"):
return queryset
raise Exception("Not authorized to access reporters.")
class ArticleType(DjangoObjectType):
class Meta:
model = Article
@classmethod
def get_queryset(cls, queryset, info):
return queryset.exclude(headline__startswith="Draft")
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType, id=graphene.ID(required=True))
article = graphene.Field(ArticleType, id=graphene.ID(required=True))
def resolve_reporter(self, info, id):
return (
ReporterType.get_queryset(Reporter.objects, info)
.filter(id=id)
.last()
)
def resolve_article(self, info, id):
return (
ArticleType.get_queryset(Article.objects, info).filter(id=id).last()
)
self.schema = graphene.Schema(query=Query)
self.reporter = Reporter.objects.create(first_name="Jane", last_name="Doe")
self.articles = [
Article.objects.create(
headline="A fantastic article",
reporter=self.reporter,
editor=self.reporter,
),
Article.objects.create(
headline="Draft: My next best seller",
reporter=self.reporter,
editor=self.reporter,
),
]
def test_get_queryset_called_on_field(self):
# If a user tries to access an article it is fine as long as it's not a draft one
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
}
}
"""
# Non-draft
result = self.schema.execute(query, variables={"id": self.articles[0].id})
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
}
# Draft
result = self.schema.execute(query, variables={"id": self.articles[1].id})
assert not result.errors
assert result.data["article"] is None
# If a non admin user tries to access a reporter they should get our authorization error
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
}
}
"""
result = self.schema.execute(query, variables={"id": self.reporter.id})
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
}
}
"""
result = self.schema.execute(
query,
variables={"id": self.reporter.id},
context_value={"admin": True},
)
assert not result.errors
assert result.data == {"reporter": {"firstName": "Jane"}}
def test_get_queryset_called_on_foreignkey(self):
# If a user tries to access a reporter through an article they should get our authorization error
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(query, variables={"id": self.articles[0].id})
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters through an article
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": self.articles[0].id},
context_value={"admin": True},
)
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
"reporter": {"firstName": "Jane"},
}
# An admin user should not be able to access draft article through a reporter
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
articles {
headline
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": self.reporter.id},
context_value={"admin": True},
)
assert not result.errors
assert result.data["reporter"] == {
"firstName": "Jane",
"articles": [{"headline": "A fantastic article"}],
}
class TestShouldCallGetQuerySetOnForeignKeyNode:
"""
Check that the get_queryset method is called in both forward and reversed direction
of a foreignkey on types using a node interface.
(see issue #1111)
"""
@pytest.fixture(autouse=True)
def setup_schema(self):
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
@classmethod
def get_queryset(cls, queryset, info):
if info.context and info.context.get("admin"):
return queryset
raise Exception("Not authorized to access reporters.")
class ArticleType(DjangoObjectType):
class Meta:
model = Article
interfaces = (Node,)
@classmethod
def get_queryset(cls, queryset, info):
return queryset.exclude(headline__startswith="Draft")
class Query(graphene.ObjectType):
reporter = Node.Field(ReporterType)
article = Node.Field(ArticleType)
self.schema = graphene.Schema(query=Query)
self.reporter = Reporter.objects.create(first_name="Jane", last_name="Doe")
self.articles = [
Article.objects.create(
headline="A fantastic article",
reporter=self.reporter,
editor=self.reporter,
),
Article.objects.create(
headline="Draft: My next best seller",
reporter=self.reporter,
editor=self.reporter,
),
]
def test_get_queryset_called_on_node(self):
# If a user tries to access an article it is fine as long as it's not a draft one
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
}
}
"""
# Non-draft
result = self.schema.execute(
query, variables={"id": to_global_id("ArticleType", self.articles[0].id)}
)
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
}
# Draft
result = self.schema.execute(
query, variables={"id": to_global_id("ArticleType", self.articles[1].id)}
)
assert not result.errors
assert result.data["article"] is None
# If a non admin user tries to access a reporter they should get our authorization error
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
}
}
"""
result = self.schema.execute(
query, variables={"id": to_global_id("ReporterType", self.reporter.id)}
)
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
}
}
"""
result = self.schema.execute(
query,
variables={"id": to_global_id("ReporterType", self.reporter.id)},
context_value={"admin": True},
)
assert not result.errors
assert result.data == {"reporter": {"firstName": "Jane"}}
def test_get_queryset_called_on_foreignkey(self):
# If a user tries to access a reporter through an article they should get our authorization error
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(
query, variables={"id": to_global_id("ArticleType", self.articles[0].id)}
)
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters through an article
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": to_global_id("ArticleType", self.articles[0].id)},
context_value={"admin": True},
)
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
"reporter": {"firstName": "Jane"},
}
# An admin user should not be able to access draft article through a reporter
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
articles {
edges {
node {
headline
}
}
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": to_global_id("ReporterType", self.reporter.id)},
context_value={"admin": True},
)
assert not result.errors
assert result.data["reporter"] == {
"firstName": "Jane",
"articles": {"edges": [{"node": {"headline": "A fantastic article"}}]},
}

View File

@ -6,7 +6,7 @@ from django.db import models
from django.db.models import Q
from django.utils.functional import SimpleLazyObject
from graphql_relay import to_global_id
from py.test import raises
from pytest import raises
import graphene
from graphene.relay import Node
@ -15,7 +15,7 @@ from ..compat import IntegerRangeField, MissingType
from ..fields import DjangoConnectionField
from ..types import DjangoObjectType
from ..utils import DJANGO_FILTER_INSTALLED
from .models import Article, CNNReporter, Film, FilmDetails, Reporter
from .models import Article, CNNReporter, Film, FilmDetails, Person, Pet, Reporter
def test_should_query_only_fields():
@ -251,8 +251,8 @@ def test_should_node():
def test_should_query_onetoone_fields():
film = Film(id=1)
film_details = FilmDetails(id=1, film=film)
film = Film.objects.create(id=1)
film_details = FilmDetails.objects.create(id=1, film=film)
class FilmNode(DjangoObjectType):
class Meta:
@ -1151,9 +1151,9 @@ def test_connection_should_limit_after_to_list_length():
REPORTERS = [
dict(
first_name="First {}".format(i),
last_name="Last {}".format(i),
email="johndoe+{}@example.com".format(i),
first_name=f"First {i}",
last_name=f"Last {i}",
email=f"johndoe+{i}@example.com",
a_choice=1,
)
for i in range(6)
@ -1243,6 +1243,7 @@ def test_should_have_next_page(graphene_settings):
}
@pytest.mark.parametrize("max_limit", [100, 4])
class TestBackwardPagination:
def setup_schema(self, graphene_settings, max_limit):
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit
@ -1261,8 +1262,8 @@ class TestBackwardPagination:
schema = graphene.Schema(query=Query)
return schema
def do_queries(self, schema):
# Simply last 3
def test_query_last(self, graphene_settings, max_limit):
schema = self.setup_schema(graphene_settings, max_limit=max_limit)
query_last = """
query {
allReporters(last: 3) {
@ -1282,7 +1283,8 @@ class TestBackwardPagination:
e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
] == ["First 3", "First 4", "First 5"]
# Use a combination of first and last
def test_query_first_and_last(self, graphene_settings, max_limit):
schema = self.setup_schema(graphene_settings, max_limit=max_limit)
query_first_and_last = """
query {
allReporters(first: 4, last: 3) {
@ -1302,7 +1304,8 @@ class TestBackwardPagination:
e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
] == ["First 1", "First 2", "First 3"]
# Use a combination of first and last and after
def test_query_first_last_and_after(self, graphene_settings, max_limit):
schema = self.setup_schema(graphene_settings, max_limit=max_limit)
query_first_last_and_after = """
query queryAfter($after: String) {
allReporters(first: 4, last: 3, after: $after) {
@ -1317,7 +1320,8 @@ class TestBackwardPagination:
after = base64.b64encode(b"arrayconnection:0").decode()
result = schema.execute(
query_first_last_and_after, variable_values=dict(after=after)
query_first_last_and_after,
variable_values=dict(after=after),
)
assert not result.errors
assert len(result.data["allReporters"]["edges"]) == 3
@ -1325,20 +1329,35 @@ class TestBackwardPagination:
e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
] == ["First 2", "First 3", "First 4"]
def test_should_query(self, graphene_settings):
def test_query_last_and_before(self, graphene_settings, max_limit):
schema = self.setup_schema(graphene_settings, max_limit=max_limit)
query_first_last_and_after = """
query queryAfter($before: String) {
allReporters(last: 1, before: $before) {
edges {
node {
firstName
}
}
}
}
"""
Backward pagination should work as expected
"""
schema = self.setup_schema(graphene_settings, max_limit=100)
self.do_queries(schema)
def test_should_query_with_low_max_limit(self, graphene_settings):
"""
When doing backward pagination (using last) in combination with a max limit higher than the number of objects
we should really retrieve the last ones.
"""
schema = self.setup_schema(graphene_settings, max_limit=4)
self.do_queries(schema)
result = schema.execute(
query_first_last_and_after,
)
assert not result.errors
assert len(result.data["allReporters"]["edges"]) == 1
assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5"
before = base64.b64encode(b"arrayconnection:5").decode()
result = schema.execute(
query_first_last_and_after,
variable_values=dict(before=before),
)
assert not result.errors
assert len(result.data["allReporters"]["edges"]) == 1
assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 4"
def test_should_preserve_prefetch_related(django_assert_num_queries):
@ -1480,7 +1499,11 @@ def test_connection_should_enable_offset_filtering():
result = schema.execute(query)
assert not result.errors
expected = {
"allReporters": {"edges": [{"node": {"firstName": "Some", "lastName": "Guy"}},]}
"allReporters": {
"edges": [
{"node": {"firstName": "Some", "lastName": "Guy"}},
]
}
}
assert result.data == expected
@ -1521,7 +1544,9 @@ def test_connection_should_enable_offset_filtering_higher_than_max_limit(
assert not result.errors
expected = {
"allReporters": {
"edges": [{"node": {"firstName": "Some", "lastName": "Lady"}},]
"edges": [
{"node": {"firstName": "Some", "lastName": "Lady"}},
]
}
}
assert result.data == expected
@ -1590,6 +1615,149 @@ def test_connection_should_allow_offset_filtering_with_after():
result = schema.execute(query, variable_values=dict(after=after))
assert not result.errors
expected = {
"allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]}
"allReporters": {
"edges": [
{"node": {"firstName": "Jane", "lastName": "Roe"}},
]
}
}
assert result.data == expected
def test_connection_should_succeed_if_last_higher_than_number_of_objects():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
fields = "__all__"
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
schema = graphene.Schema(query=Query)
query = """
query ReporterPromiseConnectionQuery ($last: Int) {
allReporters(last: $last) {
edges {
node {
firstName
lastName
}
}
}
}
"""
result = schema.execute(query, variable_values=dict(last=2))
assert not result.errors
expected = {"allReporters": {"edges": []}}
assert result.data == expected
Reporter.objects.create(first_name="John", last_name="Doe")
Reporter.objects.create(first_name="Some", last_name="Guy")
Reporter.objects.create(first_name="Jane", last_name="Roe")
Reporter.objects.create(first_name="Some", last_name="Lady")
result = schema.execute(query, variable_values=dict(last=2))
assert not result.errors
expected = {
"allReporters": {
"edges": [
{"node": {"firstName": "Jane", "lastName": "Roe"}},
{"node": {"firstName": "Some", "lastName": "Lady"}},
]
}
}
assert result.data == expected
result = schema.execute(query, variable_values=dict(last=4))
assert not result.errors
expected = {
"allReporters": {
"edges": [
{"node": {"firstName": "John", "lastName": "Doe"}},
{"node": {"firstName": "Some", "lastName": "Guy"}},
{"node": {"firstName": "Jane", "lastName": "Roe"}},
{"node": {"firstName": "Some", "lastName": "Lady"}},
]
}
}
assert result.data == expected
result = schema.execute(query, variable_values=dict(last=20))
assert not result.errors
expected = {
"allReporters": {
"edges": [
{"node": {"firstName": "John", "lastName": "Doe"}},
{"node": {"firstName": "Some", "lastName": "Guy"}},
{"node": {"firstName": "Jane", "lastName": "Roe"}},
{"node": {"firstName": "Some", "lastName": "Lady"}},
]
}
}
assert result.data == expected
def test_should_query_nullable_foreign_key():
class PetType(DjangoObjectType):
class Meta:
model = Pet
class PersonType(DjangoObjectType):
class Meta:
model = Person
class Query(graphene.ObjectType):
pet = graphene.Field(PetType, name=graphene.String(required=True))
person = graphene.Field(PersonType, name=graphene.String(required=True))
def resolve_pet(self, info, name):
return Pet.objects.filter(name=name).first()
def resolve_person(self, info, name):
return Person.objects.filter(name=name).first()
schema = graphene.Schema(query=Query)
person = Person.objects.create(name="Jane")
pets = [
Pet.objects.create(name="Stray dog", age=1),
Pet.objects.create(name="Jane's dog", owner=person, age=1),
]
query_pet = """
query getPet($name: String!) {
pet(name: $name) {
owner {
name
}
}
}
"""
result = schema.execute(query_pet, variables={"name": "Stray dog"})
assert not result.errors
assert result.data["pet"] == {
"owner": None,
}
result = schema.execute(query_pet, variables={"name": "Jane's dog"})
assert not result.errors
assert result.data["pet"] == {
"owner": {"name": "Jane"},
}
query_owner = """
query getOwner($name: String!) {
person(name: $name) {
pets {
name
}
}
}
"""
result = schema.execute(query_owner, variables={"name": "Jane"})
assert not result.errors
assert result.data["person"] == {
"pets": [{"name": "Jane's dog"}],
}

View File

@ -1,4 +1,4 @@
from py.test import raises
from pytest import raises
from ..registry import Registry
from ..types import DjangoObjectType

View File

@ -3,7 +3,7 @@ from textwrap import dedent
import pytest
from django.db import models
from mock import patch
from unittest.mock import patch
from graphene import Connection, Field, Interface, ObjectType, Schema, String
from graphene.relay import Node
@ -104,7 +104,7 @@ def test_django_objecttype_with_custom_meta():
@classmethod
def __init_subclass_with_meta__(cls, **options):
options.setdefault("_meta", ArticleTypeOptions(cls))
super(ArticleType, cls).__init_subclass_with_meta__(**options)
super().__init_subclass_with_meta__(**options)
class Article(ArticleType):
class Meta:
@ -183,7 +183,7 @@ def test_schema_representation():
pets: [Reporter!]!
aChoice: TestsReporterAChoiceChoices
reporterType: TestsReporterReporterTypeChoices
articles(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null): ArticleConnection!
articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
}
\"""An enumeration.\"""
@ -244,8 +244,7 @@ def test_schema_representation():
\"""The ID of the object\"""
id: ID!
): Node
}
"""
}"""
)
assert str(schema) == expected
@ -485,7 +484,7 @@ def test_django_objecttype_neither_fields_nor_exclude():
def custom_enum_name(field):
return "CustomEnum{}".format(field.name.title())
return f"CustomEnum{field.name.title()}"
class TestDjangoObjectType:
@ -525,8 +524,7 @@ class TestDjangoObjectType:
id: ID!
kind: String!
cuteness: Int!
}
"""
}"""
)
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
@ -560,8 +558,7 @@ class TestDjangoObjectType:
\"""Dog\"""
DOG
}
"""
}"""
)
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
@ -586,8 +583,7 @@ class TestDjangoObjectType:
id: ID!
kind: String!
cuteness: Int!
}
"""
}"""
)
def test_django_objecttype_convert_choices_enum_naming_collisions(
@ -621,8 +617,7 @@ class TestDjangoObjectType:
\"""Dog\"""
DOG
}
"""
}"""
)
def test_django_objecttype_choices_custom_enum_name(
@ -660,8 +655,7 @@ class TestDjangoObjectType:
\"""Dog\"""
DOG
}
"""
}"""
)

View File

@ -2,7 +2,7 @@ import json
import pytest
from django.utils.translation import gettext_lazy
from mock import patch
from unittest.mock import patch
from ..utils import camelize, get_model_fields, GraphQLTestCase
from .models import Film, Reporter
@ -11,11 +11,11 @@ from ..utils.testing import graphql_query
def test_get_model_fields_no_duplication():
reporter_fields = get_model_fields(Reporter)
reporter_name_set = set([field[0] for field in reporter_fields])
reporter_name_set = {field[0] for field in reporter_fields}
assert len(reporter_fields) == len(reporter_name_set)
film_fields = get_model_fields(Film)
film_name_set = set([field[0] for field in film_fields])
film_name_set = {field[0] for field in film_fields}
assert len(film_fields) == len(film_name_set)
@ -54,7 +54,7 @@ def test_graphql_test_case_operation_name(post_mock):
tc._pre_setup()
tc.setUpClass()
tc.query("query { }", operation_name="QueryName")
body = json.loads(post_mock.call_args.args[1])
body = json.loads(post_mock.call_args[0][1])
# `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
assert (
"operationName",
@ -66,7 +66,7 @@ def test_graphql_test_case_operation_name(post_mock):
@patch("graphene_django.utils.testing.Client.post")
def test_graphql_query_case_operation_name(post_mock):
graphql_query("query { }", operation_name="QueryName")
body = json.loads(post_mock.call_args.args[1])
body = json.loads(post_mock.call_args[0][1])
# `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
assert (
"operationName",

View File

@ -2,7 +2,7 @@ import json
import pytest
from mock import patch
from unittest.mock import patch
from django.db import connection
@ -109,12 +109,10 @@ def test_reports_validation_errors(client):
{
"message": "Cannot query field 'unknownOne' on type 'QueryRoot'.",
"locations": [{"line": 1, "column": 9}],
"path": None,
},
{
"message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.",
"locations": [{"line": 1, "column": 21}],
"path": None,
},
]
}
@ -135,8 +133,6 @@ def test_errors_when_missing_operation_name(client):
"errors": [
{
"message": "Must provide operation name if query contains multiple operations.",
"locations": None,
"path": None,
}
]
}
@ -477,7 +473,6 @@ def test_handles_syntax_errors_caught_by_graphql(client):
{
"locations": [{"column": 1, "line": 1}],
"message": "Syntax Error: Unexpected Name 'syntaxerror'.",
"path": None,
}
]
}
@ -512,7 +507,7 @@ def test_handles_invalid_json_bodies(client):
def test_handles_django_request_error(client, monkeypatch):
def mocked_read(*args):
raise IOError("foo-bar")
raise OSError("foo-bar")
monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read)

View File

@ -1,8 +1,8 @@
from django.conf.urls import url
from django.urls import path
from ..views import GraphQLView
urlpatterns = [
url(r"^graphql/batch", GraphQLView.as_view(batch=True)),
url(r"^graphql", GraphQLView.as_view(graphiql=True)),
path("graphql/batch", GraphQLView.as_view(batch=True)),
path("graphql", GraphQLView.as_view(graphiql=True)),
]

View File

@ -1,4 +1,4 @@
from django.conf.urls import url
from django.urls import path
from ..views import GraphQLView
from .schema_view import schema
@ -10,4 +10,4 @@ class CustomGraphQLView(GraphQLView):
pretty = True
urlpatterns = [url(r"^graphql/inherited/$", CustomGraphQLView.as_view())]
urlpatterns = [path("graphql/inherited/", CustomGraphQLView.as_view())]

View File

@ -1,6 +1,6 @@
from django.conf.urls import url
from django.urls import path
from ..views import GraphQLView
from .schema_view import schema
urlpatterns = [url(r"^graphql", GraphQLView.as_view(schema=schema, pretty=True))]
urlpatterns = [path("graphql", GraphQLView.as_view(schema=schema, pretty=True))]

View File

@ -122,7 +122,7 @@ def validate_fields(type_, model, fields, only_fields, exclude_fields):
class DjangoObjectTypeOptions(ObjectTypeOptions):
model = None # type: Model
model = None # type: Type[Model]
registry = None # type: Registry
connection = None # type: Type[Connection]
@ -168,11 +168,9 @@ class DjangoObjectType(ObjectType):
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"
)
)
assert not (fields and exclude), (
"Cannot set both 'fields' and 'exclude' options on "
@ -216,7 +214,7 @@ class DjangoObjectType(ObjectType):
"Creating a DjangoObjectType without either the `fields` "
"or the `exclude` option is deprecated. Add an explicit `fields "
"= '__all__'` option on DjangoObjectType {class_name} to use all "
"fields".format(class_name=cls.__name__,),
"fields".format(class_name=cls.__name__),
DeprecationWarning,
stacklevel=2,
)
@ -228,7 +226,7 @@ class DjangoObjectType(ObjectType):
if use_connection is None and interfaces:
use_connection = any(
(issubclass(interface, Node) for interface in interfaces)
issubclass(interface, Node) for interface in interfaces
)
if use_connection and not connection:
@ -255,7 +253,7 @@ class DjangoObjectType(ObjectType):
_meta.fields = django_fields
_meta.connection = connection
super(DjangoObjectType, cls).__init_subclass_with_meta__(
super().__init_subclass_with_meta__(
_meta=_meta, interfaces=interfaces, **options
)

View File

@ -3,7 +3,9 @@ import warnings
from django.test import Client, TestCase, TransactionTestCase
DEFAULT_GRAPHQL_URL = "/graphql/"
from graphene_django.settings import graphene_settings
DEFAULT_GRAPHQL_URL = "/graphql"
def graphql_query(
@ -19,7 +21,7 @@ def graphql_query(
Args:
query (string) - GraphQL query to run
operation_name (string) - If the query is a mutation or named query, you must
supply the op_name. For annon queries ("{ ... }"),
supply the operation_name. For annon queries ("{ ... }"),
should be None (default).
input_data (dict) - If provided, the $input variable in GraphQL will be set
to this value. If both ``input_data`` and ``variables``,
@ -40,7 +42,7 @@ def graphql_query(
if client is None:
client = Client()
if not graphql_url:
graphql_url = DEFAULT_GRAPHQL_URL
graphql_url = graphene_settings.TESTING_ENDPOINT
body = {"query": query}
if operation_name:
@ -63,13 +65,13 @@ def graphql_query(
return resp
class GraphQLTestMixin(object):
class GraphQLTestMixin:
"""
Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/
"""
# URL to graphql endpoint
GRAPHQL_URL = DEFAULT_GRAPHQL_URL
GRAPHQL_URL = graphene_settings.TESTING_ENDPOINT
def query(
self, query, operation_name=None, input_data=None, variables=None, headers=None
@ -78,7 +80,7 @@ class GraphQLTestMixin(object):
Args:
query (string) - GraphQL query to run
operation_name (string) - If the query is a mutation or named query, you must
supply the op_name. For annon queries ("{ ... }"),
supply the operation_name. For annon queries ("{ ... }"),
should be None (default).
input_data (dict) - If provided, the $input variable in GraphQL will be set
to this value. If both ``input_data`` and ``variables``,

View File

@ -6,4 +6,4 @@ def test_to_const():
def test_to_const_unicode():
assert to_const(u"Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF"
assert to_const("Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF"

View File

@ -2,6 +2,7 @@ import pytest
from .. import GraphQLTestCase
from ...tests.test_types import with_local_registry
from ...settings import graphene_settings
from django.test import Client
@ -43,3 +44,11 @@ def test_graphql_test_case_deprecated_client_setter():
with pytest.warns(PendingDeprecationWarning):
tc._client = Client()
def test_graphql_test_case_imports_endpoint():
"""
GraphQLTestCase class should import the default endpoint from settings file
"""
assert GraphQLTestCase.GRAPHQL_URL == graphene_settings.TESTING_ENDPOINT

View File

@ -11,7 +11,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View
from graphql import OperationType, get_operation_ast, parse, validate
from graphql.error import GraphQLError
from graphql.error import format_error as format_graphql_error
from graphql.execution import ExecutionResult
from graphene import Schema
@ -27,7 +26,7 @@ class HttpError(Exception):
def __init__(self, response, message=None, *args, **kwargs):
self.response = response
self.message = message = message or response.content.decode()
super(HttpError, self).__init__(message, *args, **kwargs)
super().__init__(message, *args, **kwargs)
def get_accepted_content_types(request):
@ -67,9 +66,9 @@ class GraphQLView(View):
react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0="
# The GraphiQL React app.
graphiql_version = "1.4.1" # "1.0.3"
graphiql_sri = "sha256-JUMkXBQWZMfJ7fGEsTXalxVA10lzKOS9loXdLjwZKi4=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
graphiql_css_sri = "sha256-Md3vdR7PDzWyo/aGfsFVF4tvS5/eAUWuIsg9QHUusCY=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
graphiql_version = "1.4.7" # "1.0.3"
graphiql_sri = "sha256-cpZ8w9D/i6XdEbY/Eu7yAXeYzReVw0mxYd7OU3gUcsc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
graphiql_css_sri = "sha256-HADQowUuFum02+Ckkv5Yu5ygRoLllHZqg0TFZXY7NHI=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
# The websocket transport library for subscriptions.
subscriptions_transport_ws_version = "0.9.18"
@ -163,6 +162,7 @@ class GraphQLView(View):
subscription_path=self.subscription_path,
# GraphiQL headers tab,
graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED,
graphiql_should_persist_headers=graphene_settings.GRAPHIQL_SHOULD_PERSIST_HEADERS,
)
if self.batch:
@ -387,7 +387,7 @@ class GraphQLView(View):
@staticmethod
def format_error(error):
if isinstance(error, GraphQLError):
return format_graphql_error(error)
return error.formatted
return {"message": str(error)}

View File

@ -5,7 +5,7 @@ test=pytest
universal=1
[flake8]
exclude = docs,graphene_django/debug/sql/*,migrations
exclude = docs,graphene_django/debug/sql/*
max-line-length = 120
select =
# Dictionary key repeated

View File

@ -14,22 +14,22 @@ rest_framework_require = ["djangorestframework>=3.6.3"]
tests_require = [
"pytest>=3.6.3",
"pytest>=7.1.3",
"pytest-cov",
"pytest-random-order",
"coveralls",
"mock",
"pytz",
"django-filter>=2",
"pytest-django>=3.3.2",
"django-filter>=22.1",
"pytest-django>=4.5.2",
] + rest_framework_require
dev_requires = [
"black==19.10b0",
"flake8==3.7.9",
"flake8-black==0.1.1",
"flake8-bugbear==20.1.4",
"black==22.8.0",
"flake8==5.0.4",
"flake8-black==0.3.3",
"flake8-bugbear==22.9.11",
] + tests_require
setup(
@ -46,23 +46,23 @@ setup(
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: Implementation :: PyPy",
"Framework :: Django",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
"Framework :: Django :: 3.1",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Framework :: Django :: 4.1",
],
keywords="api graphql protocol rest relay graphene",
packages=find_packages(exclude=["tests", "examples", "examples.*"]),
install_requires=[
"graphene>=3.0.0b5,<4",
"graphene>=3.0,<4",
"graphql-core>=3.1.0,<4",
"Django>=2.2",
"graphql-relay>=3.1.1,<4",
"Django>=3.2",
"promise>=2.1",
"text-unidecode",
],

35
tox.ini
View File

@ -1,21 +1,21 @@
[tox]
envlist =
py{36,37,38,39}-django{22,30,31,32,main},
black,flake8
py{37,38,39,310}-django32,
py{38,39,310}-django{40,41,main},
pre-commit
[gh-actions]
python =
3.6: py36
3.7: py37
3.8: py38
3.9: py39
3.10: py310
[gh-actions:env]
DJANGO =
2.2: django22
3.0: django30
3.1: django31
3.2: django32
4.0: django40
4.1: django41
main: djangomain
[testenv]
@ -26,23 +26,14 @@ setenv =
deps =
-e.[test]
psycopg2-binary
django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2
django22: Django>=2.2,<3.0
django30: Django>=3.0a1,<3.1
django31: Django>=3.1,<3.2
django32: Django>=3.2a1,<3.3
django32: Django>=3.2,<4.0
django40: Django>=4.0,<4.1
django41: Django>=4.1,<4.2
djangomain: https://github.com/django/django/archive/main.zip
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
[testenv:black]
basepython = python3.9
deps = -e.[dev]
[testenv:pre-commit]
skip_install = true
deps = pre-commit
commands =
black --exclude "/migrations/" graphene_django examples setup.py --check
[testenv:flake8]
basepython = python3.9
deps = -e.[dev]
commands =
flake8 graphene_django examples setup.py
pre-commit run --all-files --show-diff-on-failure