Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dropped Python 2 compatibility. #6615

Merged
merged 1 commit into from
Apr 30, 2019

Conversation

carltongibson
Copy link
Collaborator

No description provided.

This was referenced Apr 29, 2019
@carltongibson
Copy link
Collaborator Author

Hey @jdufresne. If you have bandwidth, could you throw your eye over this one? It's the sort of thing you're hot on. I think we got most of it, but no doubt there's some stuff we missed. Ta. 🙂

@carltongibson
Copy link
Collaborator Author

carltongibson commented Apr 29, 2019

Ah, we are going to have to solve the mkdocs issue... (cc @tomchristie) https://travis-ci.org/encode/django-rest-framework/jobs/526144332

But it's not bad overall. Guardian still blocking Django master compat, but beyond that...

Copy link
Member

@auvipy auvipy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for bringing it all in a single PR. Looks good so far. Will try to have another look tomorrow if possible.

@@ -1,6 +1,6 @@
[tox]
envlist =
{py27,py34,py35,py36}-django111,
{py34,py35,py36}-django111,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

py34 is already EOL :)

@jdufresne
Copy link
Contributor

Running pyupgrade with the --py3-only flag suggests the following cleanups:

find . -name \*.py -exec pyupgrade --py3-only --keep-percent-format {} \;

(feel free to drop --keep-percent-format if you prefer)

diff
diff --git a/rest_framework/authtoken/management/commands/drf_create_token.py b/rest_framework/authtoken/management/commands/drf_create_token.py
index 8e06812d..3d653924 100644
--- a/rest_framework/authtoken/management/commands/drf_create_token.py
+++ b/rest_framework/authtoken/management/commands/drf_create_token.py
@@ -38,8 +38,8 @@ class Command(BaseCommand):
             token = self.create_user_token(username, reset_token)
         except UserModel.DoesNotExist:
             raise CommandError(
-                'Cannot create the Token: user {0} does not exist'.format(
+                'Cannot create the Token: user {} does not exist'.format(
                     username)
             )
         self.stdout.write(
-            'Generated token {0} for user {1}'.format(token.key, username))
+            'Generated token {} for user {}'.format(token.key, username))
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index d93c2da4..8326e138 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -68,7 +68,7 @@ class ErrorDetail(str):
     code = None
 
     def __new__(cls, string, code=None):
-        self = super(ErrorDetail, cls).__new__(cls, string)
+        self = super().__new__(cls, string)
         self.code = code
         return self
 
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 4418579d..ecbbbe0b 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -88,7 +88,7 @@ def get_attribute(instance, attrs):
                 # If we raised an Attribute or KeyError here it'd get treated
                 # as an omitted field in `Field.get_attribute()`. Instead we
                 # raise a ValueError to ensure the exception is not masked.
-                raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc))
+                raise ValueError('Exception raised in callable attribute "{}"; original exception was: {}'.format(attr, exc))
 
     return instance
 
@@ -598,7 +598,7 @@ class Field:
         When a field is instantiated, we store the arguments that were used,
         so that we can present a helpful representation of the object.
         """
-        instance = super(Field, cls).__new__(cls)
+        instance = super().__new__(cls)
         instance._args = args
         instance._kwargs = kwargs
         return instance
@@ -843,7 +843,7 @@ class UUIDField(Field):
         if self.uuid_format not in self.valid_formats:
             raise ValueError(
                 'Invalid format for uuid representation. '
-                'Must be one of "{0}"'.format('", "'.join(self.valid_formats))
+                'Must be one of "{}"'.format('", "'.join(self.valid_formats))
             )
         super().__init__(**kwargs)
 
@@ -1104,7 +1104,7 @@ class DecimalField(Field):
         if self.localize:
             return localize_input(quantized)
 
-        return '{0:f}'.format(quantized)
+        return '{:f}'.format(quantized)
 
     def quantize(self, value):
         """
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index 292c8f62..76113a82 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -64,7 +64,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
                             # Accepted media type is 'application/json'
                             full_media_type = ';'.join(
                                 (renderer.media_type,) +
-                                tuple('{0}={1}'.format(
+                                tuple('{}={}'.format(
                                     key, value.decode(HTTP_HEADER_ENCODING))
                                     for key, value in media_type_wrapper.params.items()))
                             return renderer, full_media_type
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index e4de4911..76c4d700 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -121,7 +121,7 @@ class RelatedField(Field):
         # `ManyRelatedField` classes instead when `many=True` is set.
         if kwargs.pop('many', False):
             return cls.many_init(*args, **kwargs)
-        return super(RelatedField, cls).__new__(cls, *args, **kwargs)
+        return super().__new__(cls, *args, **kwargs)
 
     @classmethod
     def many_init(cls, *args, **kwargs):
diff --git a/rest_framework/response.py b/rest_framework/response.py
index e031fc2a..db797777 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -62,7 +62,7 @@ class Response(SimpleTemplateResponse):
         content_type = self.content_type
 
         if content_type is None and charset is not None:
-            content_type = "{0}; charset={1}".format(media_type, charset)
+            content_type = "{}; charset={}".format(media_type, charset)
         elif content_type is None:
             content_type = media_type
         self['Content-Type'] = content_type
diff --git a/rest_framework/schemas/views.py b/rest_framework/schemas/views.py
index 73b4a807..fa5cdbdc 100644
--- a/rest_framework/schemas/views.py
+++ b/rest_framework/schemas/views.py
@@ -38,4 +38,4 @@ class SchemaView(APIView):
         self.renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
         neg = self.perform_content_negotiation(self.request, force=True)
         self.request.accepted_renderer, self.request.accepted_media_type = neg
-        return super(SchemaView, self).handle_exception(exc)
+        return super().handle_exception(exc)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d36b866b..7edbc12f 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -113,14 +113,14 @@ class BaseSerializer(Field):
         self.partial = kwargs.pop('partial', False)
         self._context = kwargs.pop('context', {})
         kwargs.pop('many', None)
-        super(BaseSerializer, self).__init__(**kwargs)
+        super().__init__(**kwargs)
 
     def __new__(cls, *args, **kwargs):
         # We override this method in order to automagically create
         # `ListSerializer` classes instead when `many=True` is set.
         if kwargs.pop('many', False):
             return cls.many_init(*args, **kwargs)
-        return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
+        return super().__new__(cls, *args, **kwargs)
 
     @classmethod
     def many_init(cls, *args, **kwargs):
@@ -313,7 +313,7 @@ class SerializerMetaclass(type):
 
     def __new__(cls, name, bases, attrs):
         attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs)
-        return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
+        return super().__new__(cls, name, bases, attrs)
 
 
 def as_serializer_error(exc):
@@ -463,7 +463,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
             to_validate.update(value)
         else:
             to_validate = value
-        super(Serializer, self).run_validators(to_validate)
+        super().run_validators(to_validate)
 
     def to_internal_value(self, data):
         """
@@ -557,12 +557,12 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
 
     @property
     def data(self):
-        ret = super(Serializer, self).data
+        ret = super().data
         return ReturnDict(ret, serializer=self)
 
     @property
     def errors(self):
-        ret = super(Serializer, self).errors
+        ret = super().errors
         if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null':
             # Edge case. Provide a more descriptive error than
             # "this field may not be null", when no data is passed.
@@ -588,11 +588,11 @@ class ListSerializer(BaseSerializer):
         self.allow_empty = kwargs.pop('allow_empty', True)
         assert self.child is not None, '`child` is a required argument.'
         assert not inspect.isclass(self.child), '`child` has not been instantiated.'
-        super(ListSerializer, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         self.child.bind(field_name='', parent=self)
 
     def bind(self, field_name, parent):
-        super(ListSerializer, self).bind(field_name, parent)
+        super().bind(field_name, parent)
         self.partial = self.parent.partial
 
     def get_initial(self):
@@ -762,12 +762,12 @@ class ListSerializer(BaseSerializer):
 
     @property
     def data(self):
-        ret = super(ListSerializer, self).data
+        ret = super().data
         return ReturnList(ret, serializer=self)
 
     @property
     def errors(self):
-        ret = super(ListSerializer, self).errors
+        ret = super().errors
         if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null':
             # Edge case. Provide a more descriptive error than
             # "this field may not be null", when no data is passed.
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 54042d6e..852d4919 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -104,7 +104,7 @@ if requests is not None:
 
     class RequestsClient(requests.Session):
         def __init__(self, *args, **kwargs):
-            super(RequestsClient, self).__init__(*args, **kwargs)
+            super().__init__(*args, **kwargs)
             adapter = DjangoTestAdapter()
             self.mount('http://', adapter)
             self.mount('https://', adapter)
@@ -112,7 +112,7 @@ if requests is not None:
         def request(self, method, url, *args, **kwargs):
             if not url.startswith('http'):
                 raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url)
-            return super(RequestsClient, self).request(method, url, *args, **kwargs)
+            return super().request(method, url, *args, **kwargs)
 
 else:
     def RequestsClient(*args, **kwargs):
@@ -124,7 +124,7 @@ if coreapi is not None:
         def __init__(self, *args, **kwargs):
             self._session = RequestsClient()
             kwargs['transports'] = [coreapi.transports.HTTPTransport(session=self.session)]
-            return super(CoreAPIClient, self).__init__(*args, **kwargs)
+            return super().__init__(*args, **kwargs)
 
         @property
         def session(self):
@@ -144,7 +144,7 @@ class APIRequestFactory(DjangoRequestFactory):
         self.renderer_classes = {}
         for cls in self.renderer_classes_list:
             self.renderer_classes[cls.format] = cls
-        super(APIRequestFactory, self).__init__(**defaults)
+        super().__init__(**defaults)
 
     def _encode_data(self, data, format=None, content_type=None):
         """
@@ -166,7 +166,7 @@ class APIRequestFactory(DjangoRequestFactory):
             format = format or self.default_format
 
             assert format in self.renderer_classes, (
-                "Invalid format '{0}'. Available formats are {1}. "
+                "Invalid format '{}'. Available formats are {}. "
                 "Set TEST_REQUEST_RENDERER_CLASSES to enable "
                 "extra request formats.".format(
                     format,
@@ -179,7 +179,7 @@ class APIRequestFactory(DjangoRequestFactory):
             ret = renderer.render(data)
 
             # Determine the content-type header from the renderer
-            content_type = "{0}; charset={1}".format(
+            content_type = "{}; charset={}".format(
                 renderer.media_type, renderer.charset
             )
 
@@ -228,11 +228,11 @@ class APIRequestFactory(DjangoRequestFactory):
         if content_type is not None:
             extra['CONTENT_TYPE'] = str(content_type)
 
-        return super(APIRequestFactory, self).generic(
+        return super().generic(
             method, path, data, content_type, secure, **extra)
 
     def request(self, **kwargs):
-        request = super(APIRequestFactory, self).request(**kwargs)
+        request = super().request(**kwargs)
         request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
         return request
 
@@ -246,18 +246,18 @@ class ForceAuthClientHandler(ClientHandler):
     def __init__(self, *args, **kwargs):
         self._force_user = None
         self._force_token = None
-        super(ForceAuthClientHandler, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
     def get_response(self, request):
         # This is the simplest place we can hook into to patch the
         # request object.
         force_authenticate(request, self._force_user, self._force_token)
-        return super(ForceAuthClientHandler, self).get_response(request)
+        return super().get_response(request)
 
 
 class APIClient(APIRequestFactory, DjangoClient):
     def __init__(self, enforce_csrf_checks=False, **defaults):
-        super(APIClient, self).__init__(**defaults)
+        super().__init__(**defaults)
         self.handler = ForceAuthClientHandler(enforce_csrf_checks)
         self._credentials = {}
 
@@ -280,17 +280,17 @@ class APIClient(APIRequestFactory, DjangoClient):
     def request(self, **kwargs):
         # Ensure that any credentials set get added to every request.
         kwargs.update(self._credentials)
-        return super(APIClient, self).request(**kwargs)
+        return super().request(**kwargs)
 
     def get(self, path, data=None, follow=False, **extra):
-        response = super(APIClient, self).get(path, data=data, **extra)
+        response = super().get(path, data=data, **extra)
         if follow:
             response = self._handle_redirects(response, **extra)
         return response
 
     def post(self, path, data=None, format=None, content_type=None,
              follow=False, **extra):
-        response = super(APIClient, self).post(
+        response = super().post(
             path, data=data, format=format, content_type=content_type, **extra)
         if follow:
             response = self._handle_redirects(response, **extra)
@@ -298,7 +298,7 @@ class APIClient(APIRequestFactory, DjangoClient):
 
     def put(self, path, data=None, format=None, content_type=None,
             follow=False, **extra):
-        response = super(APIClient, self).put(
+        response = super().put(
             path, data=data, format=format, content_type=content_type, **extra)
         if follow:
             response = self._handle_redirects(response, **extra)
@@ -306,7 +306,7 @@ class APIClient(APIRequestFactory, DjangoClient):
 
     def patch(self, path, data=None, format=None, content_type=None,
               follow=False, **extra):
-        response = super(APIClient, self).patch(
+        response = super().patch(
             path, data=data, format=format, content_type=content_type, **extra)
         if follow:
             response = self._handle_redirects(response, **extra)
@@ -314,7 +314,7 @@ class APIClient(APIRequestFactory, DjangoClient):
 
     def delete(self, path, data=None, format=None, content_type=None,
                follow=False, **extra):
-        response = super(APIClient, self).delete(
+        response = super().delete(
             path, data=data, format=format, content_type=content_type, **extra)
         if follow:
             response = self._handle_redirects(response, **extra)
@@ -322,7 +322,7 @@ class APIClient(APIRequestFactory, DjangoClient):
 
     def options(self, path, data=None, format=None, content_type=None,
                 follow=False, **extra):
-        response = super(APIClient, self).options(
+        response = super().options(
             path, data=data, format=format, content_type=content_type, **extra)
         if follow:
             response = self._handle_redirects(response, **extra)
@@ -336,7 +336,7 @@ class APIClient(APIRequestFactory, DjangoClient):
         self.handler._force_token = None
 
         if self.session:
-            super(APIClient, self).logout()
+            super().logout()
 
 
 class APITransactionTestCase(testcases.TransactionTestCase):
@@ -383,11 +383,11 @@ class URLPatternsTestCase(testcases.SimpleTestCase):
         cls._module.urlpatterns = cls.urlpatterns
 
         cls._override.enable()
-        super(URLPatternsTestCase, cls).setUpClass()
+        super().setUpClass()
 
     @classmethod
     def tearDownClass(cls):
-        super(URLPatternsTestCase, cls).tearDownClass()
+        super().tearDownClass()
         cls._override.disable()
 
         if hasattr(cls, '_module_urlpatterns'):
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index bc4ad886..0ba2ba66 100644
--- a/rest_framework/throttling.py
+++ b/rest_framework/throttling.py
@@ -230,7 +230,7 @@ class ScopedRateThrottle(SimpleRateThrottle):
         self.num_requests, self.duration = self.parse_rate(self.rate)
 
         # We can now proceed as normal.
-        return super(ScopedRateThrottle, self).allow_request(request, view)
+        return super().allow_request(request, view)
 
     def get_cache_key(self, request, view):
         """
diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py
index 8e27a925..0e56333e 100644
--- a/rest_framework/utils/serializer_helpers.py
+++ b/rest_framework/utils/serializer_helpers.py
@@ -101,7 +101,7 @@ class NestedBoundField(BoundField):
     """
 
     def __init__(self, field, value, errors, prefix=''):
-        if value is None or value is '':
+        if value is None or value == '':
             value = {}
         super().__init__(field, value, errors, prefix)
 
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index d776df8c..0631a75c 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -84,7 +84,7 @@ class URLPathVersioning(BaseVersioning):
             kwargs = {} if (kwargs is None) else kwargs
             kwargs[self.version_param] = request.version
 
-        return super(URLPathVersioning, self).reverse(
+        return super().reverse(
             viewname, args, kwargs, request, format, **extra
         )
 
@@ -130,7 +130,7 @@ class NamespaceVersioning(BaseVersioning):
     def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
         if request.version is not None:
             viewname = self.get_versioned_viewname(viewname, request)
-        return super(NamespaceVersioning, self).reverse(
+        return super().reverse(
             viewname, args, kwargs, request, format, **extra
         )
 
@@ -176,7 +176,7 @@ class QueryParameterVersioning(BaseVersioning):
         return version
 
     def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
-        url = super(QueryParameterVersioning, self).reverse(
+        url = super().reverse(
             viewname, args, kwargs, request, format, **extra
         )
         if request.version is not None:
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 354e03ec..6ef7021d 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -135,7 +135,7 @@ class APIView(View):
                 )
             cls.queryset._fetch_all = force_evaluation
 
-        view = super(APIView, cls).as_view(**initkwargs)
+        view = super().as_view(**initkwargs)
         view.cls = cls
         view.initkwargs = initkwargs
 
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index 90ae603e..ad563385 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -132,7 +132,7 @@ class ViewSetMixin:
         """
         Set the `.action` attribute on the view, depending on the request method.
         """
-        request = super(ViewSetMixin, self).initialize_request(request, *args, **kwargs)
+        request = super().initialize_request(request, *args, **kwargs)
         method = request.method.lower()
         if method == 'options':
             # This is a special case as we always provide handling for the
diff --git a/tests/authentication/test_authentication.py b/tests/authentication/test_authentication.py
index e73a3438..f7e9fcf1 100644
--- a/tests/authentication/test_authentication.py
+++ b/tests/authentication/test_authentication.py
@@ -529,7 +529,7 @@ class BasicAuthenticationUnitTests(TestCase):
     def test_basic_authentication_raises_error_if_user_not_active(self):
         from rest_framework import authentication
 
-        class MockUser(object):
+        class MockUser:
             is_active = False
         old_authenticate = authentication.authenticate
         authentication.authenticate = lambda **kwargs: MockUser()
diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py
index 0e807dec..de04d2c0 100644
--- a/tests/test_atomic_requests.py
+++ b/tests/test_atomic_requests.py
@@ -36,7 +36,7 @@ class APIExceptionView(APIView):
 class NonAtomicAPIExceptionView(APIView):
     @transaction.non_atomic_requests
     def dispatch(self, *args, **kwargs):
-        return super(NonAtomicAPIExceptionView, self).dispatch(*args, **kwargs)
+        return super().dispatch(*args, **kwargs)
 
     def get(self, request, *args, **kwargs):
         BasicModel.objects.all()
diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py
index e588ae62..dc5ab542 100644
--- a/tests/test_bound_fields.py
+++ b/tests/test_bound_fields.py
@@ -28,7 +28,7 @@ class TestSimpleBoundField:
         assert serializer['text'].value == 'abc'
         assert serializer['text'].errors is None
         assert serializer['text'].name == 'text'
-        assert serializer['amount'].value is 123
+        assert serializer['amount'].value == 123
         assert serializer['amount'].errors is None
         assert serializer['amount'].name == 'amount'
 
@@ -43,7 +43,7 @@ class TestSimpleBoundField:
         assert serializer['text'].value == 'x' * 1000
         assert serializer['text'].errors == ['Ensure this field has no more than 100 characters.']
         assert serializer['text'].name == 'text'
-        assert serializer['amount'].value is 123
+        assert serializer['amount'].value == 123
         assert serializer['amount'].errors is None
         assert serializer['amount'].name == 'amount'
 
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 3b9f3482..247f5b73 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -165,7 +165,7 @@ class TestEmpty:
         """
         field = serializers.IntegerField(default=123)
         output = field.run_validation()
-        assert output is 123
+        assert output == 123
 
 
 class TestSource:
@@ -752,7 +752,7 @@ class TestCharField(FieldValues):
         def raise_exception(value):
             raise exceptions.ValidationError('Raised error')
 
-        for validators in ([raise_exception], (raise_exception,), set([raise_exception])):
+        for validators in ([raise_exception], (raise_exception,), {raise_exception}):
             field = serializers.CharField(validators=validators)
             with pytest.raises(serializers.ValidationError) as exc_info:
                 field.run_validation(value)
@@ -820,7 +820,7 @@ class TestSlugField(FieldValues):
 
         validation_error = False
         try:
-            field.run_validation(u'slug-99-\u0420')
+            field.run_validation('slug-99-\u0420')
         except serializers.ValidationError:
             validation_error = True
 
@@ -1626,7 +1626,7 @@ class TestChoiceField(FieldValues):
             ]
         )
         field.choices = [1]
-        assert field.run_validation(1) is 1
+        assert field.run_validation(1) == 1
         with pytest.raises(serializers.ValidationError) as exc_info:
             field.run_validation(2)
         assert exc_info.value.detail == ['"2" is not a valid choice.']
diff --git a/tests/test_filters.py b/tests/test_filters.py
index bbe70c01..a52f4010 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -161,7 +161,7 @@ class SearchFilterTests(TestCase):
             def get_search_fields(self, view, request):
                 if request.query_params.get('title_only'):
                     return ('$title',)
-                return super(CustomSearchFilter, self).get_search_fields(view, request)
+                return super().get_search_fields(view, request)
 
         class SearchListView(generics.ListAPIView):
             queryset = SearchFilterModel.objects.all()
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 13539082..f41ebe6d 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -288,7 +288,7 @@ class TestInstanceView(TestCase):
         """
         data = {'text': 'foo'}
         filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
-        request = factory.put('/{0}'.format(filtered_out_pk), data, format='json')
+        request = factory.put('/{}'.format(filtered_out_pk), data, format='json')
         response = self.view(request, pk=filtered_out_pk).render()
         assert response.status_code == status.HTTP_404_NOT_FOUND
 
@@ -443,12 +443,12 @@ class TestM2MBrowsableAPI(TestCase):
         assert response.status_code == status.HTTP_200_OK
 
 
-class InclusiveFilterBackend(object):
+class InclusiveFilterBackend:
     def filter_queryset(self, request, queryset, view):
         return queryset.filter(text='foo')
 
 
-class ExclusiveFilterBackend(object):
+class ExclusiveFilterBackend:
     def filter_queryset(self, request, queryset, view):
         return queryset.filter(text='other')
 
@@ -650,7 +650,7 @@ class ApiViewsTests(TestCase):
 
 class GetObjectOr404Tests(TestCase):
     def setUp(self):
-        super(GetObjectOr404Tests, self).setUp()
+        super().setUp()
         self.uuid_object = UUIDForeignKeyTarget.objects.create(name='bar')
 
     def test_get_object_or_404_with_valid_uuid(self):
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index d339d507..2b9ab751 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -414,7 +414,7 @@ class TestGenericIPAddressFieldValidation(TestCase):
         self.assertFalse(s.is_valid())
         self.assertEqual(1, len(s.errors['address']),
                          'Unexpected number of validation errors: '
-                         '{0}'.format(s.errors))
+                         '{}'.format(s.errors))
 
 
 @pytest.mark.skipif('not postgres_fields')
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 1044992a..3c581ddf 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -499,7 +499,7 @@ class TestLimitOffset:
         content = self.get_paginated_content(queryset)
         next_limit = self.pagination.default_limit
         next_offset = self.pagination.default_limit
-        next_url = 'http://testserver/?limit={0}&offset={1}'.format(next_limit, next_offset)
+        next_url = 'http://testserver/?limit={}&offset={}'.format(next_limit, next_offset)
         assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
         assert content.get('next') == next_url
 
@@ -512,7 +512,7 @@ class TestLimitOffset:
         content = self.get_paginated_content(queryset)
         next_limit = self.pagination.default_limit
         next_offset = self.pagination.default_limit
-        next_url = 'http://testserver/?limit={0}&offset={1}'.format(next_limit, next_offset)
+        next_url = 'http://testserver/?limit={}&offset={}'.format(next_limit, next_offset)
         assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
         assert content.get('next') == next_url
 
@@ -528,9 +528,9 @@ class TestLimitOffset:
         max_limit = self.pagination.max_limit
         next_offset = offset + max_limit
         prev_offset = offset - max_limit
-        base_url = 'http://testserver/?limit={0}'.format(max_limit)
-        next_url = base_url + '&offset={0}'.format(next_offset)
-        prev_url = base_url + '&offset={0}'.format(prev_offset)
+        base_url = 'http://testserver/?limit={}'.format(max_limit)
+        next_url = base_url + '&offset={}'.format(next_offset)
+        prev_url = base_url + '&offset={}'.format(prev_offset)
         assert queryset == list(range(51, 66))
         assert content.get('next') == next_url
         assert content.get('previous') == prev_url
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
index 0ee3f1e0..c385eaef 100644
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -329,14 +329,14 @@ class ObjectPermissionsIntegrationTests(TestCase):
         everyone = Group.objects.create(name='everyone')
         model_name = BasicPermModel._meta.model_name
         app_label = BasicPermModel._meta.app_label
-        f = '{0}_{1}'.format
+        f = '{}_{}'.format
         perms = {
             'view': f('view', model_name),
             'change': f('change', model_name),
             'delete': f('delete', model_name)
         }
         for perm in perms.values():
-            perm = '{0}.{1}'.format(app_label, perm)
+            perm = '{}.{}'.format(app_label, perm)
             assign_perm(perm, everyone)
         everyone.user_set.add(*users.values())
 
diff --git a/tests/test_relations.py b/tests/test_relations.py
index 3c4b7d90..3281b7ea 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -26,7 +26,7 @@ class TestStringRelatedField(APISimpleTestCase):
         assert representation == '<MockObject name=foo, pk=1>'
 
 
-class MockApiSettings(object):
+class MockApiSettings:
     def __init__(self, cutoff, cutoff_text):
         self.HTML_SELECT_CUTOFF = cutoff
         self.HTML_SELECT_CUTOFF_TEXT = cutoff_text
diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py
index 3da3dc3e..0da9da89 100644
--- a/tests/test_relations_pk.py
+++ b/tests/test_relations_pk.py
@@ -559,7 +559,7 @@ class OneToOnePrimaryKeyTests(TestCase):
         # When: Trying to create a second object
         second_source = OneToOnePKSourceSerializer(data=data)
         self.assertFalse(second_source.is_valid())
-        expected = {'target': [u'one to one pk source with this target already exists.']}
+        expected = {'target': ['one to one pk source with this target already exists.']}
         self.assertDictEqual(second_source.errors, expected)
 
     def test_one_to_one_when_primary_key_does_not_exist(self):
diff --git a/tests/test_request.py b/tests/test_request.py
index 55268b8e..0f682deb 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -232,7 +232,7 @@ class TestUserSetter(TestCase):
         This proves that when an AttributeError is raised inside of the request.user
         property, that we can handle this and report the true, underlying error.
         """
-        class AuthRaisesAttributeError(object):
+        class AuthRaisesAttributeError:
             def authenticate(self, request):
                 self.MISSPELLED_NAME_THAT_DOESNT_EXIST
 
diff --git a/tests/test_response.py b/tests/test_response.py
index 0f74c097..d3a56d01 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -257,7 +257,7 @@ class Issue807Tests(TestCase):
         """
         headers = {"HTTP_ACCEPT": RendererA.media_type}
         resp = self.client.get('/', **headers)
-        expected = "{0}; charset={1}".format(RendererA.media_type, 'utf-8')
+        expected = "{}; charset={}".format(RendererA.media_type, 'utf-8')
         self.assertEqual(expected, resp['Content-Type'])
 
     def test_if_there_is_charset_specified_on_renderer_it_gets_appended(self):
@@ -267,7 +267,7 @@ class Issue807Tests(TestCase):
         """
         headers = {"HTTP_ACCEPT": RendererC.media_type}
         resp = self.client.get('/', **headers)
-        expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset)
+        expected = "{}; charset={}".format(RendererC.media_type, RendererC.charset)
         self.assertEqual(expected, resp['Content-Type'])
 
     def test_content_type_set_explicitly_on_response(self):
diff --git a/tests/test_schemas.py b/tests/test_schemas.py
index fc7a8302..1aad5d1d 100644
--- a/tests/test_schemas.py
+++ b/tests/test_schemas.py
@@ -112,7 +112,7 @@ class ExampleViewSet(ModelViewSet):
     def get_serializer(self, *args, **kwargs):
         assert self.request
         assert self.action
-        return super(ExampleViewSet, self).get_serializer(*args, **kwargs)
+        return super().get_serializer(*args, **kwargs)
 
     @action(methods=['get', 'post'], detail=False)
     def documented_custom_action(self, request):
@@ -1303,7 +1303,7 @@ def test_head_and_options_methods_are_excluded():
 
 
 @pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
-class TestAutoSchemaAllowsFilters(object):
+class TestAutoSchemaAllowsFilters:
     class MockAPIView(APIView):
         filter_backends = [filters.OrderingFilter]
 
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index 8a1df04e..7feb5a87 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -168,7 +168,7 @@ class TestSerializer:
             latitude = serializers.FloatField(source='y')
 
             def to_internal_value(self, data):
-                kwargs = super(NestedPointSerializer, self).to_internal_value(data)
+                kwargs = super().to_internal_value(data)
                 return Point(srid=4326, **kwargs)
 
         serializer = NestedPointSerializer(data={'longitude': 6.958307, 'latitude': 50.941357})
@@ -198,7 +198,7 @@ class TestSerializer:
         def raise_exception(value):
             raise exceptions.ValidationError('Raised error')
 
-        for validators in ([raise_exception], (raise_exception,), set([raise_exception])):
+        for validators in ([raise_exception], (raise_exception,), {raise_exception}):
             class ExampleSerializer(serializers.Serializer):
                 char = serializers.CharField(validators=validators)
                 integer = serializers.IntegerField()
@@ -606,7 +606,7 @@ class Test2555Regression:
     def test_serializer_context(self):
         class NestedSerializer(serializers.Serializer):
             def __init__(self, *args, **kwargs):
-                super(NestedSerializer, self).__init__(*args, **kwargs)
+                super().__init__(*args, **kwargs)
                 # .context should not cache
                 self.context
 
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
index 796a7f67..12816088 100644
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -222,7 +222,7 @@ class TemplateTagTests(TestCase):
         assert result == ''
 
     def test_get_pagination_html(self):
-        class MockPager(object):
+        class MockPager:
             def __init__(self):
                 self.called = False
 
@@ -337,7 +337,7 @@ class SchemaLinksTests(TestCase):
         )
         section = schema['users']
         flat_links = schema_links(section)
-        assert len(flat_links) is 0
+        assert len(flat_links) == 0
 
     def test_single_action(self):
         schema = coreapi.Document(
@@ -355,7 +355,7 @@ class SchemaLinksTests(TestCase):
         )
         section = schema['users']
         flat_links = schema_links(section)
-        assert len(flat_links) is 1
+        assert len(flat_links) == 1
         assert 'list' in flat_links
 
     def test_default_actions(self):
@@ -393,7 +393,7 @@ class SchemaLinksTests(TestCase):
         )
         section = schema['users']
         flat_links = schema_links(section)
-        assert len(flat_links) is 4
+        assert len(flat_links) == 4
         assert 'list' in flat_links
         assert 'create' in flat_links
         assert 'read' in flat_links
@@ -441,7 +441,7 @@ class SchemaLinksTests(TestCase):
         )
         section = schema['users']
         flat_links = schema_links(section)
-        assert len(flat_links) is 5
+        assert len(flat_links) == 5
         assert 'list' in flat_links
         assert 'create' in flat_links
         assert 'read' in flat_links
@@ -499,7 +499,7 @@ class SchemaLinksTests(TestCase):
         )
         section = schema['users']
         flat_links = schema_links(section)
-        assert len(flat_links) is 6
+        assert len(flat_links) == 6
         assert 'list' in flat_links
         assert 'create' in flat_links
         assert 'read' in flat_links
@@ -550,7 +550,7 @@ class SchemaLinksTests(TestCase):
         )
         section = schema['animals']
         flat_links = schema_links(section)
-        assert len(flat_links) is 4
+        assert len(flat_links) == 4
         assert 'cat > create' in flat_links
         assert 'cat > list' in flat_links
         assert 'dog > read' in flat_links
@@ -619,7 +619,7 @@ class SchemaLinksTests(TestCase):
         )
         section = schema['animals']
         flat_links = schema_links(section)
-        assert len(flat_links) is 4
+        assert len(flat_links) == 4
         assert 'cat > create' in flat_links
         assert 'cat > list' in flat_links
         assert 'dog > read' in flat_links
@@ -627,6 +627,6 @@ class SchemaLinksTests(TestCase):
 
         section = schema['farmers']
         flat_links = schema_links(section)
-        assert len(flat_links) is 2
+        assert len(flat_links) == 2
         assert 'silo > list' in flat_links
         assert 'silo > soy > list' in flat_links
diff --git a/tests/test_testing.py b/tests/test_testing.py
index b7fc7e30..8094bfd8 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -290,13 +290,13 @@ class TestUrlPatternTestCase(URLPatternsTestCase):
     @classmethod
     def setUpClass(cls):
         assert urlpatterns is not cls.urlpatterns
-        super(TestUrlPatternTestCase, cls).setUpClass()
+        super().setUpClass()
         assert urlpatterns is cls.urlpatterns
 
     @classmethod
     def tearDownClass(cls):
         assert urlpatterns is cls.urlpatterns
-        super(TestUrlPatternTestCase, cls).tearDownClass()
+        super().tearDownClass()
         assert urlpatterns is not cls.urlpatterns
 
     def test_urlpatterns(self):
diff --git a/tests/test_validation.py b/tests/test_validation.py
index 9e0d6b96..6e00b48c 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -148,14 +148,14 @@ class TestMaxValueValidatorValidation(TestCase):
 
     def test_max_value_validation_success(self):
         obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
-        request = factory.patch('/{0}'.format(obj.pk), {'number_value': 98}, format='json')
+        request = factory.patch('/{}'.format(obj.pk), {'number_value': 98}, format='json')
         view = UpdateMaxValueValidationModel().as_view()
         response = view(request, pk=obj.pk).render()
         assert response.status_code == status.HTTP_200_OK
 
     def test_max_value_validation_fail(self):
         obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
-        request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json')
+        request = factory.patch('/{}'.format(obj.pk), {'number_value': 101}, format='json')
         view = UpdateMaxValueValidationModel().as_view()
         response = view(request, pk=obj.pk).render()
         assert response.content == b'{"number_value":["Ensure this value is less than or equal to 100."]}'
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 4bbddb64..fe31ba23 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -353,7 +353,7 @@ class TestUniquenessTogetherValidation(TestCase):
         filter_queryset should add value from existing instance attribute
         if it is not provided in attributes dict
         """
-        class MockQueryset(object):
+        class MockQueryset:
             def filter(self, **kwargs):
                 self.called_with = kwargs
 
@@ -558,19 +558,19 @@ class TestHiddenFieldUniquenessForDateValidation(TestCase):
 class ValidatorsTests(TestCase):
 
     def test_qs_exists_handles_type_error(self):
-        class TypeErrorQueryset(object):
+        class TypeErrorQueryset:
             def exists(self):
                 raise TypeError
         assert qs_exists(TypeErrorQueryset()) is False
 
     def test_qs_exists_handles_value_error(self):
-        class ValueErrorQueryset(object):
+        class ValueErrorQueryset:
             def exists(self):
                 raise ValueError
         assert qs_exists(ValueErrorQueryset()) is False
 
     def test_qs_exists_handles_data_error(self):
-        class DataErrorQueryset(object):
+        class DataErrorQueryset:
             def exists(self):
                 raise DataError
         assert qs_exists(DataErrorQueryset()) is False
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
index 526df829..d4e269df 100644
--- a/tests/test_versioning.py
+++ b/tests/test_versioning.py
@@ -319,7 +319,7 @@ class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase):
     ]
 
     def setUp(self):
-        super(TestHyperlinkedRelatedField, self).setUp()
+        super().setUp()
 
         class MockQueryset:
             def get(self, pk):

@jdufresne
Copy link
Contributor

jdufresne commented Apr 30, 2019

This is great! Nice work.

Sorry the if my review looks long. I've built up quite a list of things to check when dropping Python 2 🙂. Feel free to omit any cleanups if you think they aren't worth it or you disagree with them. I can provide a hand if you'd like, just let me know.


The following fallbacks in rest_framework/compat.py are no longer relevant and can probably be removed:

try:
    from unittest import mock
except ImportError:
    mock = None
def unicode_repr(instance):
    # Get the repr of an instance, but ensure it is a unicode string
    # on both python 3 (already the case) and 2 (not the case).

    return repr(instance)


def unicode_to_repr(value):
    # Coerce a unicode string to the correct repr return type, depending on
    # the Python version. We wrap all our `__repr__` implementations with
    # this and then use unicode throughout internally.

    return value

In tests/test_views.py can drop the fallback:

if sys.version_info[:2] >= (3, 4):
    JSON_ERROR = 'JSON parse error - Expecting value:'
else:
    JSON_ERROR = 'JSON parse error - No JSON object could be decoded'

In tests/test_serializer.py, ChainMap is always available:

try:
    from collections import ChainMap
except ImportError:
    ChainMap = False

There are a few more instances of __future__ that can be dropped:

./runtests.py:2:from __future__ import print_function
./rest_framework/utils/json.py:9:from __future__ import absolute_import
./rest_framework/templatetags/rest_framework.py:1:from __future__ import absolute_import, unicode_literals

One remaining encoding cookie that can be dropped:

./rest_framework/pagination.py:1:# coding: utf-8

ErrorDetail.__ne__ can be dropped. Python 3 defaults this to not self.__eq__(...)

(Edit: Nope, can't do this. ErrorDetail inherits str.__ne__ so we must override it.)

https://docs.python.org/3/reference/datamodel.html#object.__ne__

By default, ne() delegates to eq() and inverts the result


You can add the trove classifier: 'Programming Language :: Python :: 3 :: Only' to setup.py.


The docs mention virtualenv quite a few times. You might consider changing this to Python 3's venv.


I would change setup.py and runtests.py shebang to #!/usr/bin/env python3 per PEP-394.


Python 3 allows you to omit 'utf-8' from .encode() and .decode(). Consider cleaning those up.


You can replace the ugettext* functions with the non-u versions.


Drop

[bdist_wheel]
universal = 1

From setup.cfg. The wheel is no longer universal.


Consider replacing ImportError with ModuleNotFoundError when you're only checking for the existence of a module. ImportError catches a lot more, such as syntax or programming errors.

@jdufresne
Copy link
Contributor

The docs still reference Python 2.7 in a few places. Worth verifying if it is still relevant:

./docs/index.md:87:* Python (2.7, 3.4, 3.5, 3.6, 3.7)
./docs/topics/api-clients.md:114:    < Server: WSGIServer/0.1 Python/2.7.10
./docs/api-guide/authentication.md:357:The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib].  The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.

@auvipy
Copy link
Member

auvipy commented Apr 30, 2019

#6617

@carltongibson
Copy link
Collaborator Author

Hey @jdufresne. Super thanks.

Sorry the if my review looks long. I've built up quite a list of things to check when dropping Python 2 🙂.

No. Exactly what I was hoping for. 🙂

I can provide a hand if you'd like, just let me know.

Happy to take all this. If you have capacity a PR onto carltongibson:drop-python-2 would be great.

@auvipy :

py34 is already EOL :)

Happy to drop Python 3.4 for the 3.10 release, but lets do it in a separate PR (after merging this).

Thanks!

@carltongibson
Copy link
Collaborator Author

OK, outstanding, but I'll leave for a follow-up:

Python 3 allows you to omit 'utf-8' from .encode() and .decode(). Consider cleaning those up.

You can replace the ugettext* functions with the non-u versions.

Consider replacing ImportError with ModuleNotFoundError when you're only checking for the existence of a module. ImportError catches a lot more, such as syntax or programming errors.

And

The docs still reference Python 2.7 in a few places. Worth verifying if it is still relevant:

./docs/index.md:87:* Python (2.7, 3.4, 3.5, 3.6, 3.7)
./docs/topics/api-clients.md:114:    < Server: WSGIServer/0.1 Python/2.7.10
./docs/api-guide/authentication.md:357:The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib].  The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.

Thanks to Jon Dufresne (@jdufresne) for review.

Co-authored-by: Asif Saif Uddin <[email protected]>
Co-authored-by: Rizwan Mansuri <[email protected]>
@carltongibson
Copy link
Collaborator Author

Hey @jdufresne. Thanks. I'll pulled in most of your comments. Assuming CI is green I'll merge this and we can add other clean ups separately.

@carltongibson carltongibson merged commit 0407a0d into encode:master Apr 30, 2019
@auvipy
Copy link
Member

auvipy commented Apr 30, 2019

yeah!! 👯‍♂️

@rpkilby rpkilby added this to the 3.10 Release milestone May 6, 2019
terencehonles pushed a commit to terencehonles/django-rest-framework that referenced this pull request Oct 8, 2020
Thanks to Jon Dufresne (@jdufresne) for review.

Co-authored-by: Asif Saif Uddin <[email protected]>
Co-authored-by: Rizwan Mansuri <[email protected]>
pchiquet pushed a commit to pchiquet/django-rest-framework that referenced this pull request Nov 17, 2020
Thanks to Jon Dufresne (@jdufresne) for review.

Co-authored-by: Asif Saif Uddin <[email protected]>
Co-authored-by: Rizwan Mansuri <[email protected]>
sigvef pushed a commit to sigvef/django-rest-framework that referenced this pull request Dec 3, 2022
Thanks to Jon Dufresne (@jdufresne) for review.

Co-authored-by: Asif Saif Uddin <[email protected]>
Co-authored-by: Rizwan Mansuri <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants