From 0b144e6ef86424ebeedaef8cb1ea5e0b98ea20ef Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 7 Nov 2024 16:13:22 -0500 Subject: [PATCH] Revert "Merge branch 'main' into main" This reverts commit eeaa2234b4a48a72381aaa4dc4496d3d33c3b23f, reversing changes made to 6928b3fb6c58655b23efd4d524ccfc9ce0bb695b. --- .github/ISSUE_TEMPLATE/bug_report.md | 6 +- .github/workflows/deploy.yml | 10 +- .github/workflows/lint.yml | 12 +- .github/workflows/tests.yml | 14 +- .pre-commit-config.yaml | 30 -- CONTRIBUTING.md | 2 +- MANIFEST.in | 2 +- Makefile | 22 +- README.md | 2 +- docs/authorization.rst | 2 +- docs/filtering.rst | 10 +- docs/queries.rst | 40 +-- docs/schema.py | 79 ++--- docs/settings.rst | 35 +- docs/testing.rst | 13 +- docs/tutorial-plain.rst | 13 - docs/tutorial-relay.rst | 2 +- examples/cookbook-plain/README.md | 2 +- .../ingredients/fixtures/ingredients.json | 53 +-- .../ingredients/migrations/0001_initial.py | 45 +-- .../migrations/0002_auto_20161104_0050.py | 9 +- .../migrations/0003_auto_20181018_1746.py | 7 +- .../cookbook/ingredients/schema.py | 2 +- .../recipes/migrations/0001_initial.py | 61 +--- .../migrations/0002_auto_20161104_0106.py | 25 +- .../migrations/0003_auto_20181018_1728.py | 17 +- .../cookbook-plain/cookbook/recipes/schema.py | 2 +- examples/cookbook/README.md | 4 +- .../ingredients/fixtures/ingredients.json | 53 +-- .../ingredients/migrations/0001_initial.py | 45 +-- .../migrations/0002_auto_20161104_0050.py | 9 +- .../cookbook/cookbook/ingredients/schema.py | 2 +- .../recipes/migrations/0001_initial.py | 61 +--- .../migrations/0002_auto_20161104_0106.py | 25 +- examples/cookbook/cookbook/recipes/schema.py | 2 +- examples/cookbook/dummy_data.json | 303 +----------------- examples/starwars/models.py | 2 + graphene_django/__init__.py | 2 +- graphene_django/compat.py | 2 +- graphene_django/converter.py | 39 +-- graphene_django/debug/exception/formating.py | 2 +- graphene_django/debug/middleware.py | 42 +-- graphene_django/debug/sql/tracking.py | 7 +- graphene_django/debug/tests/test_query.py | 2 +- graphene_django/fields.py | 53 ++- graphene_django/filter/fields.py | 8 +- .../filter/filters/array_filter.py | 2 +- .../filter/filters/global_id_filter.py | 6 +- graphene_django/filter/filters/list_filter.py | 2 +- .../filter/filters/typed_filter.py | 2 +- graphene_django/filter/filterset.py | 14 +- graphene_django/filter/tests/conftest.py | 11 +- .../tests/test_array_field_exact_filter.py | 5 +- .../filter/tests/test_enum_filtering.py | 19 +- graphene_django/filter/tests/test_fields.py | 36 +-- .../filter/tests/test_in_filter.py | 22 +- .../filter/tests/test_typed_filter.py | 14 +- graphene_django/filter/utils.py | 4 +- graphene_django/forms/converter.py | 22 +- graphene_django/forms/mutation.py | 12 +- graphene_django/forms/tests/test_converter.py | 7 +- graphene_django/forms/tests/test_mutation.py | 2 +- .../management/commands/graphql_schema.py | 12 +- graphene_django/registry.py | 2 +- graphene_django/rest_framework/mutation.py | 3 +- .../rest_framework/serializer_converter.py | 8 +- .../tests/test_field_converter.py | 6 +- .../rest_framework/tests/test_mutation.py | 2 +- graphene_django/settings.py | 7 +- .../static/graphene_django/graphiql.js | 169 ++++++---- .../templates/graphene/graphiql.html | 6 +- graphene_django/tests/issues/test_520.py | 4 +- graphene_django/tests/models.py | 13 +- graphene_django/tests/schema_view.py | 1 + graphene_django/tests/test_command.py | 5 +- graphene_django/tests/test_converter.py | 7 +- graphene_django/tests/test_fields.py | 153 +-------- graphene_django/tests/test_forms.py | 2 +- graphene_django/tests/test_get_queryset.py | 235 -------------- graphene_django/tests/test_query.py | 224 ++----------- graphene_django/tests/test_schema.py | 2 +- graphene_django/tests/test_types.py | 26 +- graphene_django/tests/test_utils.py | 10 +- graphene_django/tests/test_views.py | 9 +- graphene_django/tests/urls.py | 6 +- graphene_django/tests/urls_inherited.py | 4 +- graphene_django/tests/urls_pretty.py | 4 +- graphene_django/types.py | 14 +- graphene_django/utils/testing.py | 16 +- .../utils/tests/test_str_converters.py | 2 +- graphene_django/utils/tests/test_testing.py | 9 - graphene_django/views.py | 21 +- setup.cfg | 2 +- setup.py | 28 +- tox.ini | 38 ++- 95 files changed, 636 insertions(+), 1774 deletions(-) delete mode 100644 .pre-commit-config.yaml delete mode 100644 graphene_django/tests/test_get_queryset.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 26d84aa..2c933d7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 139c6f6..07c0766 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,17 +10,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v4 + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 with: - python-version: '3.11' + python-version: 3.9 - 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.8.6 + uses: pypa/gh-action-pypi-publish@v1.1.0 with: user: __token__ password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bfafa67..559326c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,16 +7,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v4 + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 with: - python-version: '3.11' + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox - - name: Run pre-commit 💅 + - name: Run lint 💅 run: tox env: - TOXENV: pre-commit + TOXENV: flake8 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2c5b755..2dbf822 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,17 +8,13 @@ jobs: strategy: max-parallel: 4 matrix: - 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" + django: ["2.2", "3.0", "3.1", "3.2"] + python-version: ["3.6", "3.7", "3.8", "3.9"] + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 9214d35..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,30 +0,0 @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a226ab..f9428e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,4 +59,4 @@ Then to produce a HTML version of the documentation: ```sh make html -``` +``` \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 1ede730..045af08 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 \ No newline at end of file diff --git a/Makefile b/Makefile index 29c412b..b850ae8 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,22 @@ -.PHONY: help -help: - @echo "Please use \`make ' where 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: tests ## Run unit tests +.PHONY: install-dev +install-dev: dev-setup # Alias install-dev -> dev-setup + +.PHONY: tests tests: py.test graphene_django --cov=graphene_django -vv -.PHONY: format ## Format code -format: - black graphene_django examples setup.py +.PHONY: test +test: tests # Alias test -> tests -.PHONY: lint ## Lint code +.PHONY: format +format: + black --exclude "/migrations/" graphene_django examples setup.py + +.PHONY: lint lint: flake8 graphene_django examples diff --git a/README.md b/README.md index 6f06ccc..5045e78 100644 --- a/README.md +++ b/README.md @@ -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)), ] ``` diff --git a/docs/authorization.rst b/docs/authorization.rst index bc88cda..39305f6 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -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 diff --git a/docs/filtering.rst b/docs/filtering.rst index 95576a0..934bad6 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -2,8 +2,8 @@ Filtering ========= Graphene integrates with -`django-filter `__ to provide filtering of results. -See the `usage documentation `__ +`django-filter `__ to provide filtering of results. +See the `usage documentation `__ 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 `__. +app `__. 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 `__ +documentation `__ 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 `__ +The context argument is passed on as the `request argument `__ 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``). diff --git a/docs/queries.rst b/docs/queries.rst index 8b85d45..1e1ba82 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -151,7 +151,7 @@ For example the following ``Model`` and ``DjangoObjectType``: Results in the following GraphQL schema definition: -.. code:: graphql +.. code:: 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:: graphql +.. code:: 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=None, bar=None): + def resolve_question(root, info, foo, bar): # 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:: graphql +.. code:: python { 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:: json +.. code:: python { "data": { diff --git a/docs/schema.py b/docs/schema.py index 058a587..1de92ed 100644 --- a/docs/schema.py +++ b/docs/schema.py @@ -1,57 +1,60 @@ -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: - category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) - all_categories = graphene.List(CategoryType) + class Query(object): + 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) - def resolve_all_categories(self, info, **kwargs): - return Category.objects.all() + ingredient = graphene.Field(IngredientType, + id=graphene.Int(), + name=graphene.String()) + all_ingredients = graphene.List(IngredientType) - def resolve_all_ingredients(self, info, **kwargs): - return Ingredient.objects.all() + def resolve_all_categories(self, info, **kwargs): + return Category.objects.all() - def resolve_category(self, info, **kwargs): - id = kwargs.get("id") - name = kwargs.get("name") + def resolve_all_ingredients(self, info, **kwargs): + return Ingredient.objects.all() - if id is not None: - return Category.objects.get(pk=id) + def resolve_category(self, info, **kwargs): + id = kwargs.get('id') + name = kwargs.get('name') - if name is not None: - return Category.objects.get(name=name) + if id is not None: + return Category.objects.get(pk=id) - return None + if name is not None: + return Category.objects.get(name=name) - def resolve_ingredient(self, info, **kwargs): - id = kwargs.get("id") - name = kwargs.get("name") + return None - if id is not None: - return Ingredient.objects.get(pk=id) + def resolve_ingredient(self, info, **kwargs): + id = kwargs.get('id') + name = kwargs.get('name') - if name is not None: - return Ingredient.objects.get(name=name) + if id is not None: + return Ingredient.objects.get(pk=id) - return None + if name is not None: + return Ingredient.objects.get(name=name) + + return None \ No newline at end of file diff --git a/docs/settings.rst b/docs/settings.rst index 5bffd08..ff1c05e 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -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,36 +207,3 @@ 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, - } diff --git a/docs/testing.rst b/docs/testing.rst index 1b32352..65b6f64 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -6,8 +6,7 @@ Using unittest If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`. -The default endpoint for testing is `/graphql`. You can override this in the `settings `__. - +Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLTestCase`. The default endpoint is `GRAPHQL_URL = "/graphql/"`. Usage: @@ -28,7 +27,7 @@ Usage: } } ''', - operation_name='myModel' + op_name='myModel' ) content = json.loads(response.content) @@ -49,7 +48,7 @@ Usage: } } ''', - operation_name='myModel', + op_name='myModel', variables={'id': 1} ) @@ -73,7 +72,7 @@ Usage: } } ''', - operation_name='myMutation', + op_name='myMutation', input_data={'my_field': 'foo', 'other_field': 'bar'} ) @@ -108,7 +107,7 @@ Usage: } } ''', - operation_name='myMutation', + op_name='myMutation', input_data={'my_field': 'foo', 'other_field': 'bar'} ) @@ -148,7 +147,7 @@ To use pytest define a simple fixture using the query helper below } } ''', - operation_name='myModel' + op_name='myModel' ) content = json.loads(response.content) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 9073c82..45927a5 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -35,7 +35,6 @@ Now sync your database for the first time: .. code:: bash - cd .. python manage.py migrate Let's create a few simple models... @@ -78,18 +77,6 @@ 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: diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index a27e255..3de9022 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -151,7 +151,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: interfaces = (relay.Node, ) - class Query(ObjectType): + class Query(graphene.ObjectType): category = relay.Node.Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) diff --git a/examples/cookbook-plain/README.md b/examples/cookbook-plain/README.md index dcd2420..0ec906b 100644 --- a/examples/cookbook-plain/README.md +++ b/examples/cookbook-plain/README.md @@ -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-plain +cd graphene-django/examples/cookbook ``` It is good idea (but not required) to create a virtual environment diff --git a/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json b/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json index 213bd8b..8625d3c 100644 --- a/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json +++ b/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json @@ -1,52 +1 @@ -[ - { - "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 - } -] +[{"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}}] \ No newline at end of file diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py index 23d71e8..0494923 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py @@ -1,52 +1,33 @@ +# -*- 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')), ], ), ] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py index 5f9e7a0..359d4fc 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -1,17 +1,20 @@ +# -*- 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), ), ] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py index e823a2e..184e79e 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py @@ -4,13 +4,14 @@ 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'}, ), ] diff --git a/examples/cookbook-plain/cookbook/ingredients/schema.py b/examples/cookbook-plain/cookbook/ingredients/schema.py index b8de8f9..24a5e95 100644 --- a/examples/cookbook-plain/cookbook/ingredients/schema.py +++ b/examples/cookbook-plain/cookbook/ingredients/schema.py @@ -16,7 +16,7 @@ class IngredientType(DjangoObjectType): fields = "__all__" -class Query: +class Query(object): category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) all_categories = graphene.List(CategoryType) diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py index c415147..338c71a 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py @@ -1,69 +1,36 @@ +# -*- 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')), ], ), ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py index f38bb69..f135392 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -1,30 +1,25 @@ +# -*- 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), ), ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py index dacdb30..7a8df49 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py @@ -4,22 +4,15 @@ 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), ), ] diff --git a/examples/cookbook-plain/cookbook/recipes/schema.py b/examples/cookbook-plain/cookbook/recipes/schema.py index 7f40d51..aa7fd2d 100644 --- a/examples/cookbook-plain/cookbook/recipes/schema.py +++ b/examples/cookbook-plain/cookbook/recipes/schema.py @@ -16,7 +16,7 @@ class RecipeIngredientType(DjangoObjectType): fields = "__all__" -class Query: +class Query(object): recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) all_recipes = graphene.List(RecipeType) diff --git a/examples/cookbook/README.md b/examples/cookbook/README.md index 098b119..0ec906b 100644 --- a/examples/cookbook/README.md +++ b/examples/cookbook/README.md @@ -1,4 +1,4 @@ -Cookbook Example (Relay) Django Project +Cookbook Example 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-relay/#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) diff --git a/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json b/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json index 213bd8b..8625d3c 100644 --- a/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json +++ b/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json @@ -1,52 +1 @@ -[ - { - "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 - } -] +[{"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}}] \ No newline at end of file diff --git a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py index 23d71e8..0494923 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py @@ -1,52 +1,33 @@ +# -*- 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')), ], ), ] diff --git a/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py b/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py index 5f9e7a0..359d4fc 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -1,17 +1,20 @@ +# -*- 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), ), ] diff --git a/examples/cookbook/cookbook/ingredients/schema.py b/examples/cookbook/cookbook/ingredients/schema.py index 4ed9eff..43b6118 100644 --- a/examples/cookbook/cookbook/ingredients/schema.py +++ b/examples/cookbook/cookbook/ingredients/schema.py @@ -28,7 +28,7 @@ class IngredientNode(DjangoObjectType): } -class Query: +class Query(object): category = Node.Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) diff --git a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py index c415147..338c71a 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py @@ -1,69 +1,36 @@ +# -*- 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')), ], ), ] diff --git a/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py b/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py index f38bb69..f135392 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py +++ b/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -1,30 +1,25 @@ +# -*- 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), ), ] diff --git a/examples/cookbook/cookbook/recipes/schema.py b/examples/cookbook/cookbook/recipes/schema.py index ea5ed38..c7298aa 100644 --- a/examples/cookbook/cookbook/recipes/schema.py +++ b/examples/cookbook/cookbook/recipes/schema.py @@ -25,7 +25,7 @@ class RecipeIngredientNode(DjangoObjectType): } -class Query: +class Query(object): recipe = Node.Field(RecipeNode) all_recipes = DjangoFilterConnectionField(RecipeNode) diff --git a/examples/cookbook/dummy_data.json b/examples/cookbook/dummy_data.json index c585bfc..f541da5 100644 --- a/examples/cookbook/dummy_data.json +++ b/examples/cookbook/dummy_data.json @@ -1,302 +1 @@ -[ - { - "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 - } -] +[{"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}}] \ No newline at end of file diff --git a/examples/starwars/models.py b/examples/starwars/models.py index fb76b03..03e06a2 100644 --- a/examples/starwars/models.py +++ b/examples/starwars/models.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from django.db import models diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index 12408a4..999f3de 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,7 +1,7 @@ from .fields import DjangoConnectionField, DjangoListField from .types import DjangoObjectType -__version__ = "3.0.2" +__version__ = "3.0.0b7" __all__ = [ "__version__", diff --git a/graphene_django/compat.py b/graphene_django/compat.py index b0e4753..1956786 100644 --- a/graphene_django/compat.py +++ b/graphene_django/compat.py @@ -1,4 +1,4 @@ -class MissingType: +class MissingType(object): def __init__(self, *args, **kwargs): pass diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 375d683..c243e82 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -24,15 +24,8 @@ 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 - -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 import GraphQLError, assert_valid_name from graphql.pyutils import register_description from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField @@ -62,7 +55,7 @@ class BlankValueField(Field): def convert_choice_name(name): name = to_const(force_str(name)) try: - assert_name(name) + assert_valid_name(name) except GraphQLError: name = "A_%s" % name return name @@ -74,7 +67,8 @@ def get_choices(choices): choices = choices.items() for value, help_text in choices: if isinstance(help_text, (tuple, list)): - yield from get_choices(help_text) + for choice in get_choices(help_text): + yield choice else: name = convert_choice_name(value) while name in converted_names: @@ -91,17 +85,12 @@ 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: + class EnumWithDescriptionsType(object): @property def description(self): return str(named_choices_descriptions[self.name]) - 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_type = Enum(name, list(named_choices), type=EnumWithDescriptionsType) return return_type @@ -113,7 +102,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(f"{django_model_meta.object_name}_{field.name}") + name = to_camel_case("{}_{}".format(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()), @@ -159,9 +148,7 @@ 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 {} ({})".format( - field, field.__class__ - ) + "Don't know how to convert the Django field %s (%s)" % (field, field.__class__) ) @@ -199,14 +186,10 @@ 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) @@ -222,9 +205,7 @@ 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=get_django_field_description(field), required=not field.null - ) + return Decimal(description=field.help_text, required=not field.null) @convert_django_field.register(models.FloatField) diff --git a/graphene_django/debug/exception/formating.py b/graphene_django/debug/exception/formating.py index 0d477b3..ed7ebab 100644 --- a/graphene_django/debug/exception/formating.py +++ b/graphene_django/debug/exception/formating.py @@ -11,7 +11,7 @@ def wrap_exception(exception): exc_type=force_str(type(exception)), stack="".join( traceback.format_exception( - exception, value=exception, tb=exception.__traceback__ + etype=type(exception), value=exception, tb=exception.__traceback__ ) ), ) diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index d3052a1..804e7c8 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -7,34 +7,34 @@ from .exception.formating import wrap_exception from .types import DjangoDebug -class DjangoDebugContext: +class DjangoDebugContext(object): def __init__(self): - self.debug_result = None - self.results = [] + self.debug_promise = None + self.promises = [] self.object = DjangoDebug(sql=[], exceptions=[]) self.enable_instrumentation() - def get_debug_result(self): - if not self.debug_result: - self.debug_result = self.results - self.results = [] - return self.on_resolve_all_results() + 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 on_resolve_error(self, value): if hasattr(self, "object"): self.object.exceptions.append(wrap_exception(value)) - return value + return Promise.reject(value) - def on_resolve_all_results(self): - if self.results: - self.debug_result = None - return self.get_debug_result() + def on_resolve_all_promises(self, values): + if self.promises: + self.debug_promise = None + return self.get_debug_promise() self.disable_instrumentation() return self.object - def add_result(self, result): - if self.debug_result: - self.results.append(result) + def add_promise(self, promise): + if self.debug_promise: + self.promises.append(promise) def enable_instrumentation(self): # This is thread-safe because database connections are thread-local. @@ -46,7 +46,7 @@ class DjangoDebugContext: unwrap_cursor(connection) -class DjangoDebugMiddleware: +class DjangoDebugMiddleware(object): def resolve(self, next, root, info, **args): context = info.context django_debug = getattr(context, "django_debug", None) @@ -62,10 +62,10 @@ class DjangoDebugMiddleware: ) ) if info.schema.get_type("DjangoDebug") == info.return_type: - return context.django_debug.get_debug_result() + return context.django_debug.get_debug_promise() try: - result = next(root, info, **args) + promise = next(root, info, **args) except Exception as e: return context.django_debug.on_resolve_error(e) - context.django_debug.add_result(result) - return result + context.django_debug.add_promise(promise) + return promise diff --git a/graphene_django/debug/sql/tracking.py b/graphene_django/debug/sql/tracking.py index bf0ea36..f7346e6 100644 --- a/graphene_django/debug/sql/tracking.py +++ b/graphene_django/debug/sql/tracking.py @@ -1,4 +1,5 @@ # Code obtained from django-debug-toolbar sql panel tracking +from __future__ import absolute_import, unicode_literals import json from threading import local @@ -49,7 +50,7 @@ def unwrap_cursor(connection): del connection._graphene_cursor -class ExceptionCursorWrapper: +class ExceptionCursorWrapper(object): """ Wraps a cursor and raises an exception on any operation. Used in Templates panel. @@ -62,7 +63,7 @@ class ExceptionCursorWrapper: raise SQLQueryTriggered() -class NormalCursorWrapper: +class NormalCursorWrapper(object): """ Wraps a cursor and logs queries. """ @@ -84,7 +85,7 @@ class NormalCursorWrapper: if not params: return params if isinstance(params, dict): - return {key: self._quote_expr(value) for key, value in params.items()} + return dict((key, self._quote_expr(value)) for key, value in params.items()) return list(map(self._quote_expr, params)) def _decode(self, param): diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index 1ea86b1..eae94dc 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -8,7 +8,7 @@ from ..middleware import DjangoDebugMiddleware from ..types import DjangoDebug -class context: +class context(object): pass diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 0fe123d..e1972c7 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,14 +1,12 @@ from functools import partial from django.db.models.query import QuerySet - -from graphql_relay import ( +from graphql_relay.connection.arrayconnection import ( connection_from_array_slice, cursor_to_offset, get_offset_with_default, offset_to_cursor, ) - from promise import Promise from graphene import Int, NonNull @@ -28,7 +26,7 @@ class DjangoListField(Field): _type = _type.of_type # Django would never return a Set of None vvvvvvv - super().__init__(List(NonNull(_type)), *args, **kwargs) + super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs) assert issubclass( self._underlying_type, DjangoObjectType @@ -63,16 +61,13 @@ class DjangoListField(Field): return queryset def wrap_resolve(self, parent_resolver): - resolver = super().wrap_resolve(parent_resolver) + resolver = super(DjangoListField, self).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(), ) @@ -87,7 +82,7 @@ class DjangoConnectionField(ConnectionField): graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST, ) kwargs.setdefault("offset", Int()) - super().__init__(*args, **kwargs) + super(DjangoConnectionField, self).__init__(*args, **kwargs) @property def type(self): @@ -149,40 +144,36 @@ class DjangoConnectionField(ConnectionField): iterable = maybe_queryset(iterable) if isinstance(iterable, QuerySet): - array_length = iterable.count() + list_length = iterable.count() else: - array_length = len(iterable) + list_length = len(iterable) + list_slice_length = ( + min(max_limit, list_length) if max_limit is not None else list_length + ) - # If after is higher than array_length, connection_from_array_slice + # If after is higher than list_length, connection_from_list_slice # would try to do a negative slicing which makes django throw an # AssertionError - slice_start = min( - get_offset_with_default(args.get("after"), -1) + 1, - array_length, - ) - array_slice_length = array_length - slice_start + after = min(get_offset_with_default(args.get("after"), -1) + 1, list_length) - # 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 + 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 connection = connection_from_array_slice( - iterable[slice_start:], + iterable[after:], args, - slice_start=slice_start, - array_length=array_length, - array_slice_length=array_slice_length, + slice_start=after, + array_length=list_length, + array_slice_length=list_slice_length, connection_type=partial(connection_adapter, connection), edge_type=connection.Edge, page_info_type=page_info_adapter, ) connection.iterable = iterable - connection.length = array_length + connection.length = list_length return connection @classmethod diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index cdb8f85..c6dd50e 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -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().__init__(type_, *args, **kwargs) + super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs) @property def args(self): @@ -90,7 +90,9 @@ class DjangoFilterConnectionField(DjangoConnectionField): kwargs[k] = convert_enum(v) return kwargs - qs = super().resolve_queryset(connection, iterable, info, args) + qs = super(DjangoFilterConnectionField, cls).resolve_queryset( + connection, iterable, info, args + ) filterset = filterset_class( data=filter_kwargs(), queryset=qs, request=info.context diff --git a/graphene_django/filter/filters/array_filter.py b/graphene_django/filter/filters/array_filter.py index b6f4808..e886cff 100644 --- a/graphene_django/filter/filters/array_filter.py +++ b/graphene_django/filter/filters/array_filter.py @@ -22,6 +22,6 @@ class ArrayFilter(TypedFilter): return qs if self.distinct: qs = qs.distinct() - lookup = f"{self.field_name}__{self.lookup_expr}" + lookup = "%s__%s" % (self.field_name, self.lookup_expr) qs = self.get_method(qs)(**{lookup: value}) return qs diff --git a/graphene_django/filter/filters/global_id_filter.py b/graphene_django/filter/filters/global_id_filter.py index 37877d5..a612a8a 100644 --- a/graphene_django/filter/filters/global_id_filter.py +++ b/graphene_django/filter/filters/global_id_filter.py @@ -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().filter(qs, _id) + return super(GlobalIDFilter, self).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().filter(qs, gids) + return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids) diff --git a/graphene_django/filter/filters/list_filter.py b/graphene_django/filter/filters/list_filter.py index 6689877..9689be3 100644 --- a/graphene_django/filter/filters/list_filter.py +++ b/graphene_django/filter/filters/list_filter.py @@ -23,4 +23,4 @@ class ListFilter(TypedFilter): else: return qs.none() else: - return super().filter(qs, value) + return super(ListFilter, self).filter(qs, value) diff --git a/graphene_django/filter/filters/typed_filter.py b/graphene_django/filter/filters/typed_filter.py index 76f903a..2c813e4 100644 --- a/graphene_django/filter/filters/typed_filter.py +++ b/graphene_django/filter/filters/typed_filter.py @@ -12,7 +12,7 @@ class TypedFilter(Filter): def __init__(self, input_type=None, *args, **kwargs): self._input_type = input_type - super().__init__(*args, **kwargs) + super(TypedFilter, self).__init__(*args, **kwargs) @property def input_type(self): diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index fa91477..b3333bf 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -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,18 +29,20 @@ 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( - f"Graphene{filterset_class.__name__}", + "Graphene{}".format(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("Meta", (object,), meta) + meta_class = type(str("Meta"), (object,), meta) filterset = type( str("%sFilterSet" % model._meta.object_name), (filterset_base_class, GrapheneFilterSetMixin), diff --git a/graphene_django/filter/tests/conftest.py b/graphene_django/filter/tests/conftest.py index f8a65d7..57924af 100644 --- a/graphene_django/filter/tests/conftest.py +++ b/graphene_django/filter/tests/conftest.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock +from mock import MagicMock import pytest from django.db import models @@ -87,11 +87,12 @@ 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 diff --git a/graphene_django/filter/tests/test_array_field_exact_filter.py b/graphene_django/filter/tests/test_array_field_exact_filter.py index 10e32ef..cd72868 100644 --- a/graphene_django/filter/tests/test_array_field_exact_filter.py +++ b/graphene_django/filter/tests/test_array_field_exact_filter.py @@ -120,7 +120,10 @@ def test_array_field_filter_schema_type(Query): "randomField": "[Boolean!]", } filters_str = ", ".join( - [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()] + [ + f"{filter_field}: {gql_type} = null" + for filter_field, gql_type in filters.items() + ] ) assert ( f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str diff --git a/graphene_django/filter/tests/test_enum_filtering.py b/graphene_django/filter/tests/test_enum_filtering.py index a284d08..09c69b3 100644 --- a/graphene_django/filter/tests/test_enum_filtering.py +++ b/graphene_django/filter/tests/test_enum_filtering.py @@ -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,13 +80,7 @@ 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 @@ -158,6 +152,9 @@ def test_filter_enum_field_schema_type(schema): "reporter_AChoice_In": "[TestsReporterAChoiceChoices]", } filters_str = ", ".join( - [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()] + [ + f"{filter_field}: {gql_type} = null" + for filter_field, gql_type in filters.items() + ] ) assert f" allArticles({filters_str}): ArticleTypeConnection\n" in schema_str diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index bee3c6c..17b4630 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -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, Decimal, Field, ObjectType, Schema, String +from graphene import Argument, Boolean, Field, Float, 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 - ), f"Expected arguments ({arguments}) did not match actual ({actual})" + ), "Expected arguments ({}) did not match actual ({})".format(arguments, actual) def assert_orderable(field): @@ -141,7 +141,7 @@ def test_filter_shortcut_filterset_context(): @property def qs(self): - qs = super().qs + qs = super(ArticleContextFilter, self).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: + class context(object): 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 == Decimal + assert max_time.type == Float 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, before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection + pets(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection } type PetTypeConnection { @@ -1056,7 +1056,8 @@ def test_integer_field_filter_type(): interface Node { \"""The ID of the object\""" id: ID! - }""" + } + """ ) @@ -1076,7 +1077,7 @@ def test_other_filter_types(): assert str(schema) == dedent( """\ type Query { - pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection + 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 } type PetTypeConnection { @@ -1124,7 +1125,8 @@ def test_other_filter_types(): interface Node { \"""The ID of the object\""" id: ID! - }""" + } + """ ) @@ -1224,7 +1226,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 @@ -1265,23 +1267,13 @@ 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"}},]} } diff --git a/graphene_django/filter/tests/test_in_filter.py b/graphene_django/filter/tests/test_in_filter.py index a69d6f5..7ad0286 100644 --- a/graphene_django/filter/tests/test_in_filter.py +++ b/graphene_django/filter/tests/test_in_filter.py @@ -349,19 +349,19 @@ def test_fk_id_in_filter(query): schema = Schema(query=query) query = """ - query {{ - articles (reporter_In: [{}, {}]) {{ - edges {{ - node {{ + query { + articles (reporter_In: [%s, %s]) { + edges { + node { headline - reporter {{ + reporter { lastName - }} - }} - }} - }} - }} - """.format( + } + } + } + } + } + """ % ( john_doe.id, jean_bon.id, ) diff --git a/graphene_django/filter/tests/test_typed_filter.py b/graphene_django/filter/tests/test_typed_filter.py index f22138f..b903b59 100644 --- a/graphene_django/filter/tests/test_typed_filter.py +++ b/graphene_django/filter/tests/test_typed_filter.py @@ -98,14 +98,20 @@ def test_typed_filter_schema(schema): ) for filter_field, gql_type in filters.items(): - assert f"{filter_field}: {gql_type}" in all_articles_filters + assert "{}: {} = null".format(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 } } } }" diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py index 3055ec7..05256f9 100644 --- a/graphene_django/filter/utils.py +++ b/graphene_django/filter/utils.py @@ -97,9 +97,7 @@ 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 diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 47eb51d..b64e478 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -3,19 +3,7 @@ from functools import singledispatch from django import forms from django.core.exceptions import ImproperlyConfigured -from graphene import ( - ID, - Boolean, - Decimal, - Float, - Int, - List, - String, - UUID, - Date, - DateTime, - Time, -) +from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField @@ -69,18 +57,12 @@ 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( diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 40d1d3c..5a3d8e7 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -82,6 +82,7 @@ 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") @@ -94,7 +95,7 @@ class DjangoFormMutation(BaseDjangoFormMutation): _meta.fields = yank_fields_from_attrs(output_fields, _as=Field) input_fields = yank_fields_from_attrs(input_fields, _as=InputField) - super().__init_subclass_with_meta__( + super(DjangoFormMutation, cls).__init_subclass_with_meta__( _meta=_meta, input_fields=input_fields, **options ) @@ -116,7 +117,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): class Meta: abstract = True - errors = graphene.List(graphene.NonNull(ErrorType), required=True) + errors = graphene.List(ErrorType) @classmethod def __init_subclass_with_meta__( @@ -126,8 +127,9 @@ 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") @@ -145,7 +147,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): registry = get_global_registry() model_type = registry.get_type_for_model(model) if not model_type: - raise Exception(f"No type registered for model: {model.__name__}") + raise Exception("No type registered for model: {}".format(model.__name__)) if not return_field_name: model_name = model.__name__ @@ -161,7 +163,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): _meta.fields = yank_fields_from_attrs(output_fields, _as=Field) input_fields = yank_fields_from_attrs(input_fields, _as=InputField) - super().__init_subclass_with_meta__( + super(DjangoModelFormMutation, cls).__init_subclass_with_meta__( _meta=_meta, input_fields=input_fields, **options ) diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py index b61227b..ccf630f 100644 --- a/graphene_django/forms/tests/test_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -1,12 +1,11 @@ from django import forms -from pytest import raises +from py.test import raises import graphene from graphene import ( String, Int, Boolean, - Decimal, Float, ID, UUID, @@ -98,8 +97,8 @@ def test_should_float_convert_float(): assert_conversion(forms.FloatField, Float) -def test_should_decimal_convert_decimal(): - assert_conversion(forms.DecimalField, Decimal) +def test_should_decimal_convert_float(): + assert_conversion(forms.DecimalField, Float) def test_should_multiple_choice_convert_list(): diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 14c407c..0770acb 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -1,7 +1,7 @@ import pytest from django import forms from django.core.exceptions import ValidationError -from pytest import raises +from py.test import raises from graphene import Field, ObjectType, Schema, String from graphene_django import DjangoObjectType diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index 42c41c1..565f5d8 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -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 = [] + requires_system_checks = False 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.graphql_schema)) + self.stdout.write(print_schema(schema)) else: # Determine format _, file_extension = os.path.splitext(out) @@ -73,12 +73,16 @@ class Command(CommandArguments): elif file_extension == ".json": self.save_json_file(out, schema_dict, indent) else: - raise CommandError(f'Unrecognised file format "{file_extension}"') + raise CommandError( + 'Unrecognised file format "{}"'.format(file_extension) + ) style = getattr(self, "style", None) success = getattr(style, "SUCCESS", lambda x: x) - self.stdout.write(success(f"Successfully dumped GraphQL schema to {out}")) + self.stdout.write( + success("Successfully dumped GraphQL schema to {}".format(out)) + ) def handle(self, *args, **options): options_schema = options.get("schema") diff --git a/graphene_django/registry.py b/graphene_django/registry.py index 4708637..50a8ae5 100644 --- a/graphene_django/registry.py +++ b/graphene_django/registry.py @@ -1,4 +1,4 @@ -class Registry: +class Registry(object): def __init__(self): self._registry = {} self._field_registry = {} diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 4062a44..000b21e 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -72,6 +72,7 @@ class SerializerMutation(ClientIDMutation): _meta=None, **options ): + if not serializer_class: raise Exception("serializer_class is required for the SerializerMutation") @@ -113,7 +114,7 @@ class SerializerMutation(ClientIDMutation): _meta.fields = yank_fields_from_attrs(output_fields, _as=Field) input_fields = yank_fields_from_attrs(input_fields, _as=InputField) - super().__init_subclass_with_meta__( + super(SerializerMutation, cls).__init_subclass_with_meta__( _meta=_meta, input_fields=input_fields, **options ) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 1d850f0..b26e5e6 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -72,7 +72,7 @@ def convert_serializer_to_input_type(serializer_class): for name, field in serializer.fields.items() } ret_type = type( - f"{serializer.__class__.__name__}Input", + "{}Input".format(serializer.__class__.__name__), (graphene.InputObjectType,), items, ) @@ -110,15 +110,11 @@ 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 diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 8da8377..daa8349 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -3,7 +3,7 @@ import copy import graphene from django.db import models from graphene import InputObjectType -from pytest import raises +from py.test 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_decimal(): +def test_should_decimal_convert_float(): assert_conversion( - serializers.DecimalField, graphene.Decimal, max_digits=4, decimal_places=2 + serializers.DecimalField, graphene.Float, max_digits=4, decimal_places=2 ) diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index 5de8237..e0e5602 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -1,6 +1,6 @@ import datetime -from pytest import raises +from py.test import raises from rest_framework import serializers from graphene import Field, ResolveInfo diff --git a/graphene_django/settings.py b/graphene_django/settings.py index 9c7dc38..467c6a3 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -11,6 +11,7 @@ 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 @@ -40,9 +41,7 @@ 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: @@ -77,7 +76,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 '{}' for Graphene setting '{}'. {}: {}.".format( + msg = "Could not import '%s' for Graphene setting '%s'. %s: %s." % ( val, setting_name, e.__class__.__name__, @@ -86,7 +85,7 @@ def import_from_string(val, setting_name): raise ImportError(msg) -class GrapheneSettings: +class GrapheneSettings(object): """ A settings object, that allows API settings to be accessed as properties. For example: diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js index 901c991..ac010e8 100644 --- a/graphene_django/static/graphene_django/graphiql.js +++ b/graphene_django/static/graphene_django/graphiql.js @@ -5,12 +5,19 @@ GraphiQL, React, ReactDOM, - graphqlWs, - GraphiQLPluginExplorer, + SubscriptionsTransportWs, 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 = {}; @@ -53,34 +60,98 @@ 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); - 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, + // 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. lazy: true, - }), - headers: headers - }) + }, + ); + + // 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]; + } // When the query and variables string is edited, update the URL bar so // that it can be easily shared. @@ -99,44 +170,23 @@ function updateURL() { history.replaceState(null, null, locationQuery(parameters)); } - - function GraphiQLWithExplorer() { - var [query, setQuery] = React.useState(parameters.query); - - function handleQuery(query) { - setQuery(query); - onEditQuery(query); - } - - var explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({ - query: query, - onEdit: handleQuery, - }); - - var options = { - 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); + 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; } - // Render into the body. ReactDOM.render( - React.createElement(GraphiQLWithExplorer), + React.createElement(GraphiQL, options), document.getElementById("editor"), ); })( @@ -146,8 +196,7 @@ window.GraphiQL, window.React, window.ReactDOM, - window.graphqlWs, - window.GraphiQLPluginExplorer, + window.SubscriptionsTransportWs, window.fetch, window.history, window.location, diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index ddff8fc..cec4893 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -33,12 +33,9 @@ add "&raw" to the end of the URL within a browser. - -
@@ -49,7 +46,6 @@ 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" }}, }; diff --git a/graphene_django/tests/issues/test_520.py b/graphene_django/tests/issues/test_520.py index 4e55f96..60c5b54 100644 --- a/graphene_django/tests/issues/test_520.py +++ b/graphene_django/tests/issues/test_520.py @@ -8,8 +8,8 @@ import graphene from graphene import Field, ResolveInfo from graphene.types.inputobjecttype import InputObjectType -from pytest import raises -from pytest import mark +from py.test import raises +from py.test import mark from rest_framework import serializers from ...types import DjangoObjectType diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 636f74c..7b76cd3 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from django.db import models from django.utils.translation import gettext_lazy as _ @@ -11,9 +13,6 @@ 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): @@ -35,7 +34,7 @@ class Film(models.Model): class DoeReporterManager(models.Manager): def get_queryset(self): - return super().get_queryset().filter(last_name="Doe") + return super(DoeReporterManager, self).get_queryset().filter(last_name="Doe") class Reporter(models.Model): @@ -55,7 +54,7 @@ class Reporter(models.Model): ) def __str__(self): # __unicode__ on Python 2 - return f"{self.first_name} {self.last_name}" + return "%s %s" % (self.first_name, self.last_name) def __init__(self, *args, **kwargs): """ @@ -65,7 +64,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().__init__(*args, **kwargs) + super(Reporter, self).__init__(*args, **kwargs) if self.reporter_type == 2: # quick and dirty way without enums self.__class__ = CNNReporter @@ -75,7 +74,7 @@ class Reporter(models.Model): class CNNReporterManager(models.Manager): def get_queryset(self): - return super().get_queryset().filter(reporter_type=2) + return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2) class CNNReporter(Reporter): diff --git a/graphene_django/tests/schema_view.py b/graphene_django/tests/schema_view.py index 4d538ba..8ed2ecf 100644 --- a/graphene_django/tests/schema_view.py +++ b/graphene_django/tests/schema_view.py @@ -5,6 +5,7 @@ from .mutations import PetFormMutation, PetMutation class QueryRoot(ObjectType): + thrower = graphene.String(required=True) request = graphene.String(required=True) test = graphene.String(who=graphene.String()) diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py index a281abb..70116b8 100644 --- a/graphene_django/tests/test_command.py +++ b/graphene_django/tests/test_command.py @@ -2,7 +2,7 @@ from textwrap import dedent from django.core import management from io import StringIO -from unittest.mock import mock_open, patch +from mock import mock_open, patch from graphene import ObjectType, Schema, String @@ -53,5 +53,6 @@ def test_generate_graphql_file_on_call_graphql_schema(): """\ type Query { hi: String - }""" + } + """ ) diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 4996505..afd744f 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -3,14 +3,13 @@ from collections import namedtuple import pytest from django.db import models from django.utils.translation import gettext_lazy as _ -from pytest import raises +from py.test 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, @@ -141,8 +140,8 @@ def test_should_small_integer_convert_int(): assert_conversion(models.SmallIntegerField, graphene.Int) -def test_should_big_integer_convert_big_int(): - assert_conversion(models.BigIntegerField, BigInt) +def test_should_big_integer_convert_int(): + assert_conversion(models.BigIntegerField, graphene.Int) def test_should_integer_convert_int(): diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 8c7b78d..835de78 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,6 +1,5 @@ import datetime -import re -from django.db.models import Count, Prefetch +from django.db.models import Count import pytest @@ -8,12 +7,8 @@ from graphene import List, NonNull, ObjectType, Schema, String from ..fields import DjangoListField from ..types import DjangoObjectType -from .models import ( - Article as ArticleModel, - Film as FilmModel, - FilmDetails as FilmDetailsModel, - Reporter as ReporterModel, -) +from .models import Article as ArticleModel +from .models import Reporter as ReporterModel class TestDjangoListField: @@ -505,145 +500,3 @@ 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"], - ) diff --git a/graphene_django/tests/test_forms.py b/graphene_django/tests/test_forms.py index a42fcee..fa6628d 100644 --- a/graphene_django/tests/test_forms.py +++ b/graphene_django/tests/test_forms.py @@ -1,5 +1,5 @@ from django.core.exceptions import ValidationError -from pytest import raises +from py.test import raises from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField diff --git a/graphene_django/tests/test_get_queryset.py b/graphene_django/tests/test_get_queryset.py deleted file mode 100644 index 7cbaa54..0000000 --- a/graphene_django/tests/test_get_queryset.py +++ /dev/null @@ -1,235 +0,0 @@ -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"}} diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 383ff2e..aabe19c 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -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 pytest import raises +from py.test 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, Person, Pet, Reporter +from .models import Article, CNNReporter, Film, FilmDetails, Reporter def test_should_query_only_fields(): @@ -251,8 +251,8 @@ def test_should_node(): def test_should_query_onetoone_fields(): - film = Film.objects.create(id=1) - film_details = FilmDetails.objects.create(id=1, film=film) + film = Film(id=1) + film_details = FilmDetails(id=1, film=film) class FilmNode(DjangoObjectType): class Meta: @@ -780,6 +780,7 @@ 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 ) @@ -817,6 +818,7 @@ 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 ) @@ -1149,9 +1151,9 @@ def test_connection_should_limit_after_to_list_length(): REPORTERS = [ dict( - first_name=f"First {i}", - last_name=f"Last {i}", - email=f"johndoe+{i}@example.com", + first_name="First {}".format(i), + last_name="Last {}".format(i), + email="johndoe+{}@example.com".format(i), a_choice=1, ) for i in range(6) @@ -1241,7 +1243,6 @@ 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 @@ -1260,8 +1261,8 @@ class TestBackwardPagination: schema = graphene.Schema(query=Query) return schema - def test_query_last(self, graphene_settings, max_limit): - schema = self.setup_schema(graphene_settings, max_limit=max_limit) + def do_queries(self, schema): + # Simply last 3 query_last = """ query { allReporters(last: 3) { @@ -1281,8 +1282,7 @@ class TestBackwardPagination: e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 3", "First 4", "First 5"] - def test_query_first_and_last(self, graphene_settings, max_limit): - schema = self.setup_schema(graphene_settings, max_limit=max_limit) + # Use a combination of first and last query_first_and_last = """ query { allReporters(first: 4, last: 3) { @@ -1302,8 +1302,7 @@ class TestBackwardPagination: e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 1", "First 2", "First 3"] - def test_query_first_last_and_after(self, graphene_settings, max_limit): - schema = self.setup_schema(graphene_settings, max_limit=max_limit) + # Use a combination of first and last and after query_first_last_and_after = """ query queryAfter($after: String) { allReporters(first: 4, last: 3, after: $after) { @@ -1318,8 +1317,7 @@ 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 @@ -1327,35 +1325,20 @@ class TestBackwardPagination: e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 2", "First 3", "First 4"] - 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 - } - } - } - } + def test_should_query(self, graphene_settings): """ + Backward pagination should work as expected + """ + schema = self.setup_schema(graphene_settings, max_limit=100) + 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_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) def test_should_preserve_prefetch_related(django_assert_num_queries): @@ -1497,11 +1480,7 @@ 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 @@ -1542,9 +1521,7 @@ 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 @@ -1613,149 +1590,6 @@ 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"}], - } diff --git a/graphene_django/tests/test_schema.py b/graphene_django/tests/test_schema.py index ff2d8a6..1c889f1 100644 --- a/graphene_django/tests/test_schema.py +++ b/graphene_django/tests/test_schema.py @@ -1,4 +1,4 @@ -from pytest import raises +from py.test import raises from ..registry import Registry from ..types import DjangoObjectType diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index fad26e2..bde72c7 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -3,7 +3,7 @@ from textwrap import dedent import pytest from django.db import models -from unittest.mock import patch +from 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().__init_subclass_with_meta__(**options) + super(ArticleType, cls).__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, before: String, after: String, first: Int, last: Int): ArticleConnection! + articles(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null): ArticleConnection! } \"""An enumeration.\""" @@ -244,7 +244,8 @@ def test_schema_representation(): \"""The ID of the object\""" id: ID! ): Node - }""" + } + """ ) assert str(schema) == expected @@ -484,7 +485,7 @@ def test_django_objecttype_neither_fields_nor_exclude(): def custom_enum_name(field): - return f"CustomEnum{field.name.title()}" + return "CustomEnum{}".format(field.name.title()) class TestDjangoObjectType: @@ -524,7 +525,8 @@ class TestDjangoObjectType: id: ID! kind: String! cuteness: Int! - }""" + } + """ ) def test_django_objecttype_convert_choices_enum_list(self, PetModel): @@ -558,7 +560,8 @@ class TestDjangoObjectType: \"""Dog\""" DOG - }""" + } + """ ) def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel): @@ -583,7 +586,8 @@ class TestDjangoObjectType: id: ID! kind: String! cuteness: Int! - }""" + } + """ ) def test_django_objecttype_convert_choices_enum_naming_collisions( @@ -617,7 +621,8 @@ class TestDjangoObjectType: \"""Dog\""" DOG - }""" + } + """ ) def test_django_objecttype_choices_custom_enum_name( @@ -655,7 +660,8 @@ class TestDjangoObjectType: \"""Dog\""" DOG - }""" + } + """ ) diff --git a/graphene_django/tests/test_utils.py b/graphene_django/tests/test_utils.py index fa269b4..adad00e 100644 --- a/graphene_django/tests/test_utils.py +++ b/graphene_django/tests/test_utils.py @@ -2,7 +2,7 @@ import json import pytest from django.utils.translation import gettext_lazy -from unittest.mock import patch +from 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 = {field[0] for field in reporter_fields} + reporter_name_set = 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 = {field[0] for field in film_fields} + film_name_set = 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[0][1]) + body = json.loads(post_mock.call_args.args[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[0][1]) + body = json.loads(post_mock.call_args.args[1]) # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request assert ( "operationName", diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index 5cadefe..945fa87 100644 --- a/graphene_django/tests/test_views.py +++ b/graphene_django/tests/test_views.py @@ -2,7 +2,7 @@ import json import pytest -from unittest.mock import patch +from mock import patch from django.db import connection @@ -109,10 +109,12 @@ 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, }, ] } @@ -133,6 +135,8 @@ def test_errors_when_missing_operation_name(client): "errors": [ { "message": "Must provide operation name if query contains multiple operations.", + "locations": None, + "path": None, } ] } @@ -473,6 +477,7 @@ def test_handles_syntax_errors_caught_by_graphql(client): { "locations": [{"column": 1, "line": 1}], "message": "Syntax Error: Unexpected Name 'syntaxerror'.", + "path": None, } ] } @@ -507,7 +512,7 @@ def test_handles_invalid_json_bodies(client): def test_handles_django_request_error(client, monkeypatch): def mocked_read(*args): - raise OSError("foo-bar") + raise IOError("foo-bar") monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read) diff --git a/graphene_django/tests/urls.py b/graphene_django/tests/urls.py index 3702ce5..66b3fc4 100644 --- a/graphene_django/tests/urls.py +++ b/graphene_django/tests/urls.py @@ -1,8 +1,8 @@ -from django.urls import path +from django.conf.urls import url from ..views import GraphQLView urlpatterns = [ - path("graphql/batch", GraphQLView.as_view(batch=True)), - path("graphql", GraphQLView.as_view(graphiql=True)), + url(r"^graphql/batch", GraphQLView.as_view(batch=True)), + url(r"^graphql", GraphQLView.as_view(graphiql=True)), ] diff --git a/graphene_django/tests/urls_inherited.py b/graphene_django/tests/urls_inherited.py index 1e65da0..6fa8019 100644 --- a/graphene_django/tests/urls_inherited.py +++ b/graphene_django/tests/urls_inherited.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.conf.urls import url from ..views import GraphQLView from .schema_view import schema @@ -10,4 +10,4 @@ class CustomGraphQLView(GraphQLView): pretty = True -urlpatterns = [path("graphql/inherited/", CustomGraphQLView.as_view())] +urlpatterns = [url(r"^graphql/inherited/$", CustomGraphQLView.as_view())] diff --git a/graphene_django/tests/urls_pretty.py b/graphene_django/tests/urls_pretty.py index 6275934..1133c87 100644 --- a/graphene_django/tests/urls_pretty.py +++ b/graphene_django/tests/urls_pretty.py @@ -1,6 +1,6 @@ -from django.urls import path +from django.conf.urls import url from ..views import GraphQLView from .schema_view import schema -urlpatterns = [path("graphql", GraphQLView.as_view(schema=schema, pretty=True))] +urlpatterns = [url(r"^graphql", GraphQLView.as_view(schema=schema, pretty=True))] diff --git a/graphene_django/types.py b/graphene_django/types.py index a6e54af..d272412 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -122,7 +122,7 @@ def validate_fields(type_, model, fields, only_fields, exclude_fields): class DjangoObjectTypeOptions(ObjectTypeOptions): - model = None # type: Type[Model] + model = None # type: Model registry = None # type: Registry connection = None # type: Type[Connection] @@ -168,8 +168,10 @@ 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), ( @@ -214,7 +216,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, ) @@ -226,7 +228,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: @@ -253,7 +255,7 @@ class DjangoObjectType(ObjectType): _meta.fields = django_fields _meta.connection = connection - super().__init_subclass_with_meta__( + super(DjangoObjectType, cls).__init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) diff --git a/graphene_django/utils/testing.py b/graphene_django/utils/testing.py index ad9ff35..763196d 100644 --- a/graphene_django/utils/testing.py +++ b/graphene_django/utils/testing.py @@ -3,9 +3,7 @@ import warnings from django.test import Client, TestCase, TransactionTestCase -from graphene_django.settings import graphene_settings - -DEFAULT_GRAPHQL_URL = "/graphql" +DEFAULT_GRAPHQL_URL = "/graphql/" def graphql_query( @@ -21,7 +19,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 operation_name. For annon queries ("{ ... }"), + supply the op_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``, @@ -42,7 +40,7 @@ def graphql_query( if client is None: client = Client() if not graphql_url: - graphql_url = graphene_settings.TESTING_ENDPOINT + graphql_url = DEFAULT_GRAPHQL_URL body = {"query": query} if operation_name: @@ -65,13 +63,13 @@ def graphql_query( return resp -class GraphQLTestMixin: +class GraphQLTestMixin(object): """ Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/ """ # URL to graphql endpoint - GRAPHQL_URL = graphene_settings.TESTING_ENDPOINT + GRAPHQL_URL = DEFAULT_GRAPHQL_URL def query( self, query, operation_name=None, input_data=None, variables=None, headers=None @@ -80,7 +78,7 @@ class GraphQLTestMixin: Args: query (string) - GraphQL query to run operation_name (string) - If the query is a mutation or named query, you must - supply the operation_name. For annon queries ("{ ... }"), + supply the op_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``, @@ -91,7 +89,7 @@ class GraphQLTestMixin: 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 diff --git a/graphene_django/utils/tests/test_str_converters.py b/graphene_django/utils/tests/test_str_converters.py index d3d33c2..6460c4e 100644 --- a/graphene_django/utils/tests/test_str_converters.py +++ b/graphene_django/utils/tests/test_str_converters.py @@ -6,4 +6,4 @@ def test_to_const(): def test_to_const_unicode(): - assert to_const("Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF" + assert to_const(u"Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF" diff --git a/graphene_django/utils/tests/test_testing.py b/graphene_django/utils/tests/test_testing.py index de56158..2ef78f9 100644 --- a/graphene_django/utils/tests/test_testing.py +++ b/graphene_django/utils/tests/test_testing.py @@ -2,7 +2,6 @@ import pytest from .. import GraphQLTestCase from ...tests.test_types import with_local_registry -from ...settings import graphene_settings from django.test import Client @@ -44,11 +43,3 @@ 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 diff --git a/graphene_django/views.py b/graphene_django/views.py index 5733660..e90d94f 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -11,6 +11,7 @@ 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 @@ -26,7 +27,7 @@ class HttpError(Exception): def __init__(self, response, message=None, *args, **kwargs): self.response = response self.message = message = message or response.content.decode() - super().__init__(message, *args, **kwargs) + super(HttpError, self).__init__(message, *args, **kwargs) def get_accepted_content_types(request): @@ -66,19 +67,16 @@ class GraphQLView(View): react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=" # The GraphiQL React app. - 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=" + 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=" # The websocket transport library for subscriptions. - subscriptions_transport_ws_version = "5.12.1" + subscriptions_transport_ws_version = "0.9.18" subscriptions_transport_ws_sri = ( - "sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw=" + "sha256-i0hAXd4PdJ/cHX3/8tIy/Q/qKiWr5WSTxMFuL9tACkw=" ) - graphiql_plugin_explorer_version = "0.1.15" - graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE=" - schema = None graphiql = False middleware = None @@ -161,13 +159,10 @@ 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: @@ -392,7 +387,7 @@ class GraphQLView(View): @staticmethod def format_error(error): if isinstance(error, GraphQLError): - return error.formatted + return format_graphql_error(error) return {"message": str(error)} diff --git a/setup.cfg b/setup.cfg index c725df1..52f6bf6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ test=pytest universal=1 [flake8] -exclude = docs,graphene_django/debug/sql/* +exclude = docs,graphene_django/debug/sql/*,migrations max-line-length = 120 select = # Dictionary key repeated diff --git a/setup.py b/setup.py index 37b57a8..fd403c0 100644 --- a/setup.py +++ b/setup.py @@ -14,23 +14,22 @@ rest_framework_require = ["djangorestframework>=3.6.3"] tests_require = [ - "pytest>=7.3.1", + "pytest>=3.6.3", "pytest-cov", "pytest-random-order", "coveralls", "mock", "pytz", - "django-filter>=22.1", - "pytest-django>=4.5.2", + "django-filter>=2", + "pytest-django>=3.3.2", ] + rest_framework_require dev_requires = [ - "black==23.3.0", - "flake8==6.0.0", - "flake8-black==0.3.6", - "flake8-bugbear==23.3.23", - "pre-commit", + "black==19.10b0", + "flake8==3.7.9", + "flake8-black==0.1.1", + "flake8-bugbear==20.1.4", ] + tests_require setup( @@ -47,24 +46,23 @@ setup( "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 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,<4", + "graphene>=3.0.0b5,<4", "graphql-core>=3.1.0,<4", - "graphql-relay>=3.1.1,<4", - "Django>=3.2", + "Django>=2.2", "promise>=2.1", "text-unidecode", ], diff --git a/tox.ini b/tox.ini index e186f30..7128afe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,23 +1,21 @@ [tox] envlist = - py{37,38,39,310}-django32, - py{38,39,310}-django{40,41,main}, - py311-django{41,main} - pre-commit + py{36,37,38,39}-django{22,30,31,32,main}, + black,flake8 [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] @@ -25,18 +23,26 @@ passenv = * usedevelop = True setenv = DJANGO_SETTINGS_MODULE=examples.django_test_settings - PYTHONPATH=. deps = -e.[test] psycopg2-binary - django32: Django>=3.2,<4.0 - django40: Django>=4.0,<4.1 - django41: Django>=4.1,<4.2 + 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 djangomain: https://github.com/django/django/archive/main.zip commands = {posargs:py.test --cov=graphene_django graphene_django examples} -[testenv:pre-commit] -skip_install = true -deps = pre-commit +[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] commands = - pre-commit run --all-files --show-diff-on-failure + flake8 graphene_django examples setup.py