Merge branch 'main' into main

This commit is contained in:
Firas Kafri 2023-05-04 22:09:06 +03:00 committed by GitHub
commit eeaa2234b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 1773 additions and 635 deletions

View File

@ -10,17 +10,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python 3.9 - name: Set up Python 3.11
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: 3.9 python-version: '3.11'
- name: Build wheel and source tarball - name: Build wheel and source tarball
run: | run: |
pip install wheel pip install wheel
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@v1.1.0 uses: pypa/gh-action-pypi-publish@v1.8.6
with: with:
user: __token__ user: __token__
password: ${{ secrets.pypi_password }} password: ${{ secrets.pypi_password }}

View File

@ -7,16 +7,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python 3.9 - name: Set up Python 3.11
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: 3.9 python-version: '3.11'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox pip install tox
- name: Run lint 💅 - name: Run pre-commit 💅
run: tox run: tox
env: env:
TOXENV: flake8 TOXENV: pre-commit

View File

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

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

@ -0,0 +1,30 @@
default_language_version:
python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.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.3.2
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8

View File

@ -1,22 +1,22 @@
.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 .PHONY: dev-setup ## Install development dependencies
dev-setup: dev-setup:
pip install -e ".[dev]" pip install -e ".[dev]"
python -m pre_commit install
.PHONY: install-dev .PHONY: tests ## Run unit tests
install-dev: dev-setup # Alias install-dev -> dev-setup
.PHONY: tests
tests: tests:
py.test graphene_django --cov=graphene_django -vv py.test graphene_django --cov=graphene_django -vv
.PHONY: test .PHONY: format ## Format code
test: tests # Alias test -> tests
.PHONY: format
format: format:
black --exclude "/migrations/" graphene_django examples setup.py black graphene_django examples setup.py
.PHONY: lint .PHONY: lint ## Lint code
lint: lint:
flake8 graphene_django examples flake8 graphene_django examples

View File

@ -55,7 +55,7 @@ from graphene_django.views import GraphQLView
urlpatterns = [ 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 = [ urlpatterns = [
# some other urls # 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 .. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin

View File

@ -2,8 +2,8 @@ Filtering
========= =========
Graphene integrates with Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results. `django-filter <https://django-filter.readthedocs.io/en/stable/>`__ to provide filtering of results.
See the `usage documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__ See the `usage documentation <https://django-filter.readthedocs.io/en/stable/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``. for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``. 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 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 Filterable fields
----------------- -----------------
@ -34,7 +34,7 @@ Filterable fields
The ``filter_fields`` parameter is used to specify the fields which can The ``filter_fields`` parameter is used to specify the fields which can
be filtered upon. The value specified here is passed directly to be filtered upon. The value specified here is passed directly to
``django-filter``, so see the `filtering ``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 full details on the range of options available.
For example: For example:
@ -192,7 +192,7 @@ in unison with the ``filter_fields`` parameter:
all_animals = DjangoFilterConnectionField(AnimalNode) all_animals = DjangoFilterConnectionField(AnimalNode)
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__ 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 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 filters to be context-dependent. We could modify the ``AnimalFilter`` above to
pre-filter animals owned by the authenticated user (set in ``context.user``). 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: Results in the following GraphQL schema definition:
.. code:: .. code:: graphql
type Pet { type Pet {
id: ID! id: ID!
@ -178,7 +178,7 @@ You can disable this automatic conversion by setting
fields = ("id", "kind",) fields = ("id", "kind",)
convert_choices_to_enum = False convert_choices_to_enum = False
.. code:: .. code:: graphql
type Pet { type Pet {
id: ID! id: ID!
@ -313,7 +313,7 @@ Additionally, Resolvers will receive **any arguments declared in the field defin
bar=graphene.Int() 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. # 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() 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: You can now execute queries like:
.. code:: python .. code:: graphql
{ {
questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") { questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
@ -440,7 +440,7 @@ You can now execute queries like:
Which returns: Which returns:
.. code:: python .. code:: json
{ {
"data": { "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: class Meta:
model = Category model = Category
fields = '__all__' fields = "__all__"
class IngredientType(DjangoObjectType): class IngredientType(DjangoObjectType):
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = '__all__' fields = "__all__"
class Query(object): class Query:
category = graphene.Field(CategoryType, category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
id=graphene.Int(),
name=graphene.String())
all_categories = graphene.List(CategoryType) all_categories = graphene.List(CategoryType)
ingredient = graphene.Field(
ingredient = graphene.Field(IngredientType, IngredientType, id=graphene.Int(), name=graphene.String()
id=graphene.Int(), )
name=graphene.String())
all_ingredients = graphene.List(IngredientType) all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, info, **kwargs): def resolve_all_categories(self, info, **kwargs):
@ -36,8 +33,8 @@
return Ingredient.objects.all() return Ingredient.objects.all()
def resolve_category(self, info, **kwargs): def resolve_category(self, info, **kwargs):
id = kwargs.get('id') id = kwargs.get("id")
name = kwargs.get('name') name = kwargs.get("name")
if id is not None: if id is not None:
return Category.objects.get(pk=id) return Category.objects.get(pk=id)
@ -48,8 +45,8 @@
return None return None
def resolve_ingredient(self, info, **kwargs): def resolve_ingredient(self, info, **kwargs):
id = kwargs.get('id') id = kwargs.get("id")
name = kwargs.get('name') name = kwargs.get("name")
if id is not None: if id is not None:
return Ingredient.objects.get(pk=id) return Ingredient.objects.get(pk=id)

View File

@ -189,7 +189,7 @@ Default: ``None``
``GRAPHIQL_HEADER_EDITOR_ENABLED`` ``GRAPHIQL_HEADER_EDITOR_ENABLED``
--------------------- ----------------------------------
GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables. GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables.
@ -207,3 +207,36 @@ Default: ``True``
GRAPHENE = { GRAPHENE = {
'GRAPHIQL_HEADER_EDITOR_ENABLED': True, '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`. 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: Usage:
@ -27,7 +28,7 @@ Usage:
} }
} }
''', ''',
op_name='myModel' operation_name='myModel'
) )
content = json.loads(response.content) content = json.loads(response.content)
@ -48,7 +49,7 @@ Usage:
} }
} }
''', ''',
op_name='myModel', operation_name='myModel',
variables={'id': 1} variables={'id': 1}
) )
@ -72,7 +73,7 @@ Usage:
} }
} }
''', ''',
op_name='myMutation', operation_name='myMutation',
input_data={'my_field': 'foo', 'other_field': 'bar'} 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'} 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) content = json.loads(response.content)

View File

@ -35,6 +35,7 @@ Now sync your database for the first time:
.. code:: bash .. code:: bash
cd ..
python manage.py migrate python manage.py migrate
Let's create a few simple models... Let's create a few simple models...
@ -77,6 +78,18 @@ Add ingredients as INSTALLED_APPS:
"cookbook.ingredients", "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: 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, ) interfaces = (relay.Node, )
class Query(graphene.ObjectType): class Query(ObjectType):
category = relay.Node.Field(CategoryNode) category = relay.Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -14,7 +14,7 @@ whole Graphene repository:
```bash ```bash
# Get the example project code # Get the example project code
git clone https://github.com/graphql-python/graphene-django.git 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 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,33 +1,52 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:15 # Generated by Django 1.9 on 2015-12-04 18:15
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Category', name="Category",
fields=[ 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( migrations.CreateModel(
name='Ingredient', name="Ingredient",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100)), "id",
('notes', models.TextField()), models.AutoField(
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')), 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,20 +1,17 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 00:50 # Generated by Django 1.9 on 2016-11-04 00:50
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('ingredients', '0001_initial'), ("ingredients", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='ingredient', model_name="ingredient",
name='notes', name="notes",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
] ]

View File

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

View File

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

View File

@ -1,36 +1,69 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:20 # Generated by Django 1.9 on 2015-12-04 18:20
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('ingredients', '0001_initial'), ("ingredients", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Recipe', name="Recipe",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('title', models.CharField(max_length=100)), "id",
('instructions', models.TextField()), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("instructions", models.TextField()),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='RecipeIngredient', name="RecipeIngredient",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('amount', models.FloatField()), "id",
('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)), models.AutoField(
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')), auto_created=True,
('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')), 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,25 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 01:06 # Generated by Django 1.9 on 2016-11-04 01:06
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('recipes', '0001_initial'), ("recipes", "0001_initial"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name='recipeingredient', model_name="recipeingredient",
old_name='recipes', old_name="recipes",
new_name='recipe', new_name="recipe",
), ),
migrations.AlterField( migrations.AlterField(
model_name='recipeingredient', model_name="recipeingredient",
name='unit', 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), 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

@ -4,15 +4,22 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('recipes', '0002_auto_20161104_0106'), ("recipes", "0002_auto_20161104_0106"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='recipeingredient', model_name="recipeingredient",
name='unit', name="unit",
field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20), 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__" fields = "__all__"
class Query(object): class Query:
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
all_recipes = graphene.List(RecipeType) 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. 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 Now head on over to
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
and run some queries! 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) 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,33 +1,52 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:15 # Generated by Django 1.9 on 2015-12-04 18:15
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Category', name="Category",
fields=[ 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( migrations.CreateModel(
name='Ingredient', name="Ingredient",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100)), "id",
('notes', models.TextField()), models.AutoField(
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')), 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,20 +1,17 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 00:50 # Generated by Django 1.9 on 2016-11-04 00:50
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('ingredients', '0001_initial'), ("ingredients", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='ingredient', model_name="ingredient",
name='notes', name="notes",
field=models.TextField(blank=True, null=True), 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) category = Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -1,36 +1,69 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:20 # Generated by Django 1.9 on 2015-12-04 18:20
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('ingredients', '0001_initial'), ("ingredients", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Recipe', name="Recipe",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('title', models.CharField(max_length=100)), "id",
('instructions', models.TextField()), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("instructions", models.TextField()),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='RecipeIngredient', name="RecipeIngredient",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('amount', models.FloatField()), "id",
('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)), models.AutoField(
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')), auto_created=True,
('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')), 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,25 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 01:06 # Generated by Django 1.9 on 2016-11-04 01:06
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('recipes', '0001_initial'), ("recipes", "0001_initial"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name='recipeingredient', model_name="recipeingredient",
old_name='recipes', old_name="recipes",
new_name='recipe', new_name="recipe",
), ),
migrations.AlterField( migrations.AlterField(
model_name='recipeingredient', model_name="recipeingredient",
name='unit', 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), 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) recipe = Node.Field(RecipeNode)
all_recipes = DjangoFilterConnectionField(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 from django.db import models

View File

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

View File

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

View File

@ -24,8 +24,15 @@ from graphene import (
Decimal, Decimal,
) )
from graphene.types.json import JSONString from graphene.types.json import JSONString
from graphene.types.scalars import BigInt
from graphene.utils.str_converters import to_camel_case 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 graphql.pyutils import register_description
from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
@ -55,7 +62,7 @@ class BlankValueField(Field):
def convert_choice_name(name): def convert_choice_name(name):
name = to_const(force_str(name)) name = to_const(force_str(name))
try: try:
assert_valid_name(name) assert_name(name)
except GraphQLError: except GraphQLError:
name = "A_%s" % name name = "A_%s" % name
return name return name
@ -67,8 +74,7 @@ def get_choices(choices):
choices = choices.items() choices = choices.items()
for value, help_text in choices: for value, help_text in choices:
if isinstance(help_text, (tuple, list)): if isinstance(help_text, (tuple, list)):
for choice in get_choices(help_text): yield from get_choices(help_text)
yield choice
else: else:
name = convert_choice_name(value) name = convert_choice_name(value)
while name in converted_names: while name in converted_names:
@ -85,12 +91,17 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
named_choices = [(c[0], c[1]) for c in choices] named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]: c[2] for c in choices} named_choices_descriptions = {c[0]: c[2] for c in choices}
class EnumWithDescriptionsType(object): class EnumWithDescriptionsType:
@property @property
def description(self): def description(self):
return str(named_choices_descriptions[self.name]) return str(named_choices_descriptions[self.name])
return_type = Enum(name, list(named_choices), type=EnumWithDescriptionsType) return_type = Enum(
name,
list(named_choices),
type=EnumWithDescriptionsType,
description="An enumeration.", # Temporary fix until https://github.com/graphql-python/graphene/pull/1502 is merged
)
return return_type return return_type
@ -102,7 +113,7 @@ def generate_enum_name(django_model_meta, field):
) )
name = custom_func(field) name = custom_func(field)
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True: 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: else:
name = "{app_label}{object_name}{field_name}Choices".format( name = "{app_label}{object_name}{field_name}Choices".format(
app_label=to_camel_case(django_model_meta.app_label.title()), app_label=to_camel_case(django_model_meta.app_label.title()),
@ -148,7 +159,9 @@ def get_django_field_description(field):
@singledispatch @singledispatch
def convert_django_field(field, registry=None): def convert_django_field(field, registry=None):
raise Exception( 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 +199,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.PositiveIntegerField)
@convert_django_field.register(models.PositiveSmallIntegerField) @convert_django_field.register(models.PositiveSmallIntegerField)
@convert_django_field.register(models.SmallIntegerField) @convert_django_field.register(models.SmallIntegerField)
@convert_django_field.register(models.BigIntegerField)
@convert_django_field.register(models.IntegerField) @convert_django_field.register(models.IntegerField)
def convert_field_to_int(field, registry=None): def convert_field_to_int(field, registry=None):
return Int(description=get_django_field_description(field), required=not field.null) return Int(description=get_django_field_description(field), required=not field.null)
@ -205,7 +222,9 @@ def convert_field_to_boolean(field, registry=None):
@convert_django_field.register(models.DecimalField) @convert_django_field.register(models.DecimalField)
def convert_field_to_decimal(field, registry=None): 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) @convert_django_field.register(models.FloatField)

View File

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

View File

@ -7,34 +7,34 @@ from .exception.formating import wrap_exception
from .types import DjangoDebug from .types import DjangoDebug
class DjangoDebugContext(object): class DjangoDebugContext:
def __init__(self): def __init__(self):
self.debug_promise = None self.debug_result = None
self.promises = [] self.results = []
self.object = DjangoDebug(sql=[], exceptions=[]) self.object = DjangoDebug(sql=[], exceptions=[])
self.enable_instrumentation() self.enable_instrumentation()
def get_debug_promise(self): def get_debug_result(self):
if not self.debug_promise: if not self.debug_result:
self.debug_promise = Promise.all(self.promises) self.debug_result = self.results
self.promises = [] self.results = []
return self.debug_promise.then(self.on_resolve_all_promises).get() return self.on_resolve_all_results()
def on_resolve_error(self, value): def on_resolve_error(self, value):
if hasattr(self, "object"): if hasattr(self, "object"):
self.object.exceptions.append(wrap_exception(value)) self.object.exceptions.append(wrap_exception(value))
return Promise.reject(value) return value
def on_resolve_all_promises(self, values): def on_resolve_all_results(self):
if self.promises: if self.results:
self.debug_promise = None self.debug_result = None
return self.get_debug_promise() return self.get_debug_result()
self.disable_instrumentation() self.disable_instrumentation()
return self.object return self.object
def add_promise(self, promise): def add_result(self, result):
if self.debug_promise: if self.debug_result:
self.promises.append(promise) self.results.append(result)
def enable_instrumentation(self): def enable_instrumentation(self):
# This is thread-safe because database connections are thread-local. # This is thread-safe because database connections are thread-local.
@ -46,7 +46,7 @@ class DjangoDebugContext(object):
unwrap_cursor(connection) unwrap_cursor(connection)
class DjangoDebugMiddleware(object): class DjangoDebugMiddleware:
def resolve(self, next, root, info, **args): def resolve(self, next, root, info, **args):
context = info.context context = info.context
django_debug = getattr(context, "django_debug", None) django_debug = getattr(context, "django_debug", None)
@ -62,10 +62,10 @@ class DjangoDebugMiddleware(object):
) )
) )
if info.schema.get_type("DjangoDebug") == info.return_type: if info.schema.get_type("DjangoDebug") == info.return_type:
return context.django_debug.get_debug_promise() return context.django_debug.get_debug_result()
try: try:
promise = next(root, info, **args) result = next(root, info, **args)
except Exception as e: except Exception as e:
return context.django_debug.on_resolve_error(e) return context.django_debug.on_resolve_error(e)
context.django_debug.add_promise(promise) context.django_debug.add_result(result)
return promise return result

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,6 @@ class ArrayFilter(TypedFilter):
return qs return qs
if self.distinct: if self.distinct:
qs = qs.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}) qs = self.get_method(qs)(**{lookup: value})
return qs return qs

View File

@ -13,11 +13,11 @@ class GlobalIDFilter(Filter):
field_class = GlobalIDFormField field_class = GlobalIDFormField
def filter(self, qs, value): 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 _id = None
if value is not None: if value is not None:
_, _id = from_global_id(value) _, _id = from_global_id(value)
return super(GlobalIDFilter, self).filter(qs, _id) return super().filter(qs, _id)
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
@ -25,4 +25,4 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
def filter(self, qs, value): def filter(self, qs, value):
gids = [from_global_id(v)[1] for v in 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: else:
return qs.none() return qs.none()
else: 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): def __init__(self, input_type=None, *args, **kwargs):
self._input_type = input_type self._input_type = input_type
super(TypedFilter, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@property @property
def input_type(self): def input_type(self):

View File

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

View File

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

View File

@ -120,10 +120,7 @@ def test_array_field_filter_schema_type(Query):
"randomField": "[Boolean!]", "randomField": "[Boolean!]",
} }
filters_str = ", ".join( filters_str = ", ".join(
[ [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()]
f"{filter_field}: {gql_type} = null"
for filter_field, gql_type in filters.items()
]
) )
assert ( assert (
f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str 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 first_name="Jane", last_name="Doe", email="janedoe@example.com", a_choice=2
) )
Article.objects.create( 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( 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( 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) result = schema.execute(query)
assert not result.errors assert not result.errors
@ -152,9 +158,6 @@ def test_filter_enum_field_schema_type(schema):
"reporter_AChoice_In": "[TestsReporterAChoiceChoices]", "reporter_AChoice_In": "[TestsReporterAChoiceChoices]",
} }
filters_str = ", ".join( filters_str = ", ".join(
[ [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()]
f"{filter_field}: {gql_type} = null"
for filter_field, gql_type in filters.items()
]
) )
assert f" allArticles({filters_str}): ArticleTypeConnection\n" in schema_str 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 import TextField, Value
from django.db.models.functions import Concat 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.relay import Node
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField 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("_")] actual = [name for name in args if name not in ignore and not name.startswith("_")]
assert set(arguments) == set( assert set(arguments) == set(
actual actual
), "Expected arguments ({}) did not match actual ({})".format(arguments, actual) ), f"Expected arguments ({arguments}) did not match actual ({actual})"
def assert_orderable(field): def assert_orderable(field):
@ -141,7 +141,7 @@ def test_filter_shortcut_filterset_context():
@property @property
def qs(self): def qs(self):
qs = super(ArticleContextFilter, self).qs qs = super().qs
return qs.filter(reporter=self.request.reporter) return qs.filter(reporter=self.request.reporter)
class Query(ObjectType): class Query(ObjectType):
@ -166,7 +166,7 @@ def test_filter_shortcut_filterset_context():
editor=r2, editor=r2,
) )
class context(object): class context:
reporter = r2 reporter = r2
query = """ query = """
@ -401,7 +401,7 @@ def test_filterset_descriptions():
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter) field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter)
max_time = field.args["max_time"] max_time = field.args["max_time"]
assert isinstance(max_time, Argument) assert isinstance(max_time, Argument)
assert max_time.type == Float assert max_time.type == Decimal
assert max_time.description == "The maximum time" assert max_time.description == "The maximum time"
@ -1008,7 +1008,7 @@ def test_integer_field_filter_type():
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
type Query { 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 { type PetTypeConnection {
@ -1056,8 +1056,7 @@ def test_integer_field_filter_type():
interface Node { interface Node {
\"""The ID of the object\""" \"""The ID of the object\"""
id: ID! id: ID!
} }"""
"""
) )
@ -1077,7 +1076,7 @@ def test_other_filter_types():
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
type Query { 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 { type PetTypeConnection {
@ -1125,8 +1124,7 @@ def test_other_filter_types():
interface Node { interface Node {
\"""The ID of the object\""" \"""The ID of the object\"""
id: ID! 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 not result.errors
assert result.data == expected assert result.data == expected
@ -1267,13 +1265,23 @@ def test_filter_string_contains():
result = schema.execute(query, variables={"filter": "Ja"}) result = schema.execute(query, variables={"filter": "Ja"})
assert not result.errors assert not result.errors
assert result.data == { 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"}) result = schema.execute(query, variables={"filter": "o"})
assert not result.errors assert not result.errors
assert result.data == { 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) schema = Schema(query=query)
query = """ query = """
query { query {{
articles (reporter_In: [%s, %s]) { articles (reporter_In: [{}, {}]) {{
edges { edges {{
node { node {{
headline headline
reporter { reporter {{
lastName lastName
} }}
} }}
} }}
} }}
} }}
""" % ( """.format(
john_doe.id, john_doe.id,
jean_bon.id, jean_bon.id,
) )

View File

@ -98,20 +98,14 @@ def test_typed_filter_schema(schema):
) )
for filter_field, gql_type in filters.items(): 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): def test_typed_filters_work(schema):
reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="") reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="")
Article.objects.create( Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es")
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="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 } } } }" 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) field_type = graphene.List(field_type)
args[name] = graphene.Argument( args[name] = graphene.Argument(
field_type, description=filter_field.label, required=required, field_type,
description=filter_field.label,
required=required,
) )
return args return args

View File

@ -3,7 +3,19 @@ from functools import singledispatch
from django import forms from django import forms
from django.core.exceptions import ImproperlyConfigured 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 from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
@ -57,12 +69,18 @@ def convert_form_field_to_nullboolean(field):
return Boolean(description=get_form_field_description(field)) return Boolean(description=get_form_field_description(field))
@convert_form_field.register(forms.DecimalField)
@convert_form_field.register(forms.FloatField) @convert_form_field.register(forms.FloatField)
def convert_form_field_to_float(field): def convert_form_field_to_float(field):
return Float(description=get_form_field_description(field), required=field.required) 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) @convert_form_field.register(forms.MultipleChoiceField)
def convert_form_field_to_string_list(field): def convert_form_field_to_string_list(field):
return List( return List(

View File

@ -82,7 +82,6 @@ class DjangoFormMutation(BaseDjangoFormMutation):
def __init_subclass_with_meta__( def __init_subclass_with_meta__(
cls, form_class=None, only_fields=(), exclude_fields=(), **options cls, form_class=None, only_fields=(), exclude_fields=(), **options
): ):
if not form_class: if not form_class:
raise Exception("form_class is required for DjangoFormMutation") raise Exception("form_class is required for DjangoFormMutation")
@ -95,7 +94,7 @@ class DjangoFormMutation(BaseDjangoFormMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField) 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 _meta=_meta, input_fields=input_fields, **options
) )
@ -117,7 +116,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
class Meta: class Meta:
abstract = True abstract = True
errors = graphene.List(ErrorType) errors = graphene.List(graphene.NonNull(ErrorType), required=True)
@classmethod @classmethod
def __init_subclass_with_meta__( def __init_subclass_with_meta__(
@ -127,9 +126,8 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
return_field_name=None, return_field_name=None,
only_fields=(), only_fields=(),
exclude_fields=(), exclude_fields=(),
**options **options,
): ):
if not form_class: if not form_class:
raise Exception("form_class is required for DjangoModelFormMutation") raise Exception("form_class is required for DjangoModelFormMutation")
@ -147,7 +145,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
registry = get_global_registry() registry = get_global_registry()
model_type = registry.get_type_for_model(model) model_type = registry.get_type_for_model(model)
if not model_type: 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: if not return_field_name:
model_name = model.__name__ model_name = model.__name__
@ -163,7 +161,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField) 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 _meta=_meta, input_fields=input_fields, **options
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -72,7 +72,6 @@ class SerializerMutation(ClientIDMutation):
_meta=None, _meta=None,
**options **options
): ):
if not serializer_class: if not serializer_class:
raise Exception("serializer_class is required for the SerializerMutation") raise Exception("serializer_class is required for the SerializerMutation")
@ -114,7 +113,7 @@ class SerializerMutation(ClientIDMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField) 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 _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() for name, field in serializer.fields.items()
} }
ret_type = type( ret_type = type(
"{}Input".format(serializer.__class__.__name__), f"{serializer.__class__.__name__}Input",
(graphene.InputObjectType,), (graphene.InputObjectType,),
items, 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.FloatField)
@get_graphene_type_from_serializer_field.register(serializers.DecimalField)
def convert_serializer_field_to_float(field): def convert_serializer_field_to_float(field):
return graphene.Float 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) @get_graphene_type_from_serializer_field.register(serializers.DateTimeField)
def convert_serializer_field_to_datetime_time(field): def convert_serializer_field_to_datetime_time(field):
return graphene.types.datetime.DateTime return graphene.types.datetime.DateTime

View File

@ -3,7 +3,7 @@ import copy
import graphene import graphene
from django.db import models from django.db import models
from graphene import InputObjectType from graphene import InputObjectType
from py.test import raises from pytest import raises
from rest_framework import serializers from rest_framework import serializers
from ..serializer_converter import convert_serializer_field from ..serializer_converter import convert_serializer_field
@ -133,9 +133,9 @@ def test_should_float_convert_float():
assert_conversion(serializers.FloatField, graphene.Float) assert_conversion(serializers.FloatField, graphene.Float)
def test_should_decimal_convert_float(): def test_should_decimal_convert_decimal():
assert_conversion( 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 import datetime
from py.test import raises from pytest import raises
from rest_framework import serializers from rest_framework import serializers
from graphene import Field, ResolveInfo 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 Graphene settings, checking for user settings first, then falling
back to the defaults. back to the defaults.
""" """
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.test.signals import setting_changed from django.test.signals import setting_changed
@ -41,7 +40,9 @@ DEFAULTS = {
# This sets headerEditorEnabled GraphiQL option, for details go to # This sets headerEditorEnabled GraphiQL option, for details go to
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options # https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
"GRAPHIQL_HEADER_EDITOR_ENABLED": True, "GRAPHIQL_HEADER_EDITOR_ENABLED": True,
"GRAPHIQL_SHOULD_PERSIST_HEADERS": False,
"ATOMIC_MUTATIONS": False, "ATOMIC_MUTATIONS": False,
"TESTING_ENDPOINT": "/graphql",
} }
if settings.DEBUG: if settings.DEBUG:
@ -76,7 +77,7 @@ def import_from_string(val, setting_name):
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
return getattr(module, class_name) return getattr(module, class_name)
except (ImportError, AttributeError) as e: 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, val,
setting_name, setting_name,
e.__class__.__name__, e.__class__.__name__,
@ -85,7 +86,7 @@ def import_from_string(val, setting_name):
raise ImportError(msg) raise ImportError(msg)
class GrapheneSettings(object): class GrapheneSettings:
""" """
A settings object, that allows API settings to be accessed as properties. A settings object, that allows API settings to be accessed as properties.
For example: For example:

View File

@ -5,19 +5,12 @@
GraphiQL, GraphiQL,
React, React,
ReactDOM, ReactDOM,
SubscriptionsTransportWs, graphqlWs,
GraphiQLPluginExplorer,
fetch, fetch,
history, history,
location, 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 // Collect the URL parameters
var parameters = {}; var parameters = {};
@ -60,98 +53,34 @@
var fetchURL = locationQuery(otherParams); var fetchURL = locationQuery(otherParams);
// Defines a GraphQL fetcher using the fetch API.
function httpClient(graphQLParams, opts) {
if (typeof opts === 'undefined') {
opts = {};
}
var headers = opts.headers || {};
headers['Accept'] = headers['Accept'] || 'application/json';
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
if (csrftoken) {
headers['X-CSRFToken'] = csrftoken
}
return fetch(fetchURL, {
method: "post",
headers: headers,
body: JSON.stringify(graphQLParams),
credentials: "include",
})
.then(function (response) {
return response.text();
})
.then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
// Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise // Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise
// assumes the current window location with an appropriate websocket protocol. // assumes the current window location with an appropriate websocket protocol.
var subscribeURL = var subscribeURL =
location.origin.replace(/^http/, "ws") + location.origin.replace(/^http/, "ws") +
(GRAPHENE_SETTINGS.subscriptionPath || location.pathname); (GRAPHENE_SETTINGS.subscriptionPath || location.pathname);
// Create a subscription client. function trueLambda() { return true; };
var subscriptionClient = new SubscriptionsTransportWs.SubscriptionClient(
subscribeURL,
{
// Reconnect after any interruptions.
reconnect: true,
// Delay socket initialization until the first subscription is started.
lazy: true,
},
);
// Keep a reference to the currently-active subscription, if available. var headers = {};
var activeSubscription = null; var cookies = ("; " + document.cookie).split("; csrftoken=");
if (cookies.length == 2) {
// Define a GraphQL fetcher that can intelligently route queries based on the operation type. csrftoken = cookies.pop().split(";").shift();
function graphQLFetcher(graphQLParams, opts) {
var operationType = getOperationType(graphQLParams);
// If we're about to execute a new operation, and we have an active subscription,
// unsubscribe before continuing.
if (activeSubscription) {
activeSubscription.unsubscribe();
activeSubscription = null;
}
if (operationType === "subscription") {
return {
subscribe: function (observer) {
activeSubscription = subscriptionClient;
return subscriptionClient.request(graphQLParams, opts).subscribe(observer);
},
};
} else { } else {
return httpClient(graphQLParams, opts); csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
} }
if (csrftoken) {
headers['X-CSRFToken'] = csrftoken
} }
// Determine the type of operation being executed for a given set of GraphQL parameters. var graphQLFetcher = GraphiQL.createFetcher({
function getOperationType(graphQLParams) { url: fetchURL,
// Run a regex against the query to determine the operation type (query, mutation, subscription). wsClient: graphqlWs.createClient({
var operationRegex = new RegExp( url: subscribeURL,
// Look for lines that start with an operation keyword, ignoring whitespace. shouldRetry: trueLambda,
"^\\s*(query|mutation|subscription)\\s*" + lazy: true,
// The operation keyword should be followed by whitespace and the operationName in the GraphQL parameters (if available). }),
(graphQLParams.operationName ? ("\\s+" + graphQLParams.operationName) : "") + headers: headers
// The line should eventually encounter an opening curly brace. })
"[^\\{]*\\{",
// Enable multiline matching.
"m",
);
var match = operationRegex.exec(graphQLParams.query);
if (!match) {
return "query";
}
return match[1];
}
// When the query and variables string is edited, update the URL bar so // When the query and variables string is edited, update the URL bar so
// that it can be easily shared. // that it can be easily shared.
@ -170,13 +99,30 @@
function updateURL() { function updateURL() {
history.replaceState(null, null, locationQuery(parameters)); history.replaceState(null, null, locationQuery(parameters));
} }
function GraphiQLWithExplorer() {
var [query, setQuery] = React.useState(parameters.query);
function handleQuery(query) {
setQuery(query);
onEditQuery(query);
}
var explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({
query: query,
onEdit: handleQuery,
});
var options = { var options = {
fetcher: graphQLFetcher, fetcher: graphQLFetcher,
onEditQuery: onEditQuery, plugins: [explorerPlugin],
defaultEditorToolsVisibility: true,
onEditQuery: handleQuery,
onEditVariables: onEditVariables, onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName, onEditOperationName: onEditOperationName,
headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
query: parameters.query, shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders,
query: query,
}; };
if (parameters.variables) { if (parameters.variables) {
options.variables = parameters.variables; options.variables = parameters.variables;
@ -184,9 +130,13 @@
if (parameters.operation_name) { if (parameters.operation_name) {
options.operationName = parameters.operation_name; options.operationName = parameters.operation_name;
} }
return React.createElement(GraphiQL, options);
}
// Render <GraphiQL /> into the body. // Render <GraphiQL /> into the body.
ReactDOM.render( ReactDOM.render(
React.createElement(GraphiQL, options), React.createElement(GraphiQLWithExplorer),
document.getElementById("editor"), document.getElementById("editor"),
); );
})( })(
@ -196,7 +146,8 @@
window.GraphiQL, window.GraphiQL,
window.React, window.React,
window.ReactDOM, window.ReactDOM,
window.SubscriptionsTransportWs, window.graphqlWs,
window.GraphiQLPluginExplorer,
window.fetch, window.fetch,
window.history, window.history,
window.location, window.location,

View File

@ -33,9 +33,12 @@ add "&raw" to the end of the URL within a browser.
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js" <script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
integrity="{{graphiql_sri}}" integrity="{{graphiql_sri}}"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/subscriptions-transport-ws@{{subscriptions_transport_ws_version}}/browser/client.js" <script src="https://cdn.jsdelivr.net/npm/graphql-ws@{{subscriptions_transport_ws_version}}/umd/graphql-ws.min.js"
integrity="{{subscriptions_transport_ws_sri}}" integrity="{{subscriptions_transport_ws_sri}}"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@graphiql/plugin-explorer@{{graphiql_plugin_explorer_version}}/dist/graphiql-plugin-explorer.umd.js"
integrity="{{graphiql_plugin_explorer_sri}}"
crossorigin="anonymous"></script>
</head> </head>
<body> <body>
<div id="editor"></div> <div id="editor"></div>
@ -46,6 +49,7 @@ add "&raw" to the end of the URL within a browser.
subscriptionPath: "{{subscription_path}}", subscriptionPath: "{{subscription_path}}",
{% endif %} {% endif %}
graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }}, graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }},
graphiqlShouldPersistHeaders: {{ graphiql_should_persist_headers|yesno:"true,false" }},
}; };
</script> </script>
<script src="{% static 'graphene_django/graphiql.js' %}"></script> <script src="{% static 'graphene_django/graphiql.js' %}"></script>

View File

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

View File

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

View File

@ -5,7 +5,6 @@ from .mutations import PetFormMutation, PetMutation
class QueryRoot(ObjectType): class QueryRoot(ObjectType):
thrower = graphene.String(required=True) thrower = graphene.String(required=True)
request = graphene.String(required=True) request = graphene.String(required=True)
test = graphene.String(who=graphene.String()) test = graphene.String(who=graphene.String())

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import datetime import datetime
from django.db.models import Count import re
from django.db.models import Count, Prefetch
import pytest import pytest
@ -7,8 +8,12 @@ from graphene import List, NonNull, ObjectType, Schema, String
from ..fields import DjangoListField from ..fields import DjangoListField
from ..types import DjangoObjectType from ..types import DjangoObjectType
from .models import Article as ArticleModel from .models import (
from .models import Reporter as ReporterModel Article as ArticleModel,
Film as FilmModel,
FilmDetails as FilmDetailsModel,
Reporter as ReporterModel,
)
class TestDjangoListField: class TestDjangoListField:
@ -500,3 +505,145 @@ class TestDjangoListField:
assert not result.errors assert not result.errors
assert result.data == {"reporters": [{"firstName": "Tara"}]} assert result.data == {"reporters": [{"firstName": "Tara"}]}
def test_select_related_and_prefetch_related_are_respected(
self, django_assert_num_queries
):
class Article(DjangoObjectType):
class Meta:
model = ArticleModel
fields = ("headline", "editor", "reporter")
class Film(DjangoObjectType):
class Meta:
model = FilmModel
fields = ("genre", "details")
class FilmDetail(DjangoObjectType):
class Meta:
model = FilmDetailsModel
fields = ("location",)
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles", "films")
class Query(ObjectType):
articles = DjangoListField(Article)
@staticmethod
def resolve_articles(root, info):
# Optimize for querying associated editors and reporters, and the films and film
# details of those reporters. This is similar to what would happen using a library
# like https://github.com/tfoxy/graphene-django-optimizer for a query like the one
# below (albeit simplified and hardcoded here).
return ArticleModel.objects.select_related(
"editor", "reporter"
).prefetch_related(
Prefetch(
"reporter__films",
queryset=FilmModel.objects.select_related("details"),
),
)
schema = Schema(query=Query)
query = """
query {
articles {
headline
editor {
firstName
}
reporter {
firstName
films {
genre
details {
location
}
}
}
}
}
"""
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
r2 = ReporterModel.objects.create(first_name="Debra", last_name="Payne")
ArticleModel.objects.create(
headline="Amazing news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r2,
)
ArticleModel.objects.create(
headline="Not so good news",
reporter=r2,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
film1 = FilmModel.objects.create(genre="ac")
film2 = FilmModel.objects.create(genre="ot")
film3 = FilmModel.objects.create(genre="do")
FilmDetailsModel.objects.create(location="Hollywood", film=film1)
FilmDetailsModel.objects.create(location="Antarctica", film=film3)
r1.films.add(film1, film2)
r2.films.add(film3)
# We expect 2 queries to be performed based on the above resolver definition: one for all
# articles joined with the reporters model (for associated editors and reporters), and one
# for the films prefetch (which includes its `select_related` JOIN logic in its queryset)
with django_assert_num_queries(2) as captured:
result = schema.execute(query)
assert not result.errors
assert result.data == {
"articles": [
{
"headline": "Amazing news",
"editor": {"firstName": "Debra"},
"reporter": {
"firstName": "Tara",
"films": [
{"genre": "AC", "details": {"location": "Hollywood"}},
{"genre": "OT", "details": None},
],
},
},
{
"headline": "Not so good news",
"editor": {"firstName": "Tara"},
"reporter": {
"firstName": "Debra",
"films": [
{"genre": "DO", "details": {"location": "Antarctica"}},
],
},
},
]
}
assert len(captured.captured_queries) == 2 # Sanity-check
# First we should have queried for all articles in a single query, joining on the reporters
# model (for the editors and reporters ForeignKeys)
assert re.match(
r'SELECT .* "tests_article" INNER JOIN "tests_reporter"',
captured.captured_queries[0]["sql"],
)
# Then we should have queried for all of the films of all reporters, joined with the film
# details for each film, using a single query
assert re.match(
r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"',
captured.captured_queries[1]["sql"],
)

View File

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

View File

@ -0,0 +1,235 @@
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)
NOTE: For now, we do not expect this get_queryset method to be called for nested
objects, as the original attempt to do so prevented SQL query-optimization with
`select_related`/`prefetch_related` and caused N+1 queries. See discussions here
https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857
and here https://github.com/graphql-python/graphene-django/pull/1401.
"""
@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"}}
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"}}

View File

@ -6,7 +6,7 @@ from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from graphql_relay import to_global_id from graphql_relay import to_global_id
from py.test import raises from pytest import raises
import graphene import graphene
from graphene.relay import Node from graphene.relay import Node
@ -15,7 +15,7 @@ from ..compat import IntegerRangeField, MissingType
from ..fields import DjangoConnectionField from ..fields import DjangoConnectionField
from ..types import DjangoObjectType from ..types import DjangoObjectType
from ..utils import DJANGO_FILTER_INSTALLED 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(): def test_should_query_only_fields():
@ -251,8 +251,8 @@ def test_should_node():
def test_should_query_onetoone_fields(): def test_should_query_onetoone_fields():
film = Film(id=1) film = Film.objects.create(id=1)
film_details = FilmDetails(id=1, film=film) film_details = FilmDetails.objects.create(id=1, film=film)
class FilmNode(DjangoObjectType): class FilmNode(DjangoObjectType):
class Meta: class Meta:
@ -780,7 +780,6 @@ def test_should_query_promise_connectionfields():
def test_should_query_connectionfields_with_last(): def test_should_query_connectionfields_with_last():
r = Reporter.objects.create( r = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
) )
@ -818,7 +817,6 @@ def test_should_query_connectionfields_with_last():
def test_should_query_connectionfields_with_manager(): def test_should_query_connectionfields_with_manager():
r = Reporter.objects.create( r = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
) )
@ -1151,9 +1149,9 @@ def test_connection_should_limit_after_to_list_length():
REPORTERS = [ REPORTERS = [
dict( dict(
first_name="First {}".format(i), first_name=f"First {i}",
last_name="Last {}".format(i), last_name=f"Last {i}",
email="johndoe+{}@example.com".format(i), email=f"johndoe+{i}@example.com",
a_choice=1, a_choice=1,
) )
for i in range(6) for i in range(6)
@ -1243,6 +1241,7 @@ def test_should_have_next_page(graphene_settings):
} }
@pytest.mark.parametrize("max_limit", [100, 4])
class TestBackwardPagination: class TestBackwardPagination:
def setup_schema(self, graphene_settings, max_limit): def setup_schema(self, graphene_settings, max_limit):
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit
@ -1261,8 +1260,8 @@ class TestBackwardPagination:
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
return schema return schema
def do_queries(self, schema): def test_query_last(self, graphene_settings, max_limit):
# Simply last 3 schema = self.setup_schema(graphene_settings, max_limit=max_limit)
query_last = """ query_last = """
query { query {
allReporters(last: 3) { allReporters(last: 3) {
@ -1282,7 +1281,8 @@ class TestBackwardPagination:
e["node"]["firstName"] for e in result.data["allReporters"]["edges"] e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
] == ["First 3", "First 4", "First 5"] ] == ["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_first_and_last = """
query { query {
allReporters(first: 4, last: 3) { allReporters(first: 4, last: 3) {
@ -1302,7 +1302,8 @@ class TestBackwardPagination:
e["node"]["firstName"] for e in result.data["allReporters"]["edges"] e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
] == ["First 1", "First 2", "First 3"] ] == ["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_first_last_and_after = """
query queryAfter($after: String) { query queryAfter($after: String) {
allReporters(first: 4, last: 3, after: $after) { allReporters(first: 4, last: 3, after: $after) {
@ -1317,7 +1318,8 @@ class TestBackwardPagination:
after = base64.b64encode(b"arrayconnection:0").decode() after = base64.b64encode(b"arrayconnection:0").decode()
result = schema.execute( 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 not result.errors
assert len(result.data["allReporters"]["edges"]) == 3 assert len(result.data["allReporters"]["edges"]) == 3
@ -1325,20 +1327,35 @@ class TestBackwardPagination:
e["node"]["firstName"] for e in result.data["allReporters"]["edges"] e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
] == ["First 2", "First 3", "First 4"] ] == ["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): result = schema.execute(
""" query_first_last_and_after,
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. assert not result.errors
""" assert len(result.data["allReporters"]["edges"]) == 1
schema = self.setup_schema(graphene_settings, max_limit=4) assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5"
self.do_queries(schema)
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): def test_should_preserve_prefetch_related(django_assert_num_queries):
@ -1480,7 +1497,11 @@ def test_connection_should_enable_offset_filtering():
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
expected = { expected = {
"allReporters": {"edges": [{"node": {"firstName": "Some", "lastName": "Guy"}},]} "allReporters": {
"edges": [
{"node": {"firstName": "Some", "lastName": "Guy"}},
]
}
} }
assert result.data == expected assert result.data == expected
@ -1521,7 +1542,9 @@ def test_connection_should_enable_offset_filtering_higher_than_max_limit(
assert not result.errors assert not result.errors
expected = { expected = {
"allReporters": { "allReporters": {
"edges": [{"node": {"firstName": "Some", "lastName": "Lady"}},] "edges": [
{"node": {"firstName": "Some", "lastName": "Lady"}},
]
} }
} }
assert result.data == expected assert result.data == expected
@ -1590,6 +1613,149 @@ def test_connection_should_allow_offset_filtering_with_after():
result = schema.execute(query, variable_values=dict(after=after)) result = schema.execute(query, variable_values=dict(after=after))
assert not result.errors assert not result.errors
expected = { expected = {
"allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]} "allReporters": {
"edges": [
{"node": {"firstName": "Jane", "lastName": "Roe"}},
]
}
} }
assert result.data == expected 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 ..registry import Registry
from ..types import DjangoObjectType from ..types import DjangoObjectType

View File

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

View File

@ -2,7 +2,7 @@ import json
import pytest import pytest
from django.utils.translation import gettext_lazy 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 ..utils import camelize, get_model_fields, GraphQLTestCase
from .models import Film, Reporter from .models import Film, Reporter
@ -11,11 +11,11 @@ from ..utils.testing import graphql_query
def test_get_model_fields_no_duplication(): def test_get_model_fields_no_duplication():
reporter_fields = get_model_fields(Reporter) 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) assert len(reporter_fields) == len(reporter_name_set)
film_fields = get_model_fields(Film) 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) 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._pre_setup()
tc.setUpClass() tc.setUpClass()
tc.query("query { }", operation_name="QueryName") 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 # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
assert ( assert (
"operationName", "operationName",
@ -66,7 +66,7 @@ def test_graphql_test_case_operation_name(post_mock):
@patch("graphene_django.utils.testing.Client.post") @patch("graphene_django.utils.testing.Client.post")
def test_graphql_query_case_operation_name(post_mock): def test_graphql_query_case_operation_name(post_mock):
graphql_query("query { }", operation_name="QueryName") 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 # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
assert ( assert (
"operationName", "operationName",

View File

@ -2,7 +2,7 @@ import json
import pytest import pytest
from mock import patch from unittest.mock import patch
from django.db import connection from django.db import connection
@ -109,12 +109,10 @@ def test_reports_validation_errors(client):
{ {
"message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.",
"locations": [{"line": 1, "column": 9}], "locations": [{"line": 1, "column": 9}],
"path": None,
}, },
{ {
"message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.",
"locations": [{"line": 1, "column": 21}], "locations": [{"line": 1, "column": 21}],
"path": None,
}, },
] ]
} }
@ -135,8 +133,6 @@ def test_errors_when_missing_operation_name(client):
"errors": [ "errors": [
{ {
"message": "Must provide operation name if query contains multiple operations.", "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}], "locations": [{"column": 1, "line": 1}],
"message": "Syntax Error: Unexpected Name 'syntaxerror'.", "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 test_handles_django_request_error(client, monkeypatch):
def mocked_read(*args): def mocked_read(*args):
raise IOError("foo-bar") raise OSError("foo-bar")
monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read) 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 from ..views import GraphQLView
urlpatterns = [ urlpatterns = [
url(r"^graphql/batch", GraphQLView.as_view(batch=True)), path("graphql/batch", GraphQLView.as_view(batch=True)),
url(r"^graphql", GraphQLView.as_view(graphiql=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 ..views import GraphQLView
from .schema_view import schema from .schema_view import schema
@ -10,4 +10,4 @@ class CustomGraphQLView(GraphQLView):
pretty = True 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 ..views import GraphQLView
from .schema_view import schema 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): class DjangoObjectTypeOptions(ObjectTypeOptions):
model = None # type: Model model = None # type: Type[Model]
registry = None # type: Registry registry = None # type: Registry
connection = None # type: Type[Connection] connection = None # type: Type[Connection]
@ -168,11 +168,9 @@ class DjangoObjectType(ObjectType):
if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class):
raise Exception( raise Exception(
(
"Can only set filter_fields or filterset_class if " "Can only set filter_fields or filterset_class if "
"Django-Filter is installed" "Django-Filter is installed"
) )
)
assert not (fields and exclude), ( assert not (fields and exclude), (
"Cannot set both 'fields' and 'exclude' options on " "Cannot set both 'fields' and 'exclude' options on "
@ -216,7 +214,7 @@ class DjangoObjectType(ObjectType):
"Creating a DjangoObjectType without either the `fields` " "Creating a DjangoObjectType without either the `fields` "
"or the `exclude` option is deprecated. Add an explicit `fields " "or the `exclude` option is deprecated. Add an explicit `fields "
"= '__all__'` option on DjangoObjectType {class_name} to use all " "= '__all__'` option on DjangoObjectType {class_name} to use all "
"fields".format(class_name=cls.__name__,), "fields".format(class_name=cls.__name__),
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
@ -228,7 +226,7 @@ class DjangoObjectType(ObjectType):
if use_connection is None and interfaces: if use_connection is None and interfaces:
use_connection = any( use_connection = any(
(issubclass(interface, Node) for interface in interfaces) issubclass(interface, Node) for interface in interfaces
) )
if use_connection and not connection: if use_connection and not connection:
@ -255,7 +253,7 @@ class DjangoObjectType(ObjectType):
_meta.fields = django_fields _meta.fields = django_fields
_meta.connection = connection _meta.connection = connection
super(DjangoObjectType, cls).__init_subclass_with_meta__( super().__init_subclass_with_meta__(
_meta=_meta, interfaces=interfaces, **options _meta=_meta, interfaces=interfaces, **options
) )

View File

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

View File

@ -6,4 +6,4 @@ def test_to_const():
def test_to_const_unicode(): 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 .. import GraphQLTestCase
from ...tests.test_types import with_local_registry from ...tests.test_types import with_local_registry
from ...settings import graphene_settings
from django.test import Client from django.test import Client
@ -43,3 +44,11 @@ def test_graphql_test_case_deprecated_client_setter():
with pytest.warns(PendingDeprecationWarning): with pytest.warns(PendingDeprecationWarning):
tc._client = Client() 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 django.views.generic import View
from graphql import OperationType, get_operation_ast, parse, validate from graphql import OperationType, get_operation_ast, parse, validate
from graphql.error import GraphQLError from graphql.error import GraphQLError
from graphql.error import format_error as format_graphql_error
from graphql.execution import ExecutionResult from graphql.execution import ExecutionResult
from graphene import Schema from graphene import Schema
@ -27,7 +26,7 @@ class HttpError(Exception):
def __init__(self, response, message=None, *args, **kwargs): def __init__(self, response, message=None, *args, **kwargs):
self.response = response self.response = response
self.message = message = message or response.content.decode() 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): def get_accepted_content_types(request):
@ -67,16 +66,19 @@ class GraphQLView(View):
react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=" react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0="
# The GraphiQL React app. # The GraphiQL React app.
graphiql_version = "1.4.1" # "1.0.3" graphiql_version = "2.4.1" # "1.0.3"
graphiql_sri = "sha256-JUMkXBQWZMfJ7fGEsTXalxVA10lzKOS9loXdLjwZKi4=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk=" graphiql_sri = "sha256-s+f7CFAPSUIygFnRC2nfoiEKd3liCUy+snSdYFAoLUc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
graphiql_css_sri = "sha256-Md3vdR7PDzWyo/aGfsFVF4tvS5/eAUWuIsg9QHUusCY=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E=" graphiql_css_sri = "sha256-88yn8FJMyGboGs4Bj+Pbb3kWOWXo7jmb+XCRHE+282k=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
# The websocket transport library for subscriptions. # The websocket transport library for subscriptions.
subscriptions_transport_ws_version = "0.9.18" subscriptions_transport_ws_version = "5.12.1"
subscriptions_transport_ws_sri = ( subscriptions_transport_ws_sri = (
"sha256-i0hAXd4PdJ/cHX3/8tIy/Q/qKiWr5WSTxMFuL9tACkw=" "sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw="
) )
graphiql_plugin_explorer_version = "0.1.15"
graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE="
schema = None schema = None
graphiql = False graphiql = False
middleware = None middleware = None
@ -159,10 +161,13 @@ class GraphQLView(View):
graphiql_css_sri=self.graphiql_css_sri, graphiql_css_sri=self.graphiql_css_sri,
subscriptions_transport_ws_version=self.subscriptions_transport_ws_version, subscriptions_transport_ws_version=self.subscriptions_transport_ws_version,
subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri, subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri,
graphiql_plugin_explorer_version=self.graphiql_plugin_explorer_version,
graphiql_plugin_explorer_sri=self.graphiql_plugin_explorer_sri,
# The SUBSCRIPTION_PATH setting. # The SUBSCRIPTION_PATH setting.
subscription_path=self.subscription_path, subscription_path=self.subscription_path,
# GraphiQL headers tab, # GraphiQL headers tab,
graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED, graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED,
graphiql_should_persist_headers=graphene_settings.GRAPHIQL_SHOULD_PERSIST_HEADERS,
) )
if self.batch: if self.batch:
@ -387,7 +392,7 @@ class GraphQLView(View):
@staticmethod @staticmethod
def format_error(error): def format_error(error):
if isinstance(error, GraphQLError): if isinstance(error, GraphQLError):
return format_graphql_error(error) return error.formatted
return {"message": str(error)} return {"message": str(error)}

View File

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

View File

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

38
tox.ini
View File

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