Merge branch 'main' into fix/execution-context-class-as-class-attr

This commit is contained in:
Firas Kafri 2023-05-05 13:05:06 +03:00 committed by GitHub
commit cc9e1c94e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 347 additions and 433 deletions

View File

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

View File

@ -7,11 +7,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python 3.9 - name: Set up Python 3.11
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: 3.9 python-version: '3.11'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip

View File

@ -13,10 +13,12 @@ jobs:
include: include:
- django: "3.2" - django: "3.2"
python-version: "3.7" python-version: "3.7"
- django: "4.1"
python-version: "3.11"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies

View File

@ -1,8 +1,8 @@
default_language_version: default_language_version:
python: python3.9 python: python3.11
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0 rev: v4.4.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json - id: check-json
@ -16,15 +16,15 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
exclude: README.md exclude: README.md
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.2.0 rev: v3.3.2
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.10.0 rev: 23.3.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 5.0.4 rev: 6.0.0
hooks: hooks:
- id: flake8 - id: flake8

View File

@ -6,6 +6,7 @@ help:
.PHONY: dev-setup ## Install development dependencies .PHONY: dev-setup ## Install development dependencies
dev-setup: dev-setup:
pip install -e ".[dev]" pip install -e ".[dev]"
python -m pre_commit install
.PHONY: tests ## Run unit tests .PHONY: tests ## Run unit tests
tests: tests:

155
README.md
View File

@ -1,8 +1,5 @@
# ![Graphene Logo](http://graphene-python.org/favicon.png) Graphene-Django # ![Graphene Logo](http://graphene-python.org/favicon.png) Graphene-Django
A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
[![build][build-image]][build-url] [![build][build-image]][build-url]
[![pypi][pypi-image]][pypi-url] [![pypi][pypi-image]][pypi-url]
[![Anaconda-Server Badge][conda-image]][conda-url] [![Anaconda-Server Badge][conda-image]][conda-url]
@ -17,107 +14,137 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra
[conda-image]: https://img.shields.io/conda/vn/conda-forge/graphene-django.svg [conda-image]: https://img.shields.io/conda/vn/conda-forge/graphene-django.svg
[conda-url]: https://anaconda.org/conda-forge/graphene-django [conda-url]: https://anaconda.org/conda-forge/graphene-django
[💬 Join the community on Slack](https://join.slack.com/t/graphenetools/shared_invite/enQtOTE2MDQ1NTg4MDM1LTA4Nzk0MGU0NGEwNzUxZGNjNDQ4ZjAwNDJjMjY0OGE1ZDgxZTg4YjM2ZTc4MjE2ZTAzZjE2ZThhZTQzZTkyMmM) Graphene-Django is an open-source library that provides seamless integration between Django, a high-level Python web framework, and Graphene, a library for building GraphQL APIs. The library allows developers to create GraphQL APIs in Django quickly and efficiently while maintaining a high level of performance.
## Documentation ## Features
[Visit the documentation to get started!](https://docs.graphene-python.org/projects/django/en/latest/) * Seamless integration with Django models
* Automatic generation of GraphQL schema
* Integration with Django's authentication and permission system
* Easy querying and filtering of data
* Support for Django's pagination system
* Compatible with Django's form and validation system
* Extensive documentation and community support
## Quickstart ## Installation
For installing graphene, just run this command in your shell To install Graphene-Django, run the following command:
```bash ```
pip install "graphene-django>=3" pip install graphene-django
``` ```
### Settings ## Configuration
After installation, add 'graphene_django' to your Django project's `INSTALLED_APPS` list and define the GraphQL schema in your project's settings:
```python ```python
INSTALLED_APPS = ( INSTALLED_APPS = [
# ... # ...
'django.contrib.staticfiles', # Required for GraphiQL
'graphene_django', 'graphene_django',
) ]
GRAPHENE = { GRAPHENE = {
'SCHEMA': 'app.schema.schema' # Where your Graphene schema lives 'SCHEMA': 'myapp.schema.schema'
} }
``` ```
### Urls ## Usage
We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries. To use Graphene-Django, create a `schema.py` file in your Django app directory and define your GraphQL types and queries:
```python ```python
from django.urls import path
from graphene_django.views import GraphQLView
urlpatterns = [
# ...
path('graphql/', GraphQLView.as_view(graphiql=True)),
]
```
## Examples
Here is a simple Django model:
```python
from django.db import models
class UserModel(models.Model):
name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
```
To create a GraphQL schema for it you simply have to write the following:
```python
from graphene_django import DjangoObjectType
import graphene import graphene
from graphene_django import DjangoObjectType
from .models import MyModel
class User(DjangoObjectType): class MyModelType(DjangoObjectType):
class Meta: class Meta:
model = UserModel model = MyModel
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
users = graphene.List(User) mymodels = graphene.List(MyModelType)
def resolve_users(self, info): def resolve_mymodels(self, info, **kwargs):
return UserModel.objects.all() return MyModel.objects.all()
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
``` ```
Then you can query the schema: Then, expose the GraphQL API in your Django project's `urls.py` file:
```python ```python
query = ''' from django.urls import path
query { from graphene_django.views import GraphQLView
users { from . import schema
name,
lastName urlpatterns = [
} # ...
} path('graphql/', GraphQLView.as_view(graphiql=True)), # Given that schema path is defined in GRAPHENE['SCHEMA'] in your settings.py
''' ]
result = schema.execute(query)
``` ```
To learn more check out the following [examples](examples/): ## Testing
* **Schema with Filtering**: [Cookbook example](examples/cookbook) Graphene-Django provides support for testing GraphQL APIs using Django's test client. To create tests, create a `tests.py` file in your Django app directory and write your test cases:
* **Relay Schema**: [Starwars Relay example](examples/starwars)
```python
from django.test import TestCase
from graphene_django.utils.testing import GraphQLTestCase
from . import schema
## GraphQL testing clients class MyModelAPITestCase(GraphQLTestCase):
- [Firecamp](https://firecamp.io/graphql) GRAPHENE_SCHEMA = schema.schema
- [GraphiQL](https://github.com/graphql/graphiql)
def test_query_all_mymodels(self):
response = self.query(
'''
query {
mymodels {
id
name
}
}
'''
)
self.assertResponseNoErrors(response)
self.assertEqual(len(response.data['mymodels']), MyModel.objects.count())
```
## Contributing ## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) Contributions to Graphene-Django are always welcome! To get started, check the repository's [issue tracker](https://github.com/graphql-python/graphene-django/issues) and [contribution guidelines](https://github.com/graphql-python/graphene-django/blob/master/CONTRIBUTING.md).
## License
Graphene-Django is released under the [MIT License](https://github.com/graphql-python/graphene-django/blob/master/LICENSE).
## Resources
* [Official GitHub Repository](https://github.com/graphql-python/graphene-django)
* [Graphene Documentation](http://docs.graphene-python.org/en/latest/)
* [Django Documentation](https://docs.djangoproject.com/en/stable/)
* [GraphQL Specification](https://spec.graphql.org/)
* [GraphiQL](https://github.com/graphql/graphiql) - An in-browser IDE for exploring GraphQL APIs
* [Graphene-Django Community](https://spectrum.chat/graphene) - Join the community to discuss questions and share ideas related to Graphene-Django
## Tutorials and Examples
* [Official Graphene-Django Tutorial](https://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/)
* [Building a GraphQL API with Django and Graphene-Django](https://www.howtographql.com/graphql-python/0-introduction/)
* [Real-world example: Django, Graphene, and Relay](https://github.com/graphql-python/swapi-graphene)
## Related Projects
* [Graphene](https://github.com/graphql-python/graphene) - A library for building GraphQL APIs in Python
* [Graphene-SQLAlchemy](https://github.com/graphql-python/graphene-sqlalchemy) - Integration between Graphene and SQLAlchemy, an Object Relational Mapper (ORM) for Python
* [Graphene-File-Upload](https://github.com/lmcgartland/graphene-file-upload) - A package providing an Upload scalar for handling file uploads in Graphene
* [Graphene-Subscriptions](https://github.com/graphql-python/graphene-subscriptions) - A package for adding real-time subscriptions to Graphene-based GraphQL APIs
## Support
If you encounter any issues or have questions regarding Graphene-Django, feel free to [submit an issue](https://github.com/graphql-python/graphene-django/issues/new) on the official GitHub repository. You can also ask for help and share your experiences with the Graphene-Django community on [💬 Discord](https://discord.gg/Fftt273T79)
## Release Notes ## Release Notes

View File

@ -1,122 +0,0 @@
Please read
`UPGRADE-v2.0.md <https://github.com/graphql-python/graphene/blob/master/UPGRADE-v2.0.md>`__
to learn how to upgrade to Graphene ``2.0``.
--------------
|Graphene Logo| Graphene-Django |Build Status| |PyPI version| |Coverage Status|
===============================================================================
A `Django <https://www.djangoproject.com/>`__ integration for
`Graphene <http://graphene-python.org/>`__.
Documentation
-------------
`Visit the documentation to get started! <https://docs.graphene-python.org/projects/django/en/latest/>`__
Quickstart
----------
For installing graphene, just run this command in your shell
.. code:: bash
pip install "graphene-django>=3"
Settings
~~~~~~~~
.. code:: python
INSTALLED_APPS = (
# ...
'graphene_django',
)
GRAPHENE = {
'SCHEMA': 'app.schema.schema' # Where your Graphene schema lives
}
Urls
~~~~
We need to set up a ``GraphQL`` endpoint in our Django app, so we can
serve the queries.
.. code:: python
from django.conf.urls import url
from graphene_django.views import GraphQLView
urlpatterns = [
# ...
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
]
Examples
--------
Here is a simple Django model:
.. code:: python
from django.db import models
class UserModel(models.Model):
name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
To create a GraphQL schema for it you simply have to write the
following:
.. code:: python
from graphene_django import DjangoObjectType
import graphene
class User(DjangoObjectType):
class Meta:
model = UserModel
class Query(graphene.ObjectType):
users = graphene.List(User)
@graphene.resolve_only_args
def resolve_users(self):
return UserModel.objects.all()
schema = graphene.Schema(query=Query)
Then you can simply query the schema:
.. code:: python
query = '''
query {
users {
name,
lastName
}
}
'''
result = schema.execute(query)
To learn more check out the following `examples <examples/>`__:
- **Schema with Filtering**: `Cookbook example <examples/cookbook>`__
- **Relay Schema**: `Starwars Relay example <examples/starwars>`__
Contributing
------------
See `CONTRIBUTING.md <CONTRIBUTING.md>`__.
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
.. |Build Status| image:: https://github.com/graphql-python/graphene-django/workflows/Tests/badge.svg
:target: https://github.com/graphql-python/graphene-django/actions
.. |PyPI version| image:: https://badge.fury.io/py/graphene-django.svg
:target: https://badge.fury.io/py/graphene-django
.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene-django/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/graphql-python/graphene-django?branch=master

View File

@ -2,8 +2,8 @@ Filtering
========= =========
Graphene integrates with Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/main/>`__ to provide filtering of results. `django-filter <https://django-filter.readthedocs.io/en/stable/>`__ to provide filtering of results.
See the `usage documentation <https://django-filter.readthedocs.io/en/main/guide/usage.html#the-filter>`__ See the `usage documentation <https://django-filter.readthedocs.io/en/stable/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``. for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``. This filtering is automatically available when implementing a ``relay.Node``.

View File

@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = []

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("ingredients", "0001_initial"), ("ingredients", "0001_initial"),
] ]

View File

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("ingredients", "0002_auto_20161104_0050"), ("ingredients", "0002_auto_20161104_0050"),
] ]

View File

@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("recipes", "0001_initial"), ("recipes", "0001_initial"),
] ]

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("recipes", "0002_auto_20161104_0106"), ("recipes", "0002_auto_20161104_0106"),
] ]

View File

@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = []

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("ingredients", "0001_initial"), ("ingredients", "0001_initial"),
] ]

View File

@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [

View File

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("recipes", "0001_initial"), ("recipes", "0001_initial"),
] ]

View File

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

View File

@ -96,7 +96,12 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
def description(self): def description(self):
return str(named_choices_descriptions[self.name]) return str(named_choices_descriptions[self.name])
return_type = Enum(name, list(named_choices), type=EnumWithDescriptionsType) return_type = Enum(
name,
list(named_choices),
type=EnumWithDescriptionsType,
description="An enumeration.", # Temporary fix until https://github.com/graphql-python/graphene/pull/1502 is merged
)
return return_type return return_type
@ -315,26 +320,7 @@ def convert_field_to_djangomodel(field, registry=None):
if not _type: if not _type:
return return
class CustomField(Field): return Field(
def wrap_resolve(self, parent_resolver):
"""
Implements a custom resolver which go through the `get_node` method to ensure that
it goes through the `get_queryset` method of the DjangoObjectType.
"""
resolver = super().wrap_resolve(parent_resolver)
def custom_resolver(root, info, **args):
fk_obj = resolver(root, info, **args)
if not isinstance(fk_obj, model):
# In case the resolver is a custom one that overwrites
# the default Django resolver
# This happens, for example, when using custom awaitable resolvers.
return fk_obj
return _type.get_node(info, fk_obj.pk)
return custom_resolver
return CustomField(
_type, _type,
description=get_django_field_description(field), description=get_django_field_description(field),
required=not field.null, required=not field.null,

View File

@ -87,7 +87,6 @@ def Query(EventType):
events = DjangoFilterConnectionField(EventType) events = DjangoFilterConnectionField(EventType)
def resolve_events(self, info, **kwargs): def resolve_events(self, info, **kwargs):
events = [ events = [
Event(name="Live Show", tags=["concert", "music", "rock"]), Event(name="Live Show", tags=["concert", "music", "rock"]),
Event(name="Musical", tags=["movie", "music"]), Event(name="Musical", tags=["movie", "music"]),

View File

@ -82,7 +82,6 @@ class DjangoFormMutation(BaseDjangoFormMutation):
def __init_subclass_with_meta__( def __init_subclass_with_meta__(
cls, form_class=None, only_fields=(), exclude_fields=(), **options cls, form_class=None, only_fields=(), exclude_fields=(), **options
): ):
if not form_class: if not form_class:
raise Exception("form_class is required for DjangoFormMutation") raise Exception("form_class is required for DjangoFormMutation")
@ -129,7 +128,6 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
exclude_fields=(), exclude_fields=(),
**options, **options,
): ):
if not form_class: if not form_class:
raise Exception("form_class is required for DjangoModelFormMutation") raise Exception("form_class is required for DjangoModelFormMutation")

View File

@ -72,7 +72,6 @@ class SerializerMutation(ClientIDMutation):
_meta=None, _meta=None,
**options **options
): ):
if not serializer_class: if not serializer_class:
raise Exception("serializer_class is required for the SerializerMutation") raise Exception("serializer_class is required for the SerializerMutation")

View File

@ -6,6 +6,7 @@
React, React,
ReactDOM, ReactDOM,
graphqlWs, graphqlWs,
GraphiQLPluginExplorer,
fetch, fetch,
history, history,
location, location,
@ -60,40 +61,27 @@
function trueLambda() { return true; }; function trueLambda() { return true; };
var fetcher = GraphiQL.createFetcher({ 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, url: fetchURL,
wsClient: graphqlWs.createClient({ wsClient: graphqlWs.createClient({
url: subscribeURL, url: subscribeURL,
shouldRetry: trueLambda, shouldRetry: trueLambda,
lazy: true, lazy: true,
}) }),
headers: headers
}) })
function graphQLFetcher(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';
// Parse the cookie value for a CSRF token
var csrftoken;
var cookies = ("; " + document.cookie).split("; csrftoken=");
if (cookies.length == 2) {
csrftoken = cookies.pop().split(";").shift();
} else {
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
}
if (csrftoken) {
headers['X-CSRFToken'] = csrftoken
}
opts.headers = headers
return fetcher(graphQLParams, opts)
}
// When the query and variables string is edited, update the URL bar so // When the query and variables string is edited, update the URL bar so
// that it can be easily shared. // that it can be easily shared.
function onEditQuery(newQuery) { function onEditQuery(newQuery) {
@ -111,24 +99,44 @@
function updateURL() { function updateURL() {
history.replaceState(null, null, locationQuery(parameters)); history.replaceState(null, null, locationQuery(parameters));
} }
var options = {
fetcher: graphQLFetcher, function GraphiQLWithExplorer() {
onEditQuery: onEditQuery, var [query, setQuery] = React.useState(parameters.query);
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName, function handleQuery(query) {
isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, setQuery(query);
shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, onEditQuery(query);
query: parameters.query, }
};
if (parameters.variables) { var explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({
options.variables = parameters.variables; query: query,
} onEdit: handleQuery,
if (parameters.operation_name) { });
options.operationName = parameters.operation_name;
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. // Render <GraphiQL /> into the body.
ReactDOM.render( ReactDOM.render(
React.createElement(GraphiQL, options), React.createElement(GraphiQLWithExplorer),
document.getElementById("editor"), document.getElementById("editor"),
); );
})( })(
@ -139,6 +147,7 @@
window.React, window.React,
window.ReactDOM, window.ReactDOM,
window.graphqlWs, window.graphqlWs,
window.GraphiQLPluginExplorer,
window.fetch, window.fetch,
window.history, window.history,
window.location, window.location,

View File

@ -36,6 +36,9 @@ add "&raw" to the end of the URL within a browser.
<script src="https://cdn.jsdelivr.net/npm/graphql-ws@{{subscriptions_transport_ws_version}}/umd/graphql-ws.min.js" <script src="https://cdn.jsdelivr.net/npm/graphql-ws@{{subscriptions_transport_ws_version}}/umd/graphql-ws.min.js"
integrity="{{subscriptions_transport_ws_sri}}" integrity="{{subscriptions_transport_ws_sri}}"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@graphiql/plugin-explorer@{{graphiql_plugin_explorer_version}}/dist/graphiql-plugin-explorer.umd.js"
integrity="{{graphiql_plugin_explorer_sri}}"
crossorigin="anonymous"></script>
</head> </head>
<body> <body>
<div id="editor"></div> <div id="editor"></div>

View File

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

View File

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

View File

@ -16,6 +16,12 @@ class TestShouldCallGetQuerySetOnForeignKey:
Check that the get_queryset method is called in both forward and reversed direction Check that the get_queryset method is called in both forward and reversed direction
of a foreignkey on types. of a foreignkey on types.
(see issue #1111) (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) @pytest.fixture(autouse=True)
@ -121,69 +127,6 @@ class TestShouldCallGetQuerySetOnForeignKey:
assert not result.errors assert not result.errors
assert result.data == {"reporter": {"firstName": "Jane"}} assert result.data == {"reporter": {"firstName": "Jane"}}
def test_get_queryset_called_on_foreignkey(self):
# If a user tries to access a reporter through an article they should get our authorization error
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(query, variables={"id": self.articles[0].id})
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters through an article
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": self.articles[0].id},
context_value={"admin": True},
)
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
"reporter": {"firstName": "Jane"},
}
# An admin user should not be able to access draft article through a reporter
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
articles {
headline
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": self.reporter.id},
context_value={"admin": True},
)
assert not result.errors
assert result.data["reporter"] == {
"firstName": "Jane",
"articles": [{"headline": "A fantastic article"}],
}
class TestShouldCallGetQuerySetOnForeignKeyNode: class TestShouldCallGetQuerySetOnForeignKeyNode:
""" """
@ -290,72 +233,3 @@ class TestShouldCallGetQuerySetOnForeignKeyNode:
) )
assert not result.errors assert not result.errors
assert result.data == {"reporter": {"firstName": "Jane"}} assert result.data == {"reporter": {"firstName": "Jane"}}
def test_get_queryset_called_on_foreignkey(self):
# If a user tries to access a reporter through an article they should get our authorization error
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(
query, variables={"id": to_global_id("ArticleType", self.articles[0].id)}
)
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters through an article
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": to_global_id("ArticleType", self.articles[0].id)},
context_value={"admin": True},
)
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
"reporter": {"firstName": "Jane"},
}
# An admin user should not be able to access draft article through a reporter
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
articles {
edges {
node {
headline
}
}
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": to_global_id("ReporterType", self.reporter.id)},
context_value={"admin": True},
)
assert not result.errors
assert result.data["reporter"] == {
"firstName": "Jane",
"articles": {"edges": [{"node": {"headline": "A fantastic article"}}]},
}

View File

@ -780,7 +780,6 @@ def test_should_query_promise_connectionfields():
def test_should_query_connectionfields_with_last(): def test_should_query_connectionfields_with_last():
r = Reporter.objects.create( r = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
) )
@ -818,7 +817,6 @@ def test_should_query_connectionfields_with_last():
def test_should_query_connectionfields_with_manager(): def test_should_query_connectionfields_with_manager():
r = Reporter.objects.create( r = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
) )

View File

@ -9,7 +9,7 @@ from django.shortcuts import render
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View from django.views.generic import View
from graphql import OperationType, get_operation_ast, parse, validate from graphql import OperationType, get_operation_ast, parse
from graphql.error import GraphQLError from graphql.error import GraphQLError
from graphql.execution import ExecutionResult from graphql.execution import ExecutionResult
@ -76,6 +76,9 @@ class GraphQLView(View):
"sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw=" "sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw="
) )
graphiql_plugin_explorer_version = "0.1.15"
graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE="
schema = None schema = None
graphiql = False graphiql = False
middleware = None middleware = None
@ -158,6 +161,8 @@ class GraphQLView(View):
graphiql_css_sri=self.graphiql_css_sri, graphiql_css_sri=self.graphiql_css_sri,
subscriptions_transport_ws_version=self.subscriptions_transport_ws_version, subscriptions_transport_ws_version=self.subscriptions_transport_ws_version,
subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri, subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri,
graphiql_plugin_explorer_version=self.graphiql_plugin_explorer_version,
graphiql_plugin_explorer_sri=self.graphiql_plugin_explorer_sri,
# The SUBSCRIPTION_PATH setting. # The SUBSCRIPTION_PATH setting.
subscription_path=self.subscription_path, subscription_path=self.subscription_path,
# GraphiQL headers tab, # GraphiQL headers tab,
@ -303,11 +308,6 @@ class GraphQLView(View):
), ),
) )
) )
validation_errors = validate(self.schema.graphql_schema, document)
if validation_errors:
return ExecutionResult(data=None, errors=validation_errors)
try: try:
extra_options = {} extra_options = {}
if self.execution_context_class: if self.execution_context_class:

View File

@ -14,7 +14,7 @@ rest_framework_require = ["djangorestframework>=3.6.3"]
tests_require = [ tests_require = [
"pytest>=7.1.3", "pytest>=7.3.1",
"pytest-cov", "pytest-cov",
"pytest-random-order", "pytest-random-order",
"coveralls", "coveralls",
@ -26,23 +26,24 @@ tests_require = [
dev_requires = [ dev_requires = [
"black==22.8.0", "black==23.3.0",
"flake8==5.0.4", "flake8==6.0.0",
"flake8-black==0.3.3", "flake8-black==0.3.6",
"flake8-bugbear==22.9.11", "flake8-bugbear==23.3.23",
"pre-commit",
] + tests_require ] + tests_require
setup( setup(
name="graphene-django", name="graphene-django",
version=version, version=version,
description="Graphene Django integration", description="Graphene Django integration",
long_description=open("README.rst").read(), long_description=open("README.md").read(),
url="https://github.com/graphql-python/graphene-django", url="https://github.com/graphql-python/graphene-django",
author="Syrus Akbary", author="Syrus Akbary",
author_email="me@syrusakbary.com", author_email="me@syrusakbary.com",
license="MIT", license="MIT",
classifiers=[ classifiers=[
"Development Status :: 3 - Alpha", "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
@ -50,6 +51,7 @@ setup(
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Framework :: Django", "Framework :: Django",
"Framework :: Django :: 3.2", "Framework :: Django :: 3.2",

View File

@ -2,6 +2,7 @@
envlist = envlist =
py{37,38,39,310}-django32, py{37,38,39,310}-django32,
py{38,39,310}-django{40,41,main}, py{38,39,310}-django{40,41,main},
py311-django{41,main}
pre-commit pre-commit
[gh-actions] [gh-actions]
@ -10,6 +11,7 @@ python =
3.8: py38 3.8: py38
3.9: py39 3.9: py39
3.10: py310 3.10: py310
3.11: py311
[gh-actions:env] [gh-actions:env]
DJANGO = DJANGO =