mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-10 16:22:20 +03:00
Merge branch 'main' into fix-fk-resolver
This commit is contained in:
commit
91ddfbd7f2
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
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.8.6
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
password: ${{ secrets.pypi_password }}
|
password: ${{ secrets.pypi_password }}
|
||||||
|
|
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
@ -8,13 +8,15 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
max-parallel: 4
|
max-parallel: 4
|
||||||
matrix:
|
matrix:
|
||||||
django: ["3.2", "4.0", "4.1"]
|
django: ["3.2", "4.1", "4.2"]
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
include:
|
include:
|
||||||
- django: "3.2"
|
- django: "3.2"
|
||||||
python-version: "3.7"
|
python-version: "3.7"
|
||||||
- django: "4.1"
|
- django: "4.1"
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
- django: "4.2"
|
||||||
|
python-version: "3.11"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -12,6 +12,8 @@ __pycache__/
|
||||||
.Python
|
.Python
|
||||||
env/
|
env/
|
||||||
.env/
|
.env/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
|
@ -81,3 +83,8 @@ Session.vim
|
||||||
tags
|
tags
|
||||||
.tox/
|
.tox/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
.python-version
|
||||||
|
|
|
@ -78,7 +78,7 @@ release = "1.0.dev"
|
||||||
#
|
#
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
# Usually you set "language" from the command line for these cases.
|
# Usually you set "language" from the command line for these cases.
|
||||||
language = None
|
# language = None
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
# non-false value, then it is used:
|
# non-false value, then it is used:
|
||||||
|
@ -445,4 +445,7 @@ epub_exclude_files = ["search.html"]
|
||||||
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {"https://docs.python.org/": None}
|
intersphinx_mapping = {
|
||||||
|
# "https://docs.python.org/": None,
|
||||||
|
"python": ("https://docs.python.org/", None),
|
||||||
|
}
|
||||||
|
|
|
@ -125,6 +125,55 @@ to change how the form is saved or to return a different Graphene object type.
|
||||||
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
|
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
|
||||||
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
|
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
|
||||||
|
|
||||||
|
DjangoFormInputObjectType
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
``DjangoFormInputObjectType`` is used in mutations to create input fields by **using django form** to retrieve input data structure from it. This can be helpful in situations where you need to pass data to several django forms in one mutation.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django.forms.types import DjangoFormInputObjectType
|
||||||
|
|
||||||
|
|
||||||
|
class PetFormInput(DjangoFormInputObjectType):
|
||||||
|
# any other fields can be placed here as well as
|
||||||
|
# other djangoforminputobjects and intputobjects
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
object_type = PetType
|
||||||
|
|
||||||
|
class QuestionFormInput(DjangoFormInputObjectType)
|
||||||
|
class Meta:
|
||||||
|
form_class = QuestionForm
|
||||||
|
object_type = QuestionType
|
||||||
|
|
||||||
|
class SeveralFormsInputData(graphene.InputObjectType):
|
||||||
|
pet = PetFormInput(required=True)
|
||||||
|
question = QuestionFormInput(required=True)
|
||||||
|
|
||||||
|
class SomeSophisticatedMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
data = SeveralFormsInputData(required=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mutate(_root, _info, data):
|
||||||
|
pet_form_inst = PetForm(data=data.pet)
|
||||||
|
question_form_inst = QuestionForm(data=data.question)
|
||||||
|
|
||||||
|
if pet_form_inst.is_valid():
|
||||||
|
pet_model_instance = pet_form_inst.save(commit=False)
|
||||||
|
|
||||||
|
if question_form_inst.is_valid():
|
||||||
|
question_model_instance = question_form_inst.save(commit=False)
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
Additional to **InputObjectType** ``Meta`` class attributes:
|
||||||
|
|
||||||
|
* ``form_class`` is required and should be equal to django form class.
|
||||||
|
* ``object_type`` is not required and used to enable convertion of enum values back to original if model object type ``convert_choices_to_enum`` ``Meta`` class attribute is not set to ``False``. Any data field, which have choices in django, with value ``A_1`` (for example) from client will be automatically converted to ``1`` in mutation data.
|
||||||
|
* ``add_id_field_name`` is used to specify `id` field name (not required, by default equal to ``id``)
|
||||||
|
* ``add_id_field_type`` is used to specify `id` field type (not required, default is ``graphene.ID``)
|
||||||
|
|
||||||
Django REST Framework
|
Django REST Framework
|
||||||
---------------------
|
---------------------
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
Sphinx==1.5.3
|
Sphinx==7.0.0
|
||||||
sphinx-autobuild==0.7.1
|
sphinx-autobuild==2021.3.14
|
||||||
|
pygments-graphql-lexer==0.1.0
|
||||||
# Docs template
|
# Docs template
|
||||||
http://graphene-python.org/sphinx_graphene_theme.zip
|
http://graphene-python.org/sphinx_graphene_theme.zip
|
||||||
|
|
|
@ -224,7 +224,7 @@ Default: ``/graphql``
|
||||||
|
|
||||||
|
|
||||||
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
|
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
|
||||||
---------------------
|
-----------------------------------
|
||||||
|
|
||||||
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
|
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
|
||||||
|
|
||||||
|
|
|
@ -62,3 +62,12 @@ Now head on over to
|
||||||
and run some queries!
|
and run some queries!
|
||||||
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
|
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
|
||||||
for some example queries)
|
for some example queries)
|
||||||
|
|
||||||
|
Testing local graphene-django changes
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
In `requirements.txt`, replace the entire `graphene-django=...` line with the following (so that we install the local version instead of the one from PyPI):
|
||||||
|
|
||||||
|
```
|
||||||
|
../../ # graphene-django
|
||||||
|
```
|
||||||
|
|
|
@ -5,10 +5,10 @@ Django settings for cookbook project.
|
||||||
Generated by 'django-admin startproject' using Django 1.9.
|
Generated by 'django-admin startproject' using Django 1.9.
|
||||||
|
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/1.9/topics/settings/
|
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/1.9/ref/settings/
|
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
@ -18,7 +18,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
|
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
|
||||||
|
@ -81,7 +81,7 @@ WSGI_APPLICATION = "cookbook.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
|
@ -90,9 +90,11 @@ DATABASES = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
|
@ -105,7 +107,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.9/topics/i18n/
|
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = "en-us"
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
@ -119,6 +121,6 @@ USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
graphene>=2.1,<3
|
django~=3.2
|
||||||
graphene-django>=2.1,<3
|
graphene
|
||||||
graphql-core>=2.1,<3
|
graphene-django>=3.1
|
||||||
django==3.1.14
|
|
||||||
|
|
|
@ -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.2"
|
__version__ = "3.1.1"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"__version__",
|
"__version__",
|
||||||
|
|
|
@ -10,16 +10,7 @@ try:
|
||||||
IntegerRangeField,
|
IntegerRangeField,
|
||||||
ArrayField,
|
ArrayField,
|
||||||
HStoreField,
|
HStoreField,
|
||||||
JSONField as PGJSONField,
|
|
||||||
RangeField,
|
RangeField,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
IntegerRangeField, ArrayField, HStoreField, PGJSONField, RangeField = (
|
IntegerRangeField, ArrayField, HStoreField, RangeField = (MissingType,) * 4
|
||||||
MissingType,
|
|
||||||
) * 5
|
|
||||||
|
|
||||||
try:
|
|
||||||
# JSONField is only available from Django 3.1
|
|
||||||
from django.db.models import JSONField
|
|
||||||
except ImportError:
|
|
||||||
JSONField = MissingType
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ except ImportError:
|
||||||
from graphql import assert_valid_name as assert_name
|
from graphql import assert_valid_name as assert_name
|
||||||
from graphql.pyutils import register_description
|
from graphql.pyutils import register_description
|
||||||
|
|
||||||
from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
|
from .compat import ArrayField, HStoreField, RangeField
|
||||||
from .fields import DjangoListField, DjangoConnectionField
|
from .fields import DjangoListField, DjangoConnectionField
|
||||||
from .settings import graphene_settings
|
from .settings import graphene_settings
|
||||||
from .utils.str_converters import to_const
|
from .utils.str_converters import to_const
|
||||||
|
@ -467,9 +467,8 @@ def convert_postgres_array_to_list(field, registry=None):
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(HStoreField)
|
@convert_django_field.register(HStoreField)
|
||||||
@convert_django_field.register(PGJSONField)
|
@convert_django_field.register(models.JSONField)
|
||||||
@convert_django_field.register(JSONField)
|
def convert_json_field_to_string(field, registry=None):
|
||||||
def convert_pg_and_json_field_to_string(field, registry=None):
|
|
||||||
return JSONString(
|
return JSONString(
|
||||||
description=get_django_field_description(field), required=not field.null
|
description=get_django_field_description(field), required=not field.null
|
||||||
)
|
)
|
||||||
|
|
333
graphene_django/forms/tests/test_djangoinputobject.py
Normal file
333
graphene_django/forms/tests/test_djangoinputobject.py
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from ..types import DjangoFormInputObjectType
|
||||||
|
from ...tests.models import Reporter, Film, CHOICES
|
||||||
|
|
||||||
|
# Reporter a_choice CHOICES = ((1, "this"), (2, _("that")))
|
||||||
|
THIS = CHOICES[0][0]
|
||||||
|
THIS_ON_CLIENT_CONVERTED = "A_1"
|
||||||
|
|
||||||
|
# Film genre choices=[("do", "Documentary"), ("ac", "Action"), ("ot", "Other")],
|
||||||
|
DOCUMENTARY = "do"
|
||||||
|
DOCUMENTARY_ON_CLIENT_CONVERTED = "DO"
|
||||||
|
|
||||||
|
|
||||||
|
class FilmForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
exclude = ()
|
||||||
|
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class ReporterForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
exclude = ("pets", "email")
|
||||||
|
|
||||||
|
|
||||||
|
class MyForm(forms.Form):
|
||||||
|
text_field = forms.CharField()
|
||||||
|
int_field = forms.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
|
def test_needs_form_class():
|
||||||
|
with raises(Exception) as exc:
|
||||||
|
|
||||||
|
class MyInputType(DjangoFormInputObjectType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert exc.value.args[0] == "form_class is required for DjangoFormInputObjectType"
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_from_modelform_has_input_fields():
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
only_fields = ("first_name", "last_name", "a_choice")
|
||||||
|
|
||||||
|
fields = ["first_name", "last_name", "a_choice", "id"]
|
||||||
|
assert all(f in ReporterInputType._meta.fields for f in fields)
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_from_form_has_input_fields():
|
||||||
|
class MyFormInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
|
||||||
|
fields = ["text_field", "int_field", "id"]
|
||||||
|
assert all(f in MyFormInputType._meta.fields for f in fields)
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_custom_id_field():
|
||||||
|
class MyFormInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
add_id_field_name = "pk"
|
||||||
|
|
||||||
|
fields = ["text_field", "int_field", "pk"]
|
||||||
|
assert all(f in MyFormInputType._meta.fields for f in fields)
|
||||||
|
assert MyFormInputType._meta.fields["pk"].type is graphene.ID
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_custom_id_field_type():
|
||||||
|
class MyFormInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
add_id_field_name = "pk"
|
||||||
|
add_id_field_type = graphene.String(required=False)
|
||||||
|
|
||||||
|
fields = ["text_field", "int_field", "pk"]
|
||||||
|
assert all(f in MyFormInputType._meta.fields for f in fields)
|
||||||
|
assert MyFormInputType._meta.fields["pk"].type is graphene.String
|
||||||
|
|
||||||
|
|
||||||
|
class MockQuery(graphene.ObjectType):
|
||||||
|
a = graphene.String()
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutation_with_form_djangoforminputtype():
|
||||||
|
class MyFormInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
|
||||||
|
class MyFormMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
form_data = MyFormInputType(required=True)
|
||||||
|
|
||||||
|
result = graphene.Boolean()
|
||||||
|
|
||||||
|
def mutate(_root, _info, form_data):
|
||||||
|
form = MyForm(data=form_data)
|
||||||
|
if form.is_valid():
|
||||||
|
result = form.cleaned_data == {
|
||||||
|
"text_field": "text",
|
||||||
|
"int_field": 777,
|
||||||
|
}
|
||||||
|
return MyFormMutation(result=result)
|
||||||
|
return MyFormMutation(result=False)
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
myForm_mutation = MyFormMutation.Field()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation MyFormMutation($formData: MyFormInputType!) {
|
||||||
|
myFormMutation(formData: $formData) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variable_values={"formData": {"textField": "text", "intField": 777}},
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data == {"myFormMutation": {"result": True}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutation_with_modelform_djangoforminputtype():
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
object_type = ReporterType
|
||||||
|
only_fields = ("first_name", "last_name", "a_choice")
|
||||||
|
|
||||||
|
class ReporterMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
reporter_data = ReporterInputType(required=True)
|
||||||
|
|
||||||
|
result = graphene.Field(ReporterType)
|
||||||
|
|
||||||
|
def mutate(_root, _info, reporter_data):
|
||||||
|
reporter = Reporter.objects.get(pk=reporter_data.id)
|
||||||
|
form = ReporterForm(data=reporter_data, instance=reporter)
|
||||||
|
if form.is_valid():
|
||||||
|
reporter = form.save()
|
||||||
|
return ReporterMutation(result=reporter)
|
||||||
|
|
||||||
|
return ReporterMutation(result=None)
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
report_mutation = ReporterMutation.Field()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
reporter = Reporter.objects.create(
|
||||||
|
first_name="Bob", last_name="Roberts", a_choice=THIS
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation ReportMutation($reporterData: ReporterInputType!) {
|
||||||
|
reportMutation(reporterData: $reporterData) {
|
||||||
|
result {
|
||||||
|
id,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
aChoice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variable_values={
|
||||||
|
"reporterData": {
|
||||||
|
"id": reporter.pk,
|
||||||
|
"firstName": "Dave",
|
||||||
|
"lastName": "Smith",
|
||||||
|
"aChoice": THIS_ON_CLIENT_CONVERTED,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["reportMutation"]["result"] == {
|
||||||
|
"id": "1",
|
||||||
|
"firstName": "Dave",
|
||||||
|
"lastName": "Smith",
|
||||||
|
"aChoice": THIS_ON_CLIENT_CONVERTED,
|
||||||
|
}
|
||||||
|
assert Reporter.objects.count() == 1
|
||||||
|
reporter.refresh_from_db()
|
||||||
|
assert reporter.first_name == "Dave"
|
||||||
|
|
||||||
|
|
||||||
|
def reporter_enum_convert_mutation_result(
|
||||||
|
ReporterInputType, choice_val_on_client=THIS_ON_CLIENT_CONVERTED
|
||||||
|
):
|
||||||
|
class ReporterMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
reporter = ReporterInputType(required=True)
|
||||||
|
|
||||||
|
result_str = graphene.String()
|
||||||
|
result_int = graphene.Int()
|
||||||
|
|
||||||
|
def mutate(_root, _info, reporter):
|
||||||
|
if isinstance(reporter.a_choice, int) or reporter.a_choice.isdigit():
|
||||||
|
return ReporterMutation(result_int=reporter.a_choice, result_str=None)
|
||||||
|
return ReporterMutation(result_int=None, result_str=reporter.a_choice)
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
report_mutation = ReporterMutation.Field()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
return schema.execute(
|
||||||
|
""" mutation ReportMutation($reporter: ReporterInputType!) {
|
||||||
|
reportMutation(reporter: $reporter) {
|
||||||
|
resultStr,
|
||||||
|
resultInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variable_values={"reporter": {"aChoice": choice_val_on_client}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_not_converted():
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
only_fields = ("a_choice",)
|
||||||
|
|
||||||
|
result = reporter_enum_convert_mutation_result(ReporterInputType)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["reportMutation"]["resultStr"] == THIS_ON_CLIENT_CONVERTED
|
||||||
|
assert result.data["reportMutation"]["resultInt"] is None
|
||||||
|
assert ReporterInputType._meta.fields["a_choice"].type is graphene.String
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_is_converted_to_original():
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
object_type = ReporterType
|
||||||
|
only_fields = ("a_choice",)
|
||||||
|
|
||||||
|
result = reporter_enum_convert_mutation_result(ReporterInputType)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["reportMutation"]["resultInt"] == THIS
|
||||||
|
assert result.data["reportMutation"]["resultStr"] is None
|
||||||
|
assert (
|
||||||
|
ReporterInputType._meta.fields["a_choice"].type.__name__
|
||||||
|
== "AChoiceEnumBackConvString"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_choices_to_enum_is_false_and_field_type_as_in_model():
|
||||||
|
class ReporterTypeNotConvertChoices(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
convert_choices_to_enum = False
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
object_type = ReporterTypeNotConvertChoices
|
||||||
|
only_fields = ("a_choice",)
|
||||||
|
|
||||||
|
result = reporter_enum_convert_mutation_result(ReporterInputType, THIS)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["reportMutation"]["resultInt"] == THIS
|
||||||
|
assert result.data["reportMutation"]["resultStr"] is None
|
||||||
|
assert ReporterInputType._meta.fields["a_choice"].type is graphene.Int
|
||||||
|
|
||||||
|
|
||||||
|
def enum_convert_mutation_result_film(FilmInputType):
|
||||||
|
class FilmMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
film = FilmInputType(required=True)
|
||||||
|
|
||||||
|
result = graphene.String()
|
||||||
|
|
||||||
|
def mutate(_root, _info, film):
|
||||||
|
return FilmMutation(result=film.genre)
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
film_mutation = FilmMutation.Field()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
return schema.execute(
|
||||||
|
""" mutation FilmMutation($film: FilmInputType!) {
|
||||||
|
filmMutation(film: $film) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variable_values={"film": {"genre": DOCUMENTARY_ON_CLIENT_CONVERTED}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_not_converted_required_non_number():
|
||||||
|
class FilmInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = FilmForm
|
||||||
|
only_fields = ("genre",)
|
||||||
|
|
||||||
|
result = enum_convert_mutation_result_film(FilmInputType)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["filmMutation"]["result"] == DOCUMENTARY_ON_CLIENT_CONVERTED
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_is_converted_to_original_required_non_number():
|
||||||
|
class FilmType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class FilmInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = FilmForm
|
||||||
|
object_type = FilmType
|
||||||
|
only_fields = ("genre",)
|
||||||
|
|
||||||
|
result = enum_convert_mutation_result_film(FilmInputType)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["filmMutation"]["result"] == DOCUMENTARY
|
|
@ -1 +1,118 @@
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from graphene import ID
|
||||||
|
from graphene.types.inputobjecttype import InputObjectType
|
||||||
|
from graphene.utils.str_converters import to_camel_case
|
||||||
|
|
||||||
|
from .mutation import fields_for_form
|
||||||
from ..types import ErrorType # noqa Import ErrorType for backwards compatability
|
from ..types import ErrorType # noqa Import ErrorType for backwards compatability
|
||||||
|
from ..converter import BlankValueField
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoFormInputObjectType(InputObjectType):
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass_with_meta__(
|
||||||
|
cls,
|
||||||
|
container=None,
|
||||||
|
_meta=None,
|
||||||
|
only_fields=(),
|
||||||
|
exclude_fields=(),
|
||||||
|
form_class=None,
|
||||||
|
object_type=None,
|
||||||
|
add_id_field_name=None,
|
||||||
|
add_id_field_type=None,
|
||||||
|
**options,
|
||||||
|
):
|
||||||
|
"""Retrieve fields from django form (Meta.form_class). Received
|
||||||
|
fields are set to cls (they will be converted to input fields
|
||||||
|
by InputObjectType). Type of fields with choices (converted
|
||||||
|
to enum) is set to custom scalar type (using Meta.object_type)
|
||||||
|
to dynamically convert enum values back.
|
||||||
|
|
||||||
|
class MyDjangoFormInput(DjangoFormInputObjectType):
|
||||||
|
# any other fields can be placed here and other inputobjectforms as well
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
form_class = MyDjangoModelForm
|
||||||
|
object_type = MyModelType
|
||||||
|
|
||||||
|
class SomeMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
data = MyDjangoFormInput(required=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mutate(_root, _info, data):
|
||||||
|
form_inst = MyDjangoModelForm(data=data)
|
||||||
|
if form_inst.is_valid():
|
||||||
|
django_model_instance = form_inst.save(commit=False)
|
||||||
|
# ... etc ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not form_class:
|
||||||
|
raise Exception("form_class is required for DjangoFormInputObjectType")
|
||||||
|
|
||||||
|
form = form_class()
|
||||||
|
form_fields = fields_for_form(form, only_fields, exclude_fields)
|
||||||
|
|
||||||
|
for name, field in form_fields.items():
|
||||||
|
if (
|
||||||
|
object_type
|
||||||
|
and name in object_type._meta.fields
|
||||||
|
and isinstance(object_type._meta.fields[name], BlankValueField)
|
||||||
|
):
|
||||||
|
# Field type BlankValueField here means that field
|
||||||
|
# with choises have been converted to enum
|
||||||
|
# (BlankValueField is using only for that task ?)
|
||||||
|
setattr(cls, name, cls.get_enum_cnv_cls_instance(name, object_type))
|
||||||
|
elif (
|
||||||
|
object_type
|
||||||
|
and name in object_type._meta.fields
|
||||||
|
and object_type._meta.convert_choices_to_enum is False
|
||||||
|
and form.fields[name].__class__.__name__ == "TypedChoiceField"
|
||||||
|
):
|
||||||
|
# FIXME
|
||||||
|
# in case if convert_choices_to_enum is False
|
||||||
|
# form field class is converted to String but original
|
||||||
|
# model field type is needed here... (.converter.py bug?)
|
||||||
|
# This is temp workaround to get field type from ObjectType field
|
||||||
|
# TEST: test_enum_not_converted_and_field_type_as_in_model
|
||||||
|
setattr(cls, name, object_type._meta.fields[name].type())
|
||||||
|
else:
|
||||||
|
# set input field according to django form field
|
||||||
|
setattr(cls, name, field)
|
||||||
|
|
||||||
|
# explicitly adding id field (absent in django form fields)
|
||||||
|
# with name and type from Meta or 'id' with graphene.ID by default
|
||||||
|
if add_id_field_name:
|
||||||
|
setattr(cls, add_id_field_name, add_id_field_type or ID(required=False))
|
||||||
|
elif "id" not in exclude_fields:
|
||||||
|
cls.id = ID(required=False)
|
||||||
|
|
||||||
|
super().__init_subclass_with_meta__(container=container, _meta=_meta, **options)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_enum_cnv_cls_instance(field_name, object_type):
|
||||||
|
"""Saves args in context to convert enum values in
|
||||||
|
Dynamically created Scalar derived class
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_value(value):
|
||||||
|
# field_name & object_type have been saved in context (closure)
|
||||||
|
field = object_type._meta.fields[field_name]
|
||||||
|
if isinstance(field.type, graphene.NonNull):
|
||||||
|
val_before_convert = field.type._of_type[value].value
|
||||||
|
else:
|
||||||
|
val_before_convert = field.type[value].value
|
||||||
|
return graphene.String.parse_value(val_before_convert)
|
||||||
|
|
||||||
|
cls_doc = "String scalar to convert choice value back from enum to original"
|
||||||
|
scalar_type = type(
|
||||||
|
(
|
||||||
|
f"{field_name[0].upper()}{to_camel_case(field_name[1:])}"
|
||||||
|
"EnumBackConvString"
|
||||||
|
),
|
||||||
|
(graphene.String,),
|
||||||
|
{"parse_value": parse_value, "__doc__": cls_doc},
|
||||||
|
)
|
||||||
|
return scalar_type()
|
||||||
|
|
|
@ -39,6 +39,9 @@ def fields_for_serializer(
|
||||||
field.read_only
|
field.read_only
|
||||||
and is_input
|
and is_input
|
||||||
and lookup_field != name, # don't show read_only fields in Input
|
and lookup_field != name, # don't show read_only fields in Input
|
||||||
|
isinstance(
|
||||||
|
field, serializers.HiddenField
|
||||||
|
), # don't show hidden fields in Input
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,21 @@ def test_read_only_fields():
|
||||||
), "'cool_name' is read_only field and shouldn't be on arguments"
|
), "'cool_name' is read_only field and shouldn't be on arguments"
|
||||||
|
|
||||||
|
|
||||||
|
def test_hidden_fields():
|
||||||
|
class SerializerWithHiddenField(serializers.Serializer):
|
||||||
|
cool_name = serializers.CharField()
|
||||||
|
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
|
||||||
|
|
||||||
|
class MyMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = SerializerWithHiddenField
|
||||||
|
|
||||||
|
assert "cool_name" in MyMutation.Input._meta.fields
|
||||||
|
assert (
|
||||||
|
"user" not in MyMutation.Input._meta.fields
|
||||||
|
), "'user' is hidden field and shouldn't be on arguments"
|
||||||
|
|
||||||
|
|
||||||
def test_nested_model():
|
def test_nested_model():
|
||||||
class MyFakeModelGrapheneType(DjangoObjectType):
|
class MyFakeModelGrapheneType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -21,6 +21,10 @@ add "&raw" to the end of the URL within a browser.
|
||||||
integrity="{{graphiql_css_sri}}"
|
integrity="{{graphiql_css_sri}}"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
crossorigin="anonymous" />
|
crossorigin="anonymous" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/@graphiql/plugin-explorer@{{graphiql_plugin_explorer_version}}/dist/style.css"
|
||||||
|
integrity="{{graphiql_plugin_explorer_css_sri}}"
|
||||||
|
rel="stylesheet"
|
||||||
|
crossorigin="anonymous" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@{{whatwg_fetch_version}}/dist/fetch.umd.js"
|
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@{{whatwg_fetch_version}}/dist/fetch.umd.js"
|
||||||
integrity="{{whatwg_fetch_sri}}"
|
integrity="{{whatwg_fetch_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
|
|
@ -43,7 +43,7 @@ class Reporter(models.Model):
|
||||||
last_name = models.CharField(max_length=30)
|
last_name = models.CharField(max_length=30)
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
pets = models.ManyToManyField("self")
|
pets = models.ManyToManyField("self")
|
||||||
a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True)
|
a_choice = models.IntegerField(choices=CHOICES, null=True, blank=True)
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
doe_objects = DoeReporterManager()
|
doe_objects = DoeReporterManager()
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ def test_generate_graphql_file_on_call_graphql_schema():
|
||||||
open_mock.assert_called_once()
|
open_mock.assert_called_once()
|
||||||
|
|
||||||
handle = open_mock()
|
handle = open_mock()
|
||||||
assert handle.write.called_once()
|
handle.write.assert_called_once()
|
||||||
|
|
||||||
schema_output = handle.write.call_args[0][0]
|
schema_output = handle.write.call_args[0][0]
|
||||||
assert schema_output == dedent(
|
assert schema_output == dedent(
|
||||||
|
|
|
@ -15,8 +15,6 @@ from graphene.types.scalars import BigInt
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
ArrayField,
|
ArrayField,
|
||||||
HStoreField,
|
HStoreField,
|
||||||
JSONField,
|
|
||||||
PGJSONField,
|
|
||||||
MissingType,
|
MissingType,
|
||||||
RangeField,
|
RangeField,
|
||||||
)
|
)
|
||||||
|
@ -372,16 +370,6 @@ def test_should_postgres_hstore_convert_string():
|
||||||
assert_conversion(HStoreField, JSONString)
|
assert_conversion(HStoreField, JSONString)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(PGJSONField is MissingType, reason="PGJSONField should exist")
|
|
||||||
def test_should_postgres_json_convert_string():
|
|
||||||
assert_conversion(PGJSONField, JSONString)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(JSONField is MissingType, reason="JSONField should exist")
|
|
||||||
def test_should_json_convert_string():
|
|
||||||
assert_conversion(JSONField, JSONString)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(RangeField is MissingType, reason="RangeField should exist")
|
@pytest.mark.skipif(RangeField is MissingType, reason="RangeField should exist")
|
||||||
def test_should_postgres_range_convert_list():
|
def test_should_postgres_range_convert_list():
|
||||||
from django.contrib.postgres.fields import IntegerRangeField
|
from django.contrib.postgres.fields import IntegerRangeField
|
||||||
|
|
|
@ -119,13 +119,12 @@ def test_should_query_postgres_fields():
|
||||||
from django.contrib.postgres.fields import (
|
from django.contrib.postgres.fields import (
|
||||||
IntegerRangeField,
|
IntegerRangeField,
|
||||||
ArrayField,
|
ArrayField,
|
||||||
JSONField,
|
|
||||||
HStoreField,
|
HStoreField,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Event(models.Model):
|
class Event(models.Model):
|
||||||
ages = IntegerRangeField(help_text="The age ranges")
|
ages = IntegerRangeField(help_text="The age ranges")
|
||||||
data = JSONField(help_text="Data")
|
data = models.JSONField(help_text="Data")
|
||||||
store = HStoreField()
|
store = HStoreField()
|
||||||
tags = ArrayField(models.CharField(max_length=50))
|
tags = ArrayField(models.CharField(max_length=50))
|
||||||
|
|
||||||
|
|
|
@ -252,6 +252,7 @@ class DjangoObjectType(ObjectType):
|
||||||
_meta.filterset_class = filterset_class
|
_meta.filterset_class = filterset_class
|
||||||
_meta.fields = django_fields
|
_meta.fields = django_fields
|
||||||
_meta.connection = connection
|
_meta.connection = connection
|
||||||
|
_meta.convert_choices_to_enum = convert_choices_to_enum
|
||||||
|
|
||||||
super().__init_subclass_with_meta__(
|
super().__init_subclass_with_meta__(
|
||||||
_meta=_meta, interfaces=interfaces, **options
|
_meta=_meta, interfaces=interfaces, **options
|
||||||
|
|
|
@ -66,18 +66,21 @@ class GraphQLView(View):
|
||||||
react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0="
|
react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0="
|
||||||
|
|
||||||
# The GraphiQL React app.
|
# The GraphiQL React app.
|
||||||
graphiql_version = "2.4.1" # "1.0.3"
|
graphiql_version = "2.4.7"
|
||||||
graphiql_sri = "sha256-s+f7CFAPSUIygFnRC2nfoiEKd3liCUy+snSdYFAoLUc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
|
graphiql_sri = "sha256-n/LKaELupC1H/PU6joz+ybeRJHT2xCdekEt6OYMOOZU="
|
||||||
graphiql_css_sri = "sha256-88yn8FJMyGboGs4Bj+Pbb3kWOWXo7jmb+XCRHE+282k=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
|
graphiql_css_sri = "sha256-OsbM+LQHcnFHi0iH7AUKueZvDcEBoy/z4hJ7jx1cpsM="
|
||||||
|
|
||||||
# The websocket transport library for subscriptions.
|
# The websocket transport library for subscriptions.
|
||||||
subscriptions_transport_ws_version = "5.12.1"
|
subscriptions_transport_ws_version = "5.13.1"
|
||||||
subscriptions_transport_ws_sri = (
|
subscriptions_transport_ws_sri = (
|
||||||
"sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw="
|
"sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw="
|
||||||
)
|
)
|
||||||
|
|
||||||
graphiql_plugin_explorer_version = "0.1.15"
|
graphiql_plugin_explorer_version = "0.1.15"
|
||||||
graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE="
|
graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE="
|
||||||
|
graphiql_plugin_explorer_css_sri = (
|
||||||
|
"sha256-fA0LPUlukMNR6L4SPSeFqDTYav8QdWjQ2nr559Zln1U="
|
||||||
|
)
|
||||||
|
|
||||||
schema = None
|
schema = None
|
||||||
graphiql = False
|
graphiql = False
|
||||||
|
@ -105,17 +108,19 @@ class GraphQLView(View):
|
||||||
if middleware is None:
|
if middleware is None:
|
||||||
middleware = graphene_settings.MIDDLEWARE
|
middleware = graphene_settings.MIDDLEWARE
|
||||||
|
|
||||||
self.schema = self.schema or schema
|
self.schema = schema or self.schema
|
||||||
if middleware is not None:
|
if middleware is not None:
|
||||||
if isinstance(middleware, MiddlewareManager):
|
if isinstance(middleware, MiddlewareManager):
|
||||||
self.middleware = middleware
|
self.middleware = middleware
|
||||||
else:
|
else:
|
||||||
self.middleware = list(instantiate_middleware(middleware))
|
self.middleware = list(instantiate_middleware(middleware))
|
||||||
self.root_value = root_value
|
self.root_value = root_value
|
||||||
self.pretty = self.pretty or pretty
|
self.pretty = pretty or self.pretty
|
||||||
self.graphiql = self.graphiql or graphiql
|
self.graphiql = graphiql or self.graphiql
|
||||||
self.batch = self.batch or batch
|
self.batch = batch or self.batch
|
||||||
self.execution_context_class = execution_context_class
|
self.execution_context_class = (
|
||||||
|
execution_context_class or self.execution_context_class
|
||||||
|
)
|
||||||
if subscription_path is None:
|
if subscription_path is None:
|
||||||
self.subscription_path = graphene_settings.SUBSCRIPTION_PATH
|
self.subscription_path = graphene_settings.SUBSCRIPTION_PATH
|
||||||
|
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -38,6 +38,7 @@ setup(
|
||||||
version=version,
|
version=version,
|
||||||
description="Graphene Django integration",
|
description="Graphene Django integration",
|
||||||
long_description=open("README.md").read(),
|
long_description=open("README.md").read(),
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
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",
|
||||||
|
@ -55,8 +56,8 @@ setup(
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
"Framework :: Django",
|
"Framework :: Django",
|
||||||
"Framework :: Django :: 3.2",
|
"Framework :: Django :: 3.2",
|
||||||
"Framework :: Django :: 4.0",
|
|
||||||
"Framework :: Django :: 4.1",
|
"Framework :: Django :: 4.1",
|
||||||
|
"Framework :: Django :: 4.2",
|
||||||
],
|
],
|
||||||
keywords="api graphql protocol rest relay graphene",
|
keywords="api graphql protocol rest relay graphene",
|
||||||
packages=find_packages(exclude=["tests", "examples", "examples.*"]),
|
packages=find_packages(exclude=["tests", "examples", "examples.*"]),
|
||||||
|
|
8
tox.ini
8
tox.ini
|
@ -1,8 +1,8 @@
|
||||||
[tox]
|
[tox]
|
||||||
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{41,42,main},
|
||||||
py311-django{41,main}
|
py311-django{41,42,main}
|
||||||
pre-commit
|
pre-commit
|
||||||
|
|
||||||
[gh-actions]
|
[gh-actions]
|
||||||
|
@ -16,8 +16,8 @@ python =
|
||||||
[gh-actions:env]
|
[gh-actions:env]
|
||||||
DJANGO =
|
DJANGO =
|
||||||
3.2: django32
|
3.2: django32
|
||||||
4.0: django40
|
|
||||||
4.1: django41
|
4.1: django41
|
||||||
|
4.2: django42
|
||||||
main: djangomain
|
main: djangomain
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
@ -30,8 +30,8 @@ deps =
|
||||||
-e.[test]
|
-e.[test]
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
django32: Django>=3.2,<4.0
|
django32: Django>=3.2,<4.0
|
||||||
django40: Django>=4.0,<4.1
|
|
||||||
django41: Django>=4.1,<4.2
|
django41: Django>=4.1,<4.2
|
||||||
|
django42: Django>=4.2,<4.3
|
||||||
djangomain: https://github.com/django/django/archive/main.zip
|
djangomain: https://github.com/django/django/archive/main.zip
|
||||||
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
|
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user