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

@ -27,8 +27,8 @@ a github repo, https://repl.it or similar (you can use this template as a starti
* **Please tell us about your environment:**
- Version:
- Platform:
- Version:
- Platform:
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow)

View File

@ -10,17 +10,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.9
python-version: '3.11'
- name: Build wheel and source tarball
run: |
pip install wheel
python setup.py sdist bdist_wheel
- 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:
user: __token__
password: ${{ secrets.pypi_password }}

View File

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

View File

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

@ -59,4 +59,4 @@ Then to produce a HTML version of the documentation:
```sh
make html
```
```

View File

@ -3,4 +3,4 @@ recursive-include graphene_django/templates *
recursive-include graphene_django/static *
include examples/cookbook/cookbook/ingredients/fixtures/ingredients.json
include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json
include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json

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

View File

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

View File

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

View File

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

View File

@ -151,7 +151,7 @@ For example the following ``Model`` and ``DjangoObjectType``:
Results in the following GraphQL schema definition:
.. code::
.. code:: graphql
type Pet {
id: ID!
@ -178,7 +178,7 @@ You can disable this automatic conversion by setting
fields = ("id", "kind",)
convert_choices_to_enum = False
.. code::
.. code:: graphql
type Pet {
id: ID!
@ -313,7 +313,7 @@ Additionally, Resolvers will receive **any arguments declared in the field defin
bar=graphene.Int()
)
def resolve_question(root, info, foo, bar):
def resolve_question(root, info, foo=None, bar=None):
# If `foo` or `bar` are declared in the GraphQL query they will be here, else None.
return Question.objects.filter(foo=foo, bar=bar).first()
@ -336,12 +336,12 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen
class Query(graphene.ObjectType):
questions = graphene.List(QuestionType)
def resolve_questions(root, info):
# See if a user is authenticated
if info.context.user.is_authenticated():
return Question.objects.all()
else:
return Question.objects.none()
def resolve_questions(root, info):
# See if a user is authenticated
if info.context.user.is_authenticated():
return Question.objects.all()
else:
return Question.objects.none()
DjangoObjectTypes
@ -418,29 +418,29 @@ the core graphene pages for more information on customizing the Relay experience
You can now execute queries like:
.. code:: python
.. code:: graphql
{
questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
startCursor
endCursor
hasNextPage
hasPreviousPage
}
edges {
cursor
node {
id
question_text
}
cursor
node {
id
question_text
}
}
}
}
Which returns:
.. code:: python
.. code:: json
{
"data": {

View File

@ -1,60 +1,57 @@
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 Meta:
model = Category
fields = '__all__'
class CategoryType(DjangoObjectType):
class Meta:
model = Category
fields = "__all__"
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
fields = '__all__'
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
fields = "__all__"
class Query(object):
category = graphene.Field(CategoryType,
id=graphene.Int(),
name=graphene.String())
all_categories = graphene.List(CategoryType)
class Query:
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType)
ingredient = graphene.Field(
IngredientType, id=graphene.Int(), name=graphene.String()
)
all_ingredients = graphene.List(IngredientType)
ingredient = graphene.Field(IngredientType,
id=graphene.Int(),
name=graphene.String())
all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, info, **kwargs):
return Category.objects.all()
def resolve_all_categories(self, info, **kwargs):
return Category.objects.all()
def resolve_all_ingredients(self, info, **kwargs):
return Ingredient.objects.all()
def resolve_all_ingredients(self, info, **kwargs):
return Ingredient.objects.all()
def resolve_category(self, info, **kwargs):
id = kwargs.get("id")
name = kwargs.get("name")
def resolve_category(self, info, **kwargs):
id = kwargs.get('id')
name = kwargs.get('name')
if id is not None:
return Category.objects.get(pk=id)
if id is not None:
return Category.objects.get(pk=id)
if name is not None:
return Category.objects.get(name=name)
if name is not None:
return Category.objects.get(name=name)
return None
return None
def resolve_ingredient(self, info, **kwargs):
id = kwargs.get("id")
name = kwargs.get("name")
def resolve_ingredient(self, info, **kwargs):
id = kwargs.get('id')
name = kwargs.get('name')
if id is not None:
return Ingredient.objects.get(pk=id)
if id is not None:
return Ingredient.objects.get(pk=id)
if name is not None:
return Ingredient.objects.get(name=name)
if name is not None:
return Ingredient.objects.get(name=name)
return None
return None

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,8 +24,15 @@ from graphene import (
Decimal,
)
from graphene.types.json import JSONString
from graphene.types.scalars import BigInt
from graphene.utils.str_converters import to_camel_case
from graphql import GraphQLError, assert_valid_name
from graphql import GraphQLError
try:
from graphql import assert_name
except ImportError:
# Support for older versions of graphql
from graphql import assert_valid_name as assert_name
from graphql.pyutils import register_description
from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
@ -55,7 +62,7 @@ class BlankValueField(Field):
def convert_choice_name(name):
name = to_const(force_str(name))
try:
assert_valid_name(name)
assert_name(name)
except GraphQLError:
name = "A_%s" % name
return name
@ -67,8 +74,7 @@ def get_choices(choices):
choices = choices.items()
for value, help_text in choices:
if isinstance(help_text, (tuple, list)):
for choice in get_choices(help_text):
yield choice
yield from get_choices(help_text)
else:
name = convert_choice_name(value)
while name in converted_names:
@ -85,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_descriptions = {c[0]: c[2] for c in choices}
class EnumWithDescriptionsType(object):
class EnumWithDescriptionsType:
@property
def description(self):
return str(named_choices_descriptions[self.name])
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
@ -102,7 +113,7 @@ def generate_enum_name(django_model_meta, field):
)
name = custom_func(field)
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True:
name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name))
name = to_camel_case(f"{django_model_meta.object_name}_{field.name}")
else:
name = "{app_label}{object_name}{field_name}Choices".format(
app_label=to_camel_case(django_model_meta.app_label.title()),
@ -148,7 +159,9 @@ def get_django_field_description(field):
@singledispatch
def convert_django_field(field, registry=None):
raise Exception(
"Don't know how to convert the Django field %s (%s)" % (field, field.__class__)
"Don't know how to convert the Django field {} ({})".format(
field, field.__class__
)
)
@ -186,10 +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.PositiveSmallIntegerField)
@convert_django_field.register(models.SmallIntegerField)
@convert_django_field.register(models.BigIntegerField)
@convert_django_field.register(models.IntegerField)
def convert_field_to_int(field, registry=None):
return Int(description=get_django_field_description(field), required=not field.null)
@ -205,7 +222,9 @@ def convert_field_to_boolean(field, registry=None):
@convert_django_field.register(models.DecimalField)
def convert_field_to_decimal(field, registry=None):
return Decimal(description=field.help_text, required=not field.null)
return Decimal(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.FloatField)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,7 +72,6 @@ class SerializerMutation(ClientIDMutation):
_meta=None,
**options
):
if not serializer_class:
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)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super(SerializerMutation, cls).__init_subclass_with_meta__(
super().__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options
)

View File

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

View File

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

View File

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

View File

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

View File

@ -5,19 +5,12 @@
GraphiQL,
React,
ReactDOM,
SubscriptionsTransportWs,
graphqlWs,
GraphiQLPluginExplorer,
fetch,
history,
location,
) {
// Parse the cookie value for a CSRF token
var csrftoken;
var cookies = ("; " + document.cookie).split("; csrftoken=");
if (cookies.length == 2) {
csrftoken = cookies.pop().split(";").shift();
} else {
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
}
// Collect the URL parameters
var parameters = {};
@ -60,98 +53,34 @@
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
// assumes the current window location with an appropriate websocket protocol.
var subscribeURL =
location.origin.replace(/^http/, "ws") +
(GRAPHENE_SETTINGS.subscriptionPath || location.pathname);
// Create a subscription client.
var subscriptionClient = new SubscriptionsTransportWs.SubscriptionClient(
subscribeURL,
{
// Reconnect after any interruptions.
reconnect: true,
// Delay socket initialization until the first subscription is started.
function trueLambda() { return true; };
var headers = {};
var cookies = ("; " + document.cookie).split("; csrftoken=");
if (cookies.length == 2) {
csrftoken = cookies.pop().split(";").shift();
} else {
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
}
if (csrftoken) {
headers['X-CSRFToken'] = csrftoken
}
var graphQLFetcher = GraphiQL.createFetcher({
url: fetchURL,
wsClient: graphqlWs.createClient({
url: subscribeURL,
shouldRetry: trueLambda,
lazy: true,
},
);
// Keep a reference to the currently-active subscription, if available.
var activeSubscription = null;
// Define a GraphQL fetcher that can intelligently route queries based on the operation type.
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 {
return httpClient(graphQLParams, opts);
}
}
// Determine the type of operation being executed for a given set of GraphQL parameters.
function getOperationType(graphQLParams) {
// Run a regex against the query to determine the operation type (query, mutation, subscription).
var operationRegex = new RegExp(
// Look for lines that start with an operation keyword, ignoring whitespace.
"^\\s*(query|mutation|subscription)\\s*" +
// The operation keyword should be followed by whitespace and the operationName in the GraphQL parameters (if available).
(graphQLParams.operationName ? ("\\s+" + graphQLParams.operationName) : "") +
// 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];
}
}),
headers: headers
})
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared.
@ -170,23 +99,44 @@
function updateURL() {
history.replaceState(null, null, locationQuery(parameters));
}
var options = {
fetcher: graphQLFetcher,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
query: parameters.query,
};
if (parameters.variables) {
options.variables = parameters.variables;
}
if (parameters.operation_name) {
options.operationName = parameters.operation_name;
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 = {
fetcher: graphQLFetcher,
plugins: [explorerPlugin],
defaultEditorToolsVisibility: true,
onEditQuery: handleQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders,
query: query,
};
if (parameters.variables) {
options.variables = parameters.variables;
}
if (parameters.operation_name) {
options.operationName = parameters.operation_name;
}
return React.createElement(GraphiQL, options);
}
// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, options),
React.createElement(GraphiQLWithExplorer),
document.getElementById("editor"),
);
})(
@ -196,7 +146,8 @@
window.GraphiQL,
window.React,
window.ReactDOM,
window.SubscriptionsTransportWs,
window.graphqlWs,
window.GraphiQLPluginExplorer,
window.fetch,
window.history,
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"
integrity="{{graphiql_sri}}"
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}}"
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>
<body>
<div id="editor"></div>
@ -46,6 +49,7 @@ add "&raw" to the end of the URL within a browser.
subscriptionPath: "{{subscription_path}}",
{% endif %}
graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }},
graphiqlShouldPersistHeaders: {{ graphiql_should_persist_headers|yesno:"true,false" }},
};
</script>
<script src="{% static 'graphene_django/graphiql.js' %}"></script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View
from graphql import OperationType, get_operation_ast, parse, validate
from graphql.error import GraphQLError
from graphql.error import format_error as format_graphql_error
from graphql.execution import ExecutionResult
from graphene import Schema
@ -27,7 +26,7 @@ class HttpError(Exception):
def __init__(self, response, message=None, *args, **kwargs):
self.response = response
self.message = message = message or response.content.decode()
super(HttpError, self).__init__(message, *args, **kwargs)
super().__init__(message, *args, **kwargs)
def get_accepted_content_types(request):
@ -67,16 +66,19 @@ class GraphQLView(View):
react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0="
# The GraphiQL React app.
graphiql_version = "1.4.1" # "1.0.3"
graphiql_sri = "sha256-JUMkXBQWZMfJ7fGEsTXalxVA10lzKOS9loXdLjwZKi4=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
graphiql_css_sri = "sha256-Md3vdR7PDzWyo/aGfsFVF4tvS5/eAUWuIsg9QHUusCY=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
graphiql_version = "2.4.1" # "1.0.3"
graphiql_sri = "sha256-s+f7CFAPSUIygFnRC2nfoiEKd3liCUy+snSdYFAoLUc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
graphiql_css_sri = "sha256-88yn8FJMyGboGs4Bj+Pbb3kWOWXo7jmb+XCRHE+282k=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
# The websocket transport library for subscriptions.
subscriptions_transport_ws_version = "0.9.18"
subscriptions_transport_ws_version = "5.12.1"
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
graphiql = False
middleware = None
@ -159,10 +161,13 @@ class GraphQLView(View):
graphiql_css_sri=self.graphiql_css_sri,
subscriptions_transport_ws_version=self.subscriptions_transport_ws_version,
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.
subscription_path=self.subscription_path,
# GraphiQL headers tab,
graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED,
graphiql_should_persist_headers=graphene_settings.GRAPHIQL_SHOULD_PERSIST_HEADERS,
)
if self.batch:
@ -387,7 +392,7 @@ class GraphQLView(View):
@staticmethod
def format_error(error):
if isinstance(error, GraphQLError):
return format_graphql_error(error)
return error.formatted
return {"message": str(error)}

View File

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

View File

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

38
tox.ini
View File

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