From dd0d6ef28ffce33d30c4c8908f48a9d44a2b75ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Clgen=20Sar=C4=B1kavak?= Date: Mon, 6 Apr 2020 15:21:07 +0300 Subject: [PATCH] Python 3 (#904) * Remove Python 2 support * Upgrade Python & Django versions * Remove unsupported Django versions * Remove unsupported Python versions * Add Python 3.8 * Drop support for django-filter < 2 * Update LoginRequiredMixin doc link * Remove redundant import * Resolve RemovedInDjango40Warning warnings * gql/graphene-django/graphene_django/tests/test_converter.py:175: RemovedInDjango40Warning: django.utils.translation.ugettext_lazy() is deprecated in favor of django.utils.translation.gettext_lazy(). * graphene-django/graphene_django/utils/utils.py:28: RemovedInDjango40Warning: force_text() is deprecated in favor of force_str(). * No need to use unicode strings with Python3 * Remove singledispatch dependency singledispatch is inluded with Python >= 3.4, no need for external package. --- .travis.yml | 37 +++++++------------ docs/authorization.rst | 2 +- docs/conf.py | 16 ++++---- docs/filtering.rst | 3 +- graphene_django/converter.py | 5 +-- graphene_django/debug/sql/tracking.py | 3 +- graphene_django/fields.py | 1 - graphene_django/filter/filterset.py | 32 +--------------- graphene_django/filter/utils.py | 4 +- graphene_django/forms/converter.py | 6 +-- .../rest_framework/serializer_converter.py | 5 +-- graphene_django/settings.py | 3 +- graphene_django/tests/models.py | 6 +-- graphene_django/tests/test_command.py | 2 +- graphene_django/tests/test_converter.py | 2 +- graphene_django/types.py | 5 +-- graphene_django/utils/__init__.py | 2 - graphene_django/utils/utils.py | 32 ++-------------- graphene_django/views.py | 5 +-- setup.py | 14 ++----- tox.ini | 13 ++----- 21 files changed, 53 insertions(+), 145 deletions(-) diff --git a/.travis.yml b/.travis.yml index bbeeb80..f1ca201 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,10 @@ dist: xenial install: - pip install tox tox-travis -script: +script: - tox -after_success: +after_success: - pip install coveralls - coveralls @@ -24,24 +24,8 @@ jobs: - env: DJANGO=master include: - - python: 2.7 - env: DJANGO=1.11 - - - python: 3.5 - env: DJANGO=1.11 - - python: 3.5 - env: DJANGO=2.0 - - python: 3.5 - env: DJANGO=2.1 - - python: 3.5 - env: DJANGO=2.2 - - python: 3.6 env: DJANGO=1.11 - - python: 3.6 - env: DJANGO=2.0 - - python: 3.6 - env: DJANGO=2.1 - python: 3.6 env: DJANGO=2.2 - python: 3.6 @@ -51,10 +35,6 @@ jobs: - python: 3.7 env: DJANGO=1.11 - - python: 3.7 - env: DJANGO=2.0 - - python: 3.7 - env: DJANGO=2.1 - python: 3.7 env: DJANGO=2.2 - python: 3.7 @@ -62,12 +42,21 @@ jobs: - python: 3.7 env: DJANGO=master - - python: 3.7 + - python: 3.8 + env: DJANGO=1.11 + - python: 3.8 + env: DJANGO=2.2 + - python: 3.8 + env: DJANGO=3.0 + - python: 3.8 + env: DJANGO=master + + - python: 3.8 env: TOXENV=black,flake8 - stage: deploy script: skip - python: 3.7 + python: 3.8 after_success: true deploy: provider: pypi diff --git a/docs/authorization.rst b/docs/authorization.rst index 63123b0..7e09c37 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -184,4 +184,4 @@ For Django 2.0 and above: path('graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), ] -.. _LoginRequiredMixin: https://docs.djangoproject.com/en/1.10/topics/auth/default/#the-loginrequired-mixin +.. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin diff --git a/docs/conf.py b/docs/conf.py index a485d5b..b83e0f0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,18 +60,18 @@ source_suffix = ".rst" master_doc = "index" # General information about the project. -project = u"Graphene Django" -copyright = u"Graphene 2017" -author = u"Syrus Akbary" +project = "Graphene Django" +copyright = "Graphene 2017" +author = "Syrus Akbary" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = u"1.0" +version = "1.0" # The full version, including alpha/beta/rc tags. -release = u"1.0.dev" +release = "1.0.dev" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -276,7 +276,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual") + (master_doc, "Graphene.tex", "Graphene Documentation", "Syrus Akbary", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -317,7 +317,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, "graphene_django", u"Graphene Django Documentation", [author], 1) + (master_doc, "graphene_django", "Graphene Django Documentation", [author], 1) ] # If true, show URL addresses after external links. @@ -334,7 +334,7 @@ texinfo_documents = [ ( master_doc, "Graphene-Django", - u"Graphene Django Documentation", + "Graphene Django Documentation", author, "Graphene Django", "One line description of project.", diff --git a/docs/filtering.rst b/docs/filtering.rst index 6fe7cab..fb73744 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -2,8 +2,7 @@ Filtering ========= Graphene integrates with -`django-filter `__ (2.x for -Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage +`django-filter `__ to provide filtering of results. See the `usage documentation `__ for details on the format for ``filter_fields``. diff --git a/graphene_django/converter.py b/graphene_django/converter.py index bd8f79d..36116ed 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -1,4 +1,6 @@ from collections import OrderedDict +from functools import singledispatch + from django.db import models from django.utils.encoding import force_str from django.utils.module_loading import import_string @@ -26,9 +28,6 @@ from graphql import assert_valid_name from .settings import graphene_settings from .compat import ArrayField, HStoreField, JSONField, RangeField from .fields import DjangoListField, DjangoConnectionField -from .utils import import_single_dispatch - -singledispatch = import_single_dispatch() def convert_choice_name(name): diff --git a/graphene_django/debug/sql/tracking.py b/graphene_django/debug/sql/tracking.py index a7c9d8d..aacd1a0 100644 --- a/graphene_django/debug/sql/tracking.py +++ b/graphene_django/debug/sql/tracking.py @@ -5,7 +5,6 @@ import json from threading import local from time import time -import six from django.utils.encoding import force_str from .types import DjangoDebugSQL @@ -77,7 +76,7 @@ class NormalCursorWrapper(object): self.logger = logger def _quote_expr(self, element): - if isinstance(element, six.string_types): + if isinstance(element, str): return "'%s'" % force_str(element).replace("'", "''") else: return repr(element) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index fb6b98a..f0a3828 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,6 +1,5 @@ from functools import partial -import six from django.db.models.query import QuerySet from graphql_relay.connection.arrayconnection import connection_from_list_slice from promise import Promise diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index 7676ea8..34108ae 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -1,7 +1,7 @@ import itertools from django.db import models -from django_filters import Filter, MultipleChoiceFilter, VERSION +from django_filters import Filter, MultipleChoiceFilter from django_filters.filterset import BaseFilterSet, FilterSet from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS @@ -50,36 +50,6 @@ class GrapheneFilterSetMixin(BaseFilterSet): ) -# To support a Django 1.11 + Python 2.7 combination django-filter must be -# < 2.x.x. To support the earlier version of django-filter, the -# filter_for_reverse_field method must be present on GrapheneFilterSetMixin and -# must not be present for later versions of django-filter. -if VERSION[0] < 2: - from django.utils.text import capfirst - - class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin): - @classmethod - def filter_for_reverse_field(cls, f, name): - """Handles retrieving filters for reverse relationships - We override the default implementation so that we can handle - Global IDs (the default implementation expects database - primary keys) - """ - try: - rel = f.field.remote_field - except AttributeError: - rel = f.field.rel - default = {"name": name, "label": capfirst(rel.related_name)} - if rel.multiple: - # For to-many relationships - return GlobalIDMultipleChoiceFilter(**default) - else: - # For to-one relationships - return GlobalIDFilter(**default) - - GrapheneFilterSetMixin = GrapheneFilterSetMixinPython2 - - def setup_filterset(filterset_class): """ Wrap a provided filterset in Graphene-specific functionality """ diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py index c5f18e2..ea4f8dc 100644 --- a/graphene_django/filter/utils.py +++ b/graphene_django/filter/utils.py @@ -1,5 +1,3 @@ -import six - from django_filters.utils import get_model_field from .filterset import custom_filterset_factory, setup_filterset @@ -13,7 +11,7 @@ def get_filtering_args_from_filterset(filterset_class, type): args = {} model = filterset_class._meta.model - for name, filter_field in six.iteritems(filterset_class.base_filters): + for name, filter_field in filterset_class.base_filters.items(): form_field = None if name in filterset_class.declared_filters: diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 8916456..7b154b4 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -1,13 +1,11 @@ +from functools import singledispatch + from django import forms from django.core.exceptions import ImproperlyConfigured from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField -from ..utils import import_single_dispatch - - -singledispatch = import_single_dispatch() @singledispatch diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 82a113a..b26e5e6 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -1,3 +1,5 @@ +from functools import singledispatch + from django.core.exceptions import ImproperlyConfigured from rest_framework import serializers @@ -5,11 +7,8 @@ import graphene from ..registry import get_global_registry from ..converter import convert_choices_to_named_enum_with_descriptions -from ..utils import import_single_dispatch from .types import DictType -singledispatch = import_single_dispatch() - @singledispatch def get_graphene_type_from_serializer_field(field): diff --git a/graphene_django/settings.py b/graphene_django/settings.py index 666ad8a..07cbba1 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -13,7 +13,6 @@ back to the defaults. """ from __future__ import unicode_literals -import six from django.conf import settings from django.test.signals import setting_changed @@ -55,7 +54,7 @@ def perform_import(val, setting_name): """ if val is None: return None - elif isinstance(val, six.string_types): + elif isinstance(val, str): return import_from_string(val, setting_name) elif isinstance(val, (list, tuple)): return [import_from_string(item, setting_name) for item in val] diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 44a5d8a..708fe57 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -1,7 +1,7 @@ from __future__ import absolute_import from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ CHOICES = ((1, "this"), (2, _("that"))) @@ -46,7 +46,7 @@ class Reporter(models.Model): "Reporter Type", null=True, blank=True, - choices=[(1, u"Regular"), (2, u"CNN Reporter")], + choices=[(1, "Regular"), (2, "CNN Reporter")], ) def __str__(self): # __unicode__ on Python 2 @@ -105,7 +105,7 @@ class Article(models.Model): "Importance", null=True, blank=True, - choices=[(1, u"Very important"), (2, u"Not as important")], + choices=[(1, "Very important"), (2, "Not as important")], ) def __str__(self): # __unicode__ on Python 2 diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py index 8b0a8e6..6dfe330 100644 --- a/graphene_django/tests/test_command.py +++ b/graphene_django/tests/test_command.py @@ -1,8 +1,8 @@ from textwrap import dedent from django.core import management +from io import StringIO from mock import mock_open, patch -from six import StringIO from graphene import ObjectType, Schema, String diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 7f84de3..f1d1b9b 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -1,7 +1,7 @@ import pytest from collections import namedtuple from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from graphene import NonNull from py.test import raises diff --git a/graphene_django/types.py b/graphene_django/types.py index 0c0cb1c..9bb47b9 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -1,7 +1,7 @@ import warnings from collections import OrderedDict +from typing import Type -import six from django.db.models import Model from django.utils.functional import SimpleLazyObject @@ -21,9 +21,6 @@ from .utils import ( is_valid_django_model, ) -if six.PY3: - from typing import Type - ALL_FIELDS = "__all__" diff --git a/graphene_django/utils/__init__.py b/graphene_django/utils/__init__.py index 9d8658b..671b060 100644 --- a/graphene_django/utils/__init__.py +++ b/graphene_django/utils/__init__.py @@ -4,7 +4,6 @@ from .utils import ( camelize, get_model_fields, get_reverse_fields, - import_single_dispatch, is_valid_django_model, maybe_queryset, ) @@ -16,6 +15,5 @@ __all__ = [ "get_model_fields", "camelize", "is_valid_django_model", - "import_single_dispatch", "GraphQLTestCase", ] diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index c1d3572..c5ea85b 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -1,9 +1,8 @@ import inspect -import six from django.db import models from django.db.models.manager import Manager -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.functional import Promise from graphene.utils.str_converters import to_camel_case @@ -26,14 +25,14 @@ def isiterable(value): def _camelize_django_str(s): if isinstance(s, Promise): - s = force_text(s) - return to_camel_case(s) if isinstance(s, six.string_types) else s + s = force_str(s) + return to_camel_case(s) if isinstance(s, str) else s def camelize(data): if isinstance(data, dict): return {_camelize_django_str(k): camelize(v) for k, v in data.items()} - if isiterable(data) and not isinstance(data, (six.string_types, Promise)): + if isiterable(data) and not isinstance(data, (str, Promise)): return [camelize(d) for d in data] return data @@ -77,26 +76,3 @@ def get_model_fields(model): def is_valid_django_model(model): return inspect.isclass(model) and issubclass(model, models.Model) - - -def import_single_dispatch(): - try: - from functools import singledispatch - except ImportError: - singledispatch = None - - if not singledispatch: - try: - from singledispatch import singledispatch - except ImportError: - pass - - if not singledispatch: - raise Exception( - "It seems your python version does not include " - "functools.singledispatch. Please install the 'singledispatch' " - "package. More information here: " - "https://pypi.python.org/pypi/singledispatch" - ) - - return singledispatch diff --git a/graphene_django/views.py b/graphene_django/views.py index 4c58839..8d57d50 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -2,7 +2,6 @@ import inspect import json import re -import six from django.http import HttpResponse, HttpResponseNotAllowed from django.http.response import HttpResponseBadRequest from django.shortcuts import render @@ -314,7 +313,7 @@ class GraphQLView(View): variables = request.GET.get("variables") or data.get("variables") id = request.GET.get("id") or data.get("id") - if variables and isinstance(variables, six.text_type): + if variables and isinstance(variables, str): try: variables = json.loads(variables) except Exception: @@ -331,7 +330,7 @@ class GraphQLView(View): if isinstance(error, GraphQLError): return format_graphql_error(error) - return {"message": six.text_type(error)} + return {"message": str(error)} @staticmethod def get_content_type(request): diff --git a/setup.py b/setup.py index 560549a..3639fb1 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ from setuptools import find_packages, setup -import sys import ast import re @@ -19,8 +18,7 @@ tests_require = [ "coveralls", "mock", "pytz", - "django-filter<2;python_version<'3'", - "django-filter>=2;python_version>='3'", + "django-filter>=2", "pytest-django>=3.3.2", ] + rest_framework_require @@ -45,22 +43,18 @@ setup( "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: PyPy", ], keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests"]), install_requires=[ - "six>=1.10.0", "graphene>=2.1.7,<3", "graphql-core>=2.1.0,<3", - "Django>=1.11", - "singledispatch>=3.4.0.3", + "Django>=1.11,!=2.0.*,!=2.1.*", "promise>=2.1", ], setup_requires=["pytest-runner"], diff --git a/tox.ini b/tox.ini index e7287ff..9354bdb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,11 @@ [tox] envlist = - py{27,35,36,37}-django{111,20,21,22,master}, - py{36,37}-django30, + py{36,37,38}-django{111,django22,django30,master}, black,flake8 [travis:env] DJANGO = 1.11: django111 - 2.0: django20 - 2.1: django21 2.2: django22 3.0: django30 master: djangomaster @@ -20,23 +17,21 @@ setenv = DJANGO_SETTINGS_MODULE=django_test_settings deps = -e.[test] - psycopg2 + psycopg2-binary django111: Django>=1.11,<2.0 - django20: Django>=2.0,<2.1 - django21: Django>=2.1,<2.2 django22: Django>=2.2,<3.0 django30: Django>=3.0a1,<3.1 djangomaster: https://github.com/django/django/archive/master.zip commands = {posargs:py.test --cov=graphene_django graphene_django examples} [testenv:black] -basepython = python3.7 +basepython = python3.8 deps = -e.[dev] commands = black --exclude "/migrations/" graphene_django examples setup.py --check [testenv:flake8] -basepython = python3.7 +basepython = python3.8 deps = -e.[dev] commands = flake8 graphene_django examples