From 86c72bb2268ad06c503de4fb7caa46c9e495f8d8 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Thu, 7 Mar 2019 05:26:03 -0500 Subject: [PATCH 01/69] Fix schema generation of `ManyRelatedField` to detect the child type (#6489) * Introspect ManyRelatedField data type recursively For all `ManyRelatedField` objects, we were assuming that the inner type was always a `String`. While this may be true for the default output, a `ManyRelatedField` is a wrapper for a lot of other classes which includes more than just strings. This should allow us to document lists of things other than strings. * Added test for schemas for many-to-many fields This adds a test that makes sure we generate the schema for a many-to-many field such that it actually has the right type. For some reason we did not previously have any tests for schema generation that included them, so hopefully this will prevent any future issues from popping up. This should serve as a regression test for the `items` field on to-many relationships, which was previously forced to a `String` even though in most cases it is a different inner type within the array. --- rest_framework/schemas/inspectors.py | 4 ++- tests/test_schemas.py | 47 +++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index a17a1f1aa..85142edce 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -51,8 +51,10 @@ def field_to_schema(field): description=description ) elif isinstance(field, serializers.ManyRelatedField): + related_field_schema = field_to_schema(field.child_relation) + return coreschema.Array( - items=coreschema.String(), + items=related_field_schema, title=title, description=description ) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index d3bd43073..3cb9e0cda 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -24,7 +24,7 @@ from rest_framework.utils import formatting from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet, ModelViewSet -from .models import BasicModel, ForeignKeySource +from .models import BasicModel, ForeignKeySource, ManyToManySource factory = APIRequestFactory() @@ -701,6 +701,51 @@ class TestSchemaGeneratorWithForeignKey(TestCase): assert schema == expected +class ManyToManySourceSerializer(serializers.ModelSerializer): + class Meta: + model = ManyToManySource + fields = ('id', 'name', 'targets') + + +class ManyToManySourceView(generics.CreateAPIView): + queryset = ManyToManySource.objects.all() + serializer_class = ManyToManySourceSerializer + + +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class TestSchemaGeneratorWithManyToMany(TestCase): + def setUp(self): + self.patterns = [ + url(r'^example/?$', ManyToManySourceView.as_view()), + ] + + def test_schema_for_regular_views(self): + """ + Ensure that AutoField many to many fields are output as Integer. + """ + generator = SchemaGenerator(title='Example API', patterns=self.patterns) + schema = generator.get_schema() + + expected = coreapi.Document( + url='', + title='Example API', + content={ + 'example': { + 'create': coreapi.Link( + url='/example/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('name', required=True, location='form', schema=coreschema.String(title='Name')), + coreapi.Field('targets', required=True, location='form', schema=coreschema.Array(title='Targets', items=coreschema.Integer())), + ] + ) + } + } + ) + assert schema == expected + + @unittest.skipUnless(coreapi, 'coreapi is not installed') class Test4605Regression(TestCase): def test_4605_regression(self): From bcdfcf7e493663d8738b9f07d59f9426b0cfb5a3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 7 Mar 2019 11:00:50 +0000 Subject: [PATCH 02/69] Sponsor updates (#6495) --- README.md | 10 ++++------ docs/img/premium/auklet-readme.png | Bin 48745 -> 0 bytes docs/img/premium/release-history.png | Bin 0 -> 18009 bytes docs/index.md | 6 ++---- 4 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 docs/img/premium/auklet-readme.png create mode 100644 docs/img/premium/release-history.png diff --git a/README.md b/README.md index 0309ee2bd..bb05b1d92 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,15 @@ continued development by [signing up for a paid plan][funding]. The initial aim is to provide a single full-time position on REST framework. *Every single sign-up makes a significant impact towards making that possible.* -[![][rover-img]][rover-url] [![][sentry-img]][sentry-url] [![][stream-img]][stream-url] [![][rollbar-img]][rollbar-url] [![][cadre-img]][cadre-url] -[![][load-impact-img]][load-impact-url] [![][kloudless-img]][kloudless-url] -[![][auklet-img]][auklet-url] +[![][release-history-img]][release-history-url] [![][lightson-img]][lightson-url] -Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover][rover-url], [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Load Impact][load-impact-url], [Kloudless][kloudless-url], [Auklet][auklet-url], and [Lights On Software][lightson-url]. +Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Kloudless][kloudless-url], [Release History][release-history-url], and [Lights On Software][lightson-url]. --- @@ -201,7 +199,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou [cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png [load-impact-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/load-impact-readme.png [kloudless-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/kloudless-readme.png -[auklet-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/auklet-readme.png +[release-history-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/release-history-readme.png [lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png [rover-url]: http://jobs.rover.com/ @@ -211,7 +209,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou [cadre-url]: https://cadre.com/ [load-impact-url]: https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf [kloudless-url]: https://hubs.ly/H0f30Lf0 -[auklet-url]: https://auklet.io/ +[release-history-url]: https://releasehistory.io [lightson-url]: https://lightsonsoftware.com [oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth diff --git a/docs/img/premium/auklet-readme.png b/docs/img/premium/auklet-readme.png deleted file mode 100644 index f55f7a70ea7e8a5b9ec6a9de62246606c21f1136..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48745 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 MNn5c&!6xGW08Og_`v3p{ diff --git a/docs/img/premium/release-history.png b/docs/img/premium/release-history.png new file mode 100644 index 0000000000000000000000000000000000000000..b732b1ca23b8ce74f758ce03e90b4e0674ad425a GIT binary patch literal 18009 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 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index c74b2caf0..9f5d3fa15 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,19 +66,17 @@ continued development by **[signing up for a paid plan][funding]**. *Every single sign-up helps us make REST framework long-term financially sustainable.*
-*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Auklet](https://auklet.io/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Load Impact](https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).* +*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Release History](https://releasehistory.io), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).* --- From 9bfb58746ef813fefa6c2528f4b26e462a8ffc1f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 7 Mar 2019 11:02:43 +0000 Subject: [PATCH 03/69] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb05b1d92..66079edf0 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou [cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png [load-impact-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/load-impact-readme.png [kloudless-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/kloudless-readme.png -[release-history-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/release-history-readme.png +[release-history-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/release-history.png [lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png [rover-url]: http://jobs.rover.com/ From 6f24c21cfb1fa072c7bf2f36b54a63b51102f903 Mon Sep 17 00:00:00 2001 From: Matt Hegarty Date: Tue, 12 Mar 2019 11:46:02 +0000 Subject: [PATCH 04/69] Fixed typo: /Janurary/January/ (#6506) --- docs/community/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 288bf6d58..0f08342f5 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -62,7 +62,7 @@ You can determine your currently installed version using `pip show`: ### 3.9.1 -**Date**: [16th Janurary 2019][3.9.1-milestone] +**Date**: [16th January 2019][3.9.1-milestone] * Resolve XSS issue in browsable API. [#6330][gh6330] * Upgrade Bootstrap to 3.4.0 to resolve XSS issue. From d2d1888217e8cc5aba995edf522fec903b1f91be Mon Sep 17 00:00:00 2001 From: Ryan Siemens Date: Tue, 12 Mar 2019 21:15:12 -0700 Subject: [PATCH 05/69] Document DateTimeField default_timezone argument (#6469) --- docs/api-guide/fields.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 74ce2251d..ede4f15ad 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -306,10 +306,11 @@ A date and time representation. Corresponds to `django.db.models.fields.DateTimeField`. -**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)` +**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None, default_timezone=None)` * `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. In this case the datetime encoding will be determined by the renderer. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. +* `default_timezone` - A `pytz.timezone` representing the timezone. If not specified and the `USE_TZ` setting is enabled, this defaults to the [current timezone][django-current-timezone]. If `USE_TZ` is disabled, then datetime objects will be naive. #### `DateTimeField` format strings. @@ -835,3 +836,4 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide [django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore [django-hstore]: https://github.com/djangonauts/django-hstore [python-decimal-rounding-modes]: https://docs.python.org/3/library/decimal.html#rounding-modes +[django-current-timezone]: https://docs.djangoproject.com/en/stable/topics/i18n/timezones/#default-time-zone-and-current-time-zone From b25d245b893ce0a04c9ed33a12ebf6a6a1ff6b44 Mon Sep 17 00:00:00 2001 From: Patrickcai Date: Fri, 22 Mar 2019 20:29:45 +0800 Subject: [PATCH 06/69] Merge multiple isinstance() calls to one (#6513) * Merge multiple isinstance() calls to one See https://docs.python.org/3/library/functions.html#isinstance * Fix `)` mismatch Fix `)` mismatch --- rest_framework/utils/field_mapping.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index f11b4b94e..927d08ff2 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -106,8 +106,7 @@ def get_field_kwargs(field_name, model_field): if model_field.null and not isinstance(model_field, models.NullBooleanField): kwargs['allow_null'] = True - if model_field.blank and (isinstance(model_field, models.CharField) or - isinstance(model_field, models.TextField)): + if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))): kwargs['allow_blank'] = True if isinstance(model_field, models.FilePathField): @@ -193,9 +192,7 @@ def get_field_kwargs(field_name, model_field): # Ensure that max_length is passed explicitly as a keyword arg, # rather than as a validator. max_length = getattr(model_field, 'max_length', None) - if max_length is not None and (isinstance(model_field, models.CharField) or - isinstance(model_field, models.TextField) or - isinstance(model_field, models.FileField)): + if max_length is not None and (isinstance(model_field, (models.CharField, models.TextField, models.FileField))): kwargs['max_length'] = max_length validator_kwarg = [ validator for validator in validator_kwarg From d784e4220762bd8efa6c99887ef1a1c43b51d52c Mon Sep 17 00:00:00 2001 From: Turfa Auliarachman Date: Tue, 26 Mar 2019 00:42:27 +0700 Subject: [PATCH 07/69] Fix `basename` deprecation warnings in tests (#6529) --- tests/test_renderers.py | 2 +- tests/test_routers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index b4c41b148..60a0c0307 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -636,7 +636,7 @@ class BrowsableAPIRendererTests(URLPatternsTestCase): raise NotImplementedError router = SimpleRouter() - router.register('examples', ExampleViewSet, base_name='example') + router.register('examples', ExampleViewSet, basename='example') urlpatterns = [url(r'^api/', include(router.urls))] def setUp(self): diff --git a/tests/test_routers.py b/tests/test_routers.py index a3a731f93..cca2ea712 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -121,7 +121,7 @@ class BasicViewSet(viewsets.ViewSet): class TestSimpleRouter(URLPatternsTestCase, TestCase): router = SimpleRouter() - router.register('basics', BasicViewSet, base_name='basic') + router.register('basics', BasicViewSet, basename='basic') urlpatterns = [ url(r'^api/', include(router.urls)), From ac19c695396cdd0a72b81dc5fece7a850ae3d1b2 Mon Sep 17 00:00:00 2001 From: Jabi Date: Thu, 28 Mar 2019 11:45:13 +0100 Subject: [PATCH 08/69] Corrected typo in permissions docs. (#6540) --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 6a1297e60..901f810c5 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -51,7 +51,7 @@ For example: --- **Note**: With the exception of `DjangoObjectPermissions`, the provided -permission classes in `rest_framework.permssions` **do not** implement the +permission classes in `rest_framework.permissions` **do not** implement the methods necessary to check object permissions. If you wish to use the provided permission classes in order to check object From 13b9b0fb98b58c867fa3ef625d75b6835b9c3b70 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Fri, 29 Mar 2019 02:19:06 +0600 Subject: [PATCH 09/69] Upgraded to Django 2.2rc1 on Tox (#6544) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4226f1a92..776af3b6e 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ deps = django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 - django22: Django>=2.2b1,<3.0 + django22: Django>=2.2rc1,<3.0 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From f34a0a4e6a4de5d58c7abd365bd2343c228eb915 Mon Sep 17 00:00:00 2001 From: Matt Hegarty Date: Fri, 29 Mar 2019 06:32:25 +0000 Subject: [PATCH 10/69] Minor documentation fixes (#6543) --- docs/api-guide/schemas.md | 2 +- docs/tutorial/1-serialization.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 3d07ed621..b09b1606e 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -20,7 +20,7 @@ can render the schema into the commonly used YAML-based OpenAPI format. ## Quickstart -There are two different ways you can serve a schema description for you API. +There are two different ways you can serve a schema description for your API. ### Generating a schema with the `generateschema` management command diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 07ee8f208..224ebf25b 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -8,7 +8,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o --- -**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox]. +**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox]. --- @@ -218,7 +218,6 @@ Edit the `snippets/views.py` file, and add the following. from django.http import HttpResponse, JsonResponse from django.views.decorators.csrf import csrf_exempt - from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser from snippets.models import Snippet from snippets.serializers import SnippetSerializer From b1122a441aeab5664b43351378b63e5dd87ab7a5 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 1 Apr 2019 16:30:26 +0200 Subject: [PATCH 11/69] Update tox to use Django 2.2 final. (#6556) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 776af3b6e..5d7a4987e 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ deps = django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 - django22: Django>=2.2rc1,<3.0 + django22: Django>=2.2,<3.0 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From cceb416098362a582132c1f9622e85d8775b894e Mon Sep 17 00:00:00 2001 From: jozo Date: Thu, 4 Apr 2019 11:31:08 +0200 Subject: [PATCH 12/69] Link DRF Condition (cache headers) third party package. (#6557) --- docs/community/third-party-packages.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index 0d36b8ee0..ace54f6f7 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -263,6 +263,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [django-rest-messaging][django-rest-messaging], [django-rest-messaging-centrifugo][django-rest-messaging-centrifugo] and [django-rest-messaging-js][django-rest-messaging-js] - A real-time pluggable messaging service using DRM. * [djangorest-alchemy][djangorest-alchemy] - SQLAlchemy support for REST framework. * [djangorestframework-datatables][djangorestframework-datatables] - Seamless integration between Django REST framework and [Datatables](https://datatables.net). +* [django-rest-framework-condition][django-rest-framework-condition] - Decorators for managing HTTP cache headers for Django REST framework (ETag and Last-modified). [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html [cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework @@ -336,3 +337,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless [djangorest-alchemy]: https://github.com/dealertrack/djangorest-alchemy [djangorestframework-datatables]: https://github.com/izimobil/django-rest-framework-datatables +[django-rest-framework-condition]: https://github.com/jozo/django-rest-framework-condition From 29cbe574a384c3bcc09434a3a9c5ff0cb7576b99 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sat, 6 Apr 2019 03:27:07 +0600 Subject: [PATCH 13/69] Fix DeprecationWarning in tests (#6551) --- tests/test_filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 088d25436..a53fa192a 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -172,11 +172,11 @@ class SearchFilterTests(TestCase): search_fields = ('$title', '$text') view = SearchListView.as_view() - request = factory.get('/', {'search': '^\w{3}$'}) + request = factory.get('/', {'search': r'^\w{3}$'}) response = view(request) assert len(response.data) == 10 - request = factory.get('/', {'search': '^\w{3}$', 'title_only': 'true'}) + request = factory.get('/', {'search': r'^\w{3}$', 'title_only': 'true'}) response = view(request) assert response.data == [ {'id': 3, 'title': 'zzz', 'text': 'cde'} From f8c4e5079ee580c354d75523882be184961c05ac Mon Sep 17 00:00:00 2001 From: Billy Rotich Date: Sat, 13 Apr 2019 15:02:19 +0200 Subject: [PATCH 14/69] Minor documentation fixes (#6581) --- docs/api-guide/serializers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index e25053936..e77e78c15 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -572,6 +572,8 @@ This option is a dictionary, mapping field names to a dictionary of keyword argu user.save() return user +Please keep in mind that, if the field has already been explicitly declared on the serializer class, then the `extra_kwargs` option will be ignored. + ## Relational fields When serializing model instances, there are a number of different ways you might choose to represent relationships. The default representation for `ModelSerializer` is to use the primary keys of the related instances. From 1ac0f63aa9a6ceed5e4221926929117528af2714 Mon Sep 17 00:00:00 2001 From: Dmitry Alimov Date: Sun, 21 Apr 2019 19:27:13 +0300 Subject: [PATCH 15/69] Fix private attributes ignore in documentation (#6601) --- docs/api-guide/serializers.md | 2 +- docs/community/3.0-announcement.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index e77e78c15..8c17adbaf 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -965,7 +965,7 @@ The following class is an example of a generic serializer that can handle coerci def to_representation(self, obj): for attribute_name in dir(obj): attribute = getattr(obj, attribute_name) - if attribute_name('_'): + if attribute_name.startswith('_'): # Ignore private attributes. pass elif hasattr(attribute, '__call__'): diff --git a/docs/community/3.0-announcement.md b/docs/community/3.0-announcement.md index dc118d70c..7a29b5554 100644 --- a/docs/community/3.0-announcement.md +++ b/docs/community/3.0-announcement.md @@ -523,7 +523,7 @@ The following class is an example of a generic serializer that can handle coerci def to_representation(self, obj): for attribute_name in dir(obj): attribute = getattr(obj, attribute_name) - if attribute_name('_'): + if attribute_name.startswith('_'): # Ignore private attributes. pass elif hasattr(attribute, '__call__'): From 95e28b2252b58e7d5d2e33ee5cb705029eb322c5 Mon Sep 17 00:00:00 2001 From: David Sanders Date: Sat, 27 Apr 2019 12:07:49 -0700 Subject: [PATCH 16/69] Fix typo in docs --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 8c17adbaf..feb5651f7 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -626,7 +626,7 @@ The default implementation returns a serializer class based on the `serializer_f Called to generate a serializer field that maps to a relational model field. -The default implementation returns a serializer class based on the `serializer_relational_field` attribute. +The default implementation returns a serializer class based on the `serializer_related_field` attribute. The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties. From bf9859de51b8856014848b3c5eac45ade67b34dd Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 29 Apr 2019 16:08:39 +0200 Subject: [PATCH 17/69] Adjust django-guardian check for PY2 compatible version. (#6613) --- rest_framework/compat.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 9422e6ad5..d61ca5dbb 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -168,7 +168,12 @@ def is_guardian_installed(): """ django-guardian is optional and only imported if in INSTALLED_APPS. """ - if six.PY2: + try: + import guardian + except ImportError: + guardian = None + + if six.PY2 and (not guardian or guardian.VERSION >= (1, 5)): # Guardian 1.5.0, for Django 2.2 is NOT compatible with Python 2.7. # Remove when dropping PY2. return False From 83d09c7bc53c6c0b79e400fe05e1aff6788c2b2d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 29 Apr 2019 16:30:44 +0200 Subject: [PATCH 18/69] Update version and release notes for v3.9.3. --- docs/community/release-notes.md | 13 +++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 0f08342f5..b61c5fb95 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -42,6 +42,16 @@ You can determine your currently installed version using `pip show`: ### 3.9.2 +**Date**: [29th April 2019] + +This is the last Django REST Framework release that will support Python 2. +Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10. + +* Adjusted the compat check for django-guardian to allow the last guardian + version (v1.4.9) compatible with Python 2. [#6613][gh6613] + +### 3.9.2 + **Date**: [3rd March 2019][3.9.1-milestone] * Routers: invalidate `_urls` cache on `register()` [#6407][gh6407] @@ -2106,3 +2116,6 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh6340]: https://github.com/encode/django-rest-framework/issues/6340 [gh6416]: https://github.com/encode/django-rest-framework/issues/6416 [gh6407]: https://github.com/encode/django-rest-framework/issues/6407 + + +[gh6613]: https://github.com/encode/django-rest-framework/issues/6613 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 55c06982d..53dc7bd47 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.9.2' +__version__ = '3.9.3' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' From 7f16ed772720509b476e3cc4208c01b3972fa99b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 29 Apr 2019 16:33:07 +0200 Subject: [PATCH 19/69] Correct version number in release notes. --- docs/community/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index b61c5fb95..6fcb5bb6b 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -40,7 +40,7 @@ You can determine your currently installed version using `pip show`: ## 3.9.x series -### 3.9.2 +### 3.9.3 **Date**: [29th April 2019] From 1a0a8dde00187994bd4206e9d74219f148afc1c6 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 30 Apr 2019 01:44:01 -0700 Subject: [PATCH 20/69] Correct misspelled module 'typing' (#6616) https://docs.python.org/3/library/typing.html --- tests/test_fields.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 12c936b22..42adedfed 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -19,9 +19,9 @@ from rest_framework.compat import ProhibitNullCharactersValidator from rest_framework.fields import DjangoImageField, is_simple_callable try: - import typings + import typing except ImportError: - typings = False + typing = False # Tests for helper functions. @@ -93,11 +93,12 @@ class TestIsSimpleCallable: assert is_simple_callable(ChoiceModel().get_choice_field_display) - @unittest.skipUnless(typings, 'requires python 3.5') + @unittest.skipUnless(typing, 'requires python 3.5') def test_type_annotation(self): # The annotation will otherwise raise a syntax error in python < 3.5 - exec("def valid(param: str='value'): pass", locals()) - valid = locals()['valid'] + locals = {} + exec("def valid(param: str='value'): pass", locals) + valid = locals['valid'] assert is_simple_callable(valid) From 908236a5767430ca293c71f4c2cc95a8347b1edb Mon Sep 17 00:00:00 2001 From: Jithesh Eriyakkadan Janardhanan Date: Tue, 30 Apr 2019 18:01:17 +0530 Subject: [PATCH 21/69] Correct misspelled class name --- docs/api-guide/validators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index 3b50442cc..ab042ac03 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -100,7 +100,7 @@ The validator should be applied to *serializer classes*, like so: --- -**Note**: The `UniqueTogetherValidation` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input. +**Note**: The `UniqueTogetherValidator` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input. --- From 0407a0df8a16fdac94bbd08d49143a74a88001cd Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 30 Apr 2019 17:53:44 +0200 Subject: [PATCH 22/69] Dropped Python 2 compatibility. (#6615) Thanks to Jon Dufresne (@jdufresne) for review. Co-authored-by: Asif Saif Uddin Co-authored-by: Rizwan Mansuri --- .travis.yml | 5 +- README.md | 2 +- docs/api-guide/fields.md | 4 +- rest_framework/authentication.py | 7 +- .../management/commands/drf_create_token.py | 4 +- .../authtoken/migrations/0001_initial.py | 3 - .../migrations/0002_auto_20160226_1747.py | 3 - rest_framework/authtoken/models.py | 4 +- rest_framework/compat.py | 58 +---- rest_framework/decorators.py | 5 +- rest_framework/exceptions.py | 26 +- rest_framework/fields.py | 226 ++++++++---------- rest_framework/filters.py | 11 +- rest_framework/generics.py | 2 - rest_framework/metadata.py | 4 +- rest_framework/mixins.py | 12 +- rest_framework/negotiation.py | 6 +- rest_framework/pagination.py | 20 +- rest_framework/parsers.py | 15 +- rest_framework/permissions.py | 6 +- rest_framework/relations.py | 51 ++-- rest_framework/renderers.py | 17 +- rest_framework/request.py | 13 +- rest_framework/response.py | 14 +- rest_framework/reverse.py | 5 +- rest_framework/routers.py | 15 +- rest_framework/schemas/generators.py | 9 +- rest_framework/schemas/inspectors.py | 15 +- rest_framework/schemas/views.py | 4 +- rest_framework/serializers.py | 33 ++- rest_framework/settings.py | 7 +- rest_framework/status.py | 1 - rest_framework/templatetags/rest_framework.py | 11 +- rest_framework/test.py | 54 ++--- rest_framework/throttling.py | 6 +- rest_framework/urlpatterns.py | 2 - rest_framework/urls.py | 2 - rest_framework/utils/breadcrumbs.py | 2 - rest_framework/utils/encoders.py | 10 +- rest_framework/utils/field_mapping.py | 2 +- rest_framework/utils/formatting.py | 2 - rest_framework/utils/json.py | 3 - rest_framework/utils/mediatypes.py | 6 +- rest_framework/utils/representation.py | 6 +- rest_framework/utils/serializer_helpers.py | 18 +- rest_framework/utils/urls.py | 19 +- rest_framework/validators.py | 21 +- rest_framework/versioning.py | 11 +- rest_framework/views.py | 4 +- rest_framework/viewsets.py | 6 +- runtests.py | 4 +- setup.cfg | 3 - setup.py | 36 ++- .../authentication/migrations/0001_initial.py | 3 - tests/authentication/models.py | 3 - tests/authentication/test_authentication.py | 11 +- tests/browsable_api/auth_urls.py | 2 - tests/browsable_api/no_auth_urls.py | 2 - tests/browsable_api/test_browsable_api.py | 2 - .../test_browsable_nested_api.py | 2 - tests/browsable_api/views.py | 2 - tests/generic_relations/models.py | 6 - .../test_generic_relations.py | 2 - tests/models.py | 2 - tests/test_api_client.py | 2 - tests/test_atomic_requests.py | 4 +- tests/test_authtoken.py | 3 +- tests/test_bound_fields.py | 4 +- tests/test_decorators.py | 2 - tests/test_description.py | 9 +- tests/test_encoders.py | 2 +- tests/test_exceptions.py | 11 +- tests/test_fields.py | 19 +- tests/test_filters.py | 6 +- tests/test_generateschema.py | 6 +- tests/test_generics.py | 13 +- tests/test_htmlrenderer.py | 20 +- tests/test_metadata.py | 2 - tests/test_middleware.py | 4 +- tests/test_model_serializer.py | 69 ++---- tests/test_multitable_inheritance.py | 2 - tests/test_negotiation.py | 4 +- tests/test_one_to_one_with_inheritance.py | 2 - tests/test_pagination.py | 26 +- tests/test_parsers.py | 8 +- tests/test_permissions.py | 9 +- tests/test_relations.py | 2 +- tests/test_relations_hyperlink.py | 2 - tests/test_relations_pk.py | 7 +- tests/test_renderers.py | 10 +- tests/test_request.py | 13 +- tests/test_requests_client.py | 2 - tests/test_response.py | 9 +- tests/test_reverse.py | 4 +- tests/test_routers.py | 2 - tests/test_schemas.py | 6 +- tests/test_serializer.py | 26 +- tests/test_serializer_bulk_update.py | 6 +- tests/test_settings.py | 2 - tests/test_status.py | 2 - tests/test_templatetags.py | 21 +- tests/test_testing.py | 7 +- tests/test_throttling.py | 3 +- tests/test_urlpatterns.py | 2 - tests/test_utils.py | 3 - tests/test_validation.py | 9 +- tests/test_validators.py | 8 +- tests/test_versioning.py | 4 +- tests/test_views.py | 8 +- tests/utils.py | 6 +- tox.ini | 5 +- 111 files changed, 473 insertions(+), 795 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9543cb452..04a5ff99e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ dist: xenial matrix: fast_finish: true include: - - { python: "2.7", env: DJANGO=1.11 } - { python: "3.4", env: DJANGO=1.11 } - { python: "3.4", env: DJANGO=2.0 } @@ -26,8 +25,8 @@ matrix: - { python: "3.7", env: DJANGO=master } - { python: "3.7", env: TOXENV=base } - - { python: "2.7", env: TOXENV=lint } - - { python: "2.7", env: TOXENV=docs } + - { python: "3.7", env: TOXENV=lint } + - { python: "3.7", env: TOXENV=docs } - python: "3.7" env: TOXENV=dist diff --git a/README.md b/README.md index 66079edf0..7d0bdd2ad 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements -* Python (2.7, 3.4, 3.5, 3.6, 3.7) +* Python (3.4, 3.5, 3.6, 3.7) * Django (1.11, 2.0, 2.1, 2.2) We **highly recommend** and only officially support the latest patch release of diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index ede4f15ad..d371bb8fd 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -629,7 +629,7 @@ Our `ColorField` class above currently does not perform any data validation. To indicate invalid data, we should raise a `serializers.ValidationError`, like so: def to_internal_value(self, data): - if not isinstance(data, six.text_type): + if not isinstance(data, str): msg = 'Incorrect type. Expected a string, but got %s' raise ValidationError(msg % type(data).__name__) @@ -653,7 +653,7 @@ The `.fail()` method is a shortcut for raising `ValidationError` that takes a me } def to_internal_value(self, data): - if not isinstance(data, six.text_type): + if not isinstance(data, str): self.fail('incorrect_type', input_type=type(data).__name__) if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data): diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 25150d525..0612563e4 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -1,14 +1,11 @@ """ Provides various authentication policies. """ -from __future__ import unicode_literals - import base64 import binascii from django.contrib.auth import authenticate, get_user_model from django.middleware.csrf import CsrfViewMiddleware -from django.utils.six import text_type from django.utils.translation import ugettext_lazy as _ from rest_framework import HTTP_HEADER_ENCODING, exceptions @@ -21,7 +18,7 @@ def get_authorization_header(request): Hide some test client ickyness where the header can be unicode. """ auth = request.META.get('HTTP_AUTHORIZATION', b'') - if isinstance(auth, text_type): + if isinstance(auth, str): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) return auth @@ -33,7 +30,7 @@ class CSRFCheck(CsrfViewMiddleware): return reason -class BaseAuthentication(object): +class BaseAuthentication: """ All authentication classes should extend BaseAuthentication. """ diff --git a/rest_framework/authtoken/management/commands/drf_create_token.py b/rest_framework/authtoken/management/commands/drf_create_token.py index 8e06812db..3d6539244 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/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py index 75780fedf..6a46ccfff 100644 --- a/rest_framework/authtoken/migrations/0001_initial.py +++ b/rest_framework/authtoken/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models diff --git a/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py b/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py index 9f7e58e22..43119099a 100644 --- a/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py +++ b/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 7e96eff93..0ed02c415 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -3,11 +3,9 @@ import os from django.conf import settings from django.db import models -from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -@python_2_unicode_compatible class Token(models.Model): """ The default authorization token model. @@ -32,7 +30,7 @@ class Token(models.Model): def save(self, *args, **kwargs): if not self.key: self.key = self.generate_key() - return super(Token, self).save(*args, **kwargs) + return super().save(*args, **kwargs) def generate_key(self): return binascii.hexlify(os.urandom(20)).decode() diff --git a/rest_framework/compat.py b/rest_framework/compat.py index d61ca5dbb..aad44e342 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -2,23 +2,13 @@ The `compat` module provides support for backwards compatibility with older versions of Django/Python, and compatibility wrappers around optional packages. """ - -from __future__ import unicode_literals - import sys +from collections.abc import Mapping, MutableMapping # noqa from django.conf import settings from django.core import validators -from django.utils import six from django.views.generic import View -try: - # Python 3 - from collections.abc import Mapping, MutableMapping # noqa -except ImportError: - # Python 2.7 - from collections import Mapping, MutableMapping # noqa - try: from django.urls import ( # noqa URLPattern, @@ -36,11 +26,6 @@ try: except ImportError: ProhibitNullCharactersValidator = None -try: - from unittest import mock -except ImportError: - mock = None - def get_original_route(urlpattern): """ @@ -89,23 +74,6 @@ def make_url_resolver(regex, urlpatterns): return URLResolver(regex, urlpatterns) -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). - if six.PY2: - return repr(instance).decode('utf-8') - 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. - if six.PY2: - return value.encode('utf-8') - return value - - def unicode_http_header(value): # Coerce HTTP header value to unicode. if isinstance(value, bytes): @@ -168,15 +136,6 @@ def is_guardian_installed(): """ django-guardian is optional and only imported if in INSTALLED_APPS. """ - try: - import guardian - except ImportError: - guardian = None - - if six.PY2 and (not guardian or guardian.VERSION >= (1, 5)): - # Guardian 1.5.0, for Django 2.2 is NOT compatible with Python 2.7. - # Remove when dropping PY2. - return False return 'guardian' in settings.INSTALLED_APPS @@ -289,17 +248,12 @@ except ImportError: # `separators` argument to `json.dumps()` differs between 2.x and 3.x # See: https://bugs.python.org/issue22767 -if six.PY3: - SHORT_SEPARATORS = (',', ':') - LONG_SEPARATORS = (', ', ': ') - INDENT_SEPARATORS = (',', ': ') -else: - SHORT_SEPARATORS = (b',', b':') - LONG_SEPARATORS = (b', ', b': ') - INDENT_SEPARATORS = (b',', b': ') +SHORT_SEPARATORS = (',', ':') +LONG_SEPARATORS = (', ', ': ') +INDENT_SEPARATORS = (',', ': ') -class CustomValidatorMessage(object): +class CustomValidatorMessage: """ We need to avoid evaluation of `lazy` translated `message` in `django.core.validators.BaseValidator.__init__`. https://github.com/django/django/blob/75ed5900321d170debef4ac452b8b3cf8a1c2384/django/core/validators.py#L297 @@ -309,7 +263,7 @@ class CustomValidatorMessage(object): def __init__(self, *args, **kwargs): self.message = kwargs.pop('message', self.message) - super(CustomValidatorMessage, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class MinValueValidator(CustomValidatorMessage, validators.MinValueValidator): diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 30bfcc4e5..5d7bd14a3 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -6,13 +6,10 @@ There are also various decorators for setting the API policies on function based views, as well as the `@detail_route` and `@list_route` decorators, which are used to annotate methods on viewsets that should be included by routers. """ -from __future__ import unicode_literals - import types import warnings from django.forms.utils import pretty_name -from django.utils import six from rest_framework import RemovedInDRF310Warning from rest_framework.views import APIView @@ -28,7 +25,7 @@ def api_view(http_method_names=None): def decorator(func): WrappedAPIView = type( - six.PY3 and 'WrappedAPIView' or b'WrappedAPIView', + 'WrappedAPIView', (APIView,), {'__doc__': func.__doc__} ) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index f79b16129..8fbdfcd08 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -4,18 +4,14 @@ Handled exceptions raised by REST framework. In addition Django's built in 403 and 404 exceptions are handled. (`django.http.Http404` and `django.core.exceptions.PermissionDenied`) """ -from __future__ import unicode_literals - import math from django.http import JsonResponse -from django.utils import six from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext from rest_framework import status -from rest_framework.compat import unicode_to_repr from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList @@ -64,19 +60,19 @@ def _get_full_details(detail): } -class ErrorDetail(six.text_type): +class ErrorDetail(str): """ A string-like object that can additionally have a code. """ 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 def __eq__(self, other): - r = super(ErrorDetail, self).__eq__(other) + r = super().__eq__(other) try: return r and self.code == other.code except AttributeError: @@ -86,10 +82,10 @@ class ErrorDetail(six.text_type): return not self.__eq__(other) def __repr__(self): - return unicode_to_repr('ErrorDetail(string=%r, code=%r)' % ( - six.text_type(self), + return 'ErrorDetail(string=%r, code=%r)' % ( + str(self), self.code, - )) + ) def __hash__(self): return hash(str(self)) @@ -113,7 +109,7 @@ class APIException(Exception): self.detail = _get_error_details(detail, code) def __str__(self): - return six.text_type(self.detail) + return str(self.detail) def get_codes(self): """ @@ -196,7 +192,7 @@ class MethodNotAllowed(APIException): def __init__(self, method, detail=None, code=None): if detail is None: detail = force_text(self.default_detail).format(method=method) - super(MethodNotAllowed, self).__init__(detail, code) + super().__init__(detail, code) class NotAcceptable(APIException): @@ -206,7 +202,7 @@ class NotAcceptable(APIException): def __init__(self, detail=None, code=None, available_renderers=None): self.available_renderers = available_renderers - super(NotAcceptable, self).__init__(detail, code) + super().__init__(detail, code) class UnsupportedMediaType(APIException): @@ -217,7 +213,7 @@ class UnsupportedMediaType(APIException): def __init__(self, media_type, detail=None, code=None): if detail is None: detail = force_text(self.default_detail).format(media_type=media_type) - super(UnsupportedMediaType, self).__init__(detail, code) + super().__init__(detail, code) class Throttled(APIException): @@ -238,7 +234,7 @@ class Throttled(APIException): self.extra_detail_plural.format(wait=wait), wait)))) self.wait = wait - super(Throttled, self).__init__(detail, code) + super().__init__(detail, code) def server_error(request, *args, **kwargs): diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c8f65db0e..ad9611e05 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import copy import datetime import decimal @@ -17,7 +15,7 @@ from django.core.validators import ( ) from django.forms import FilePathField as DjangoFilePathField from django.forms import ImageField as DjangoImageField -from django.utils import six, timezone +from django.utils import timezone from django.utils.dateparse import ( parse_date, parse_datetime, parse_duration, parse_time ) @@ -33,8 +31,7 @@ from pytz.exceptions import InvalidTimeError from rest_framework import ISO_8601 from rest_framework.compat import ( Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator, - MinValueValidator, ProhibitNullCharactersValidator, unicode_repr, - unicode_to_repr + MinValueValidator, ProhibitNullCharactersValidator ) from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.settings import api_settings @@ -51,39 +48,21 @@ class empty: pass -if six.PY3: - def is_simple_callable(obj): - """ - True if the object is a callable that takes no arguments. - """ - if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)): - return False +def is_simple_callable(obj): + """ + True if the object is a callable that takes no arguments. + """ + if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)): + return False - sig = inspect.signature(obj) - params = sig.parameters.values() - return all( - param.kind == param.VAR_POSITIONAL or - param.kind == param.VAR_KEYWORD or - param.default != param.empty - for param in params - ) - -else: - def is_simple_callable(obj): - function = inspect.isfunction(obj) - method = inspect.ismethod(obj) - - if not (function or method): - return False - - if method: - is_unbound = obj.im_self is None - - args, _, _, defaults = inspect.getargspec(obj) - - len_args = len(args) if function or is_unbound else len(args) - 1 - len_defaults = len(defaults) if defaults else 0 - return len_args <= len_defaults + sig = inspect.signature(obj) + params = sig.parameters.values() + return all( + param.kind == param.VAR_POSITIONAL or + param.kind == param.VAR_KEYWORD or + param.default != param.empty + for param in params + ) def get_attribute(instance, attrs): @@ -108,7 +87,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 @@ -185,18 +164,18 @@ def iter_options(grouped_choices, cutoff=None, cutoff_text=None): """ Helper function for options and option groups in templates. """ - class StartOptionGroup(object): + class StartOptionGroup: start_option_group = True end_option_group = False def __init__(self, label): self.label = label - class EndOptionGroup(object): + class EndOptionGroup: start_option_group = False end_option_group = True - class Option(object): + class Option: start_option_group = False end_option_group = False @@ -251,7 +230,7 @@ def get_error_detail(exc_info): } -class CreateOnlyDefault(object): +class CreateOnlyDefault: """ This class may be used to provide default values that are only used for create operations, but that do not return any value for update @@ -273,12 +252,10 @@ class CreateOnlyDefault(object): return self.default def __repr__(self): - return unicode_to_repr( - '%s(%s)' % (self.__class__.__name__, unicode_repr(self.default)) - ) + return '%s(%s)' % (self.__class__.__name__, repr(self.default)) -class CurrentUserDefault(object): +class CurrentUserDefault: def set_context(self, serializer_field): self.user = serializer_field.context['request'].user @@ -286,7 +263,7 @@ class CurrentUserDefault(object): return self.user def __repr__(self): - return unicode_to_repr('%s()' % self.__class__.__name__) + return '%s()' % self.__class__.__name__ class SkipField(Exception): @@ -305,7 +282,7 @@ MISSING_ERROR_MESSAGE = ( ) -class Field(object): +class Field: _creation_counter = 0 default_error_messages = { @@ -618,7 +595,7 @@ class Field(object): 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 @@ -647,7 +624,7 @@ class Field(object): This allows us to create descriptive representations for serializer instances that show all the declared fields on the serializer. """ - return unicode_to_repr(representation.field_repr(self)) + return representation.field_repr(self) # Boolean types... @@ -724,7 +701,7 @@ class NullBooleanField(Field): def __init__(self, **kwargs): assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.' kwargs['allow_null'] = True - super(NullBooleanField, self).__init__(**kwargs) + super().__init__(**kwargs) def to_internal_value(self, data): try: @@ -764,17 +741,14 @@ class CharField(Field): self.trim_whitespace = kwargs.pop('trim_whitespace', True) self.max_length = kwargs.pop('max_length', None) self.min_length = kwargs.pop('min_length', None) - super(CharField, self).__init__(**kwargs) + super().__init__(**kwargs) if self.max_length is not None: - message = lazy( - self.error_messages['max_length'].format, - six.text_type)(max_length=self.max_length) + message = lazy(self.error_messages['max_length'].format, str)(max_length=self.max_length) self.validators.append( MaxLengthValidator(self.max_length, message=message)) if self.min_length is not None: message = lazy( - self.error_messages['min_length'].format, - six.text_type)(min_length=self.min_length) + self.error_messages['min_length'].format, str)(min_length=self.min_length) self.validators.append( MinLengthValidator(self.min_length, message=message)) @@ -786,23 +760,23 @@ class CharField(Field): # Test for the empty string here so that it does not get validated, # and so that subclasses do not need to handle it explicitly # inside the `to_internal_value()` method. - if data == '' or (self.trim_whitespace and six.text_type(data).strip() == ''): + if data == '' or (self.trim_whitespace and str(data).strip() == ''): if not self.allow_blank: self.fail('blank') return '' - return super(CharField, self).run_validation(data) + return super().run_validation(data) def to_internal_value(self, data): # We're lenient with allowing basic numerics to be coerced into strings, # but other types should fail. Eg. unclear if booleans should represent as `true` or `True`, # and composites such as lists are likely user error. - if isinstance(data, bool) or not isinstance(data, six.string_types + six.integer_types + (float,)): + if isinstance(data, bool) or not isinstance(data, (str, int, float,)): self.fail('invalid') - value = six.text_type(data) + value = str(data) return value.strip() if self.trim_whitespace else value def to_representation(self, value): - return six.text_type(value) + return str(value) class EmailField(CharField): @@ -811,7 +785,7 @@ class EmailField(CharField): } def __init__(self, **kwargs): - super(EmailField, self).__init__(**kwargs) + super().__init__(**kwargs) validator = EmailValidator(message=self.error_messages['invalid']) self.validators.append(validator) @@ -822,7 +796,7 @@ class RegexField(CharField): } def __init__(self, regex, **kwargs): - super(RegexField, self).__init__(**kwargs) + super().__init__(**kwargs) validator = RegexValidator(regex, message=self.error_messages['invalid']) self.validators.append(validator) @@ -834,7 +808,7 @@ class SlugField(CharField): } def __init__(self, allow_unicode=False, **kwargs): - super(SlugField, self).__init__(**kwargs) + super().__init__(**kwargs) self.allow_unicode = allow_unicode if self.allow_unicode: validator = RegexValidator(re.compile(r'^[-\w]+\Z', re.UNICODE), message=self.error_messages['invalid_unicode']) @@ -849,7 +823,7 @@ class URLField(CharField): } def __init__(self, **kwargs): - super(URLField, self).__init__(**kwargs) + super().__init__(**kwargs) validator = URLValidator(message=self.error_messages['invalid']) self.validators.append(validator) @@ -866,16 +840,16 @@ 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(UUIDField, self).__init__(**kwargs) + super().__init__(**kwargs) def to_internal_value(self, data): if not isinstance(data, uuid.UUID): try: - if isinstance(data, six.integer_types): + if isinstance(data, int): return uuid.UUID(int=data) - elif isinstance(data, six.string_types): + elif isinstance(data, str): return uuid.UUID(hex=data) else: self.fail('invalid', value=data) @@ -900,12 +874,12 @@ class IPAddressField(CharField): def __init__(self, protocol='both', **kwargs): self.protocol = protocol.lower() self.unpack_ipv4 = (self.protocol == 'both') - super(IPAddressField, self).__init__(**kwargs) + super().__init__(**kwargs) validators, error_message = ip_address_validators(protocol, self.unpack_ipv4) self.validators.extend(validators) def to_internal_value(self, data): - if not isinstance(data, six.string_types): + if not isinstance(data, str): self.fail('invalid', value=data) if ':' in data: @@ -915,7 +889,7 @@ class IPAddressField(CharField): except DjangoValidationError: self.fail('invalid', value=data) - return super(IPAddressField, self).to_internal_value(data) + return super().to_internal_value(data) # Number types... @@ -933,22 +907,20 @@ class IntegerField(Field): def __init__(self, **kwargs): self.max_value = kwargs.pop('max_value', None) self.min_value = kwargs.pop('min_value', None) - super(IntegerField, self).__init__(**kwargs) + super().__init__(**kwargs) if self.max_value is not None: message = lazy( - self.error_messages['max_value'].format, - six.text_type)(max_value=self.max_value) + self.error_messages['max_value'].format, str)(max_value=self.max_value) self.validators.append( MaxValueValidator(self.max_value, message=message)) if self.min_value is not None: message = lazy( - self.error_messages['min_value'].format, - six.text_type)(min_value=self.min_value) + self.error_messages['min_value'].format, str)(min_value=self.min_value) self.validators.append( MinValueValidator(self.min_value, message=message)) def to_internal_value(self, data): - if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: + if isinstance(data, str) and len(data) > self.MAX_STRING_LENGTH: self.fail('max_string_length') try: @@ -973,23 +945,23 @@ class FloatField(Field): def __init__(self, **kwargs): self.max_value = kwargs.pop('max_value', None) self.min_value = kwargs.pop('min_value', None) - super(FloatField, self).__init__(**kwargs) + super().__init__(**kwargs) if self.max_value is not None: message = lazy( self.error_messages['max_value'].format, - six.text_type)(max_value=self.max_value) + str)(max_value=self.max_value) self.validators.append( MaxValueValidator(self.max_value, message=message)) if self.min_value is not None: message = lazy( self.error_messages['min_value'].format, - six.text_type)(min_value=self.min_value) + str)(min_value=self.min_value) self.validators.append( MinValueValidator(self.min_value, message=message)) def to_internal_value(self, data): - if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: + if isinstance(data, str) and len(data) > self.MAX_STRING_LENGTH: self.fail('max_string_length') try: @@ -1031,18 +1003,17 @@ class DecimalField(Field): else: self.max_whole_digits = None - super(DecimalField, self).__init__(**kwargs) + super().__init__(**kwargs) if self.max_value is not None: message = lazy( self.error_messages['max_value'].format, - six.text_type)(max_value=self.max_value) + str)(max_value=self.max_value) self.validators.append( MaxValueValidator(self.max_value, message=message)) if self.min_value is not None: message = lazy( - self.error_messages['min_value'].format, - six.text_type)(min_value=self.min_value) + self.error_messages['min_value'].format, str)(min_value=self.min_value) self.validators.append( MinValueValidator(self.min_value, message=message)) @@ -1121,7 +1092,7 @@ class DecimalField(Field): coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING) if not isinstance(value, decimal.Decimal): - value = decimal.Decimal(six.text_type(value).strip()) + value = decimal.Decimal(str(value).strip()) quantized = self.quantize(value) @@ -1130,7 +1101,7 @@ class DecimalField(Field): if self.localize: return localize_input(quantized) - return '{0:f}'.format(quantized) + return '{:f}'.format(quantized) def quantize(self, value): """ @@ -1167,7 +1138,7 @@ class DateTimeField(Field): self.input_formats = input_formats if default_timezone is not None: self.timezone = default_timezone - super(DateTimeField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def enforce_timezone(self, value): """ @@ -1226,7 +1197,7 @@ class DateTimeField(Field): output_format = getattr(self, 'format', api_settings.DATETIME_FORMAT) - if output_format is None or isinstance(value, six.string_types): + if output_format is None or isinstance(value, str): return value value = self.enforce_timezone(value) @@ -1251,7 +1222,7 @@ class DateField(Field): self.format = format if input_formats is not None: self.input_formats = input_formats - super(DateField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def to_internal_value(self, value): input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS) @@ -1288,7 +1259,7 @@ class DateField(Field): output_format = getattr(self, 'format', api_settings.DATE_FORMAT) - if output_format is None or isinstance(value, six.string_types): + if output_format is None or isinstance(value, str): return value # Applying a `DateField` to a datetime value is almost always @@ -1317,7 +1288,7 @@ class TimeField(Field): self.format = format if input_formats is not None: self.input_formats = input_formats - super(TimeField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def to_internal_value(self, value): input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS) @@ -1351,7 +1322,7 @@ class TimeField(Field): output_format = getattr(self, 'format', api_settings.TIME_FORMAT) - if output_format is None or isinstance(value, six.string_types): + if output_format is None or isinstance(value, str): return value # Applying a `TimeField` to a datetime value is almost always @@ -1378,24 +1349,24 @@ class DurationField(Field): def __init__(self, **kwargs): self.max_value = kwargs.pop('max_value', None) self.min_value = kwargs.pop('min_value', None) - super(DurationField, self).__init__(**kwargs) + super().__init__(**kwargs) if self.max_value is not None: message = lazy( self.error_messages['max_value'].format, - six.text_type)(max_value=self.max_value) + str)(max_value=self.max_value) self.validators.append( MaxValueValidator(self.max_value, message=message)) if self.min_value is not None: message = lazy( self.error_messages['min_value'].format, - six.text_type)(min_value=self.min_value) + str)(min_value=self.min_value) self.validators.append( MinValueValidator(self.min_value, message=message)) def to_internal_value(self, value): if isinstance(value, datetime.timedelta): return value - parsed = parse_duration(six.text_type(value)) + parsed = parse_duration(str(value)) if parsed is not None: return parsed self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]') @@ -1420,21 +1391,21 @@ class ChoiceField(Field): self.allow_blank = kwargs.pop('allow_blank', False) - super(ChoiceField, self).__init__(**kwargs) + super().__init__(**kwargs) def to_internal_value(self, data): if data == '' and self.allow_blank: return '' try: - return self.choice_strings_to_values[six.text_type(data)] + return self.choice_strings_to_values[str(data)] except KeyError: self.fail('invalid_choice', input=data) def to_representation(self, value): if value in ('', None): return value - return self.choice_strings_to_values.get(six.text_type(value), value) + return self.choice_strings_to_values.get(str(value), value) def iter_options(self): """ @@ -1457,7 +1428,7 @@ class ChoiceField(Field): # Allows us to deal with eg. integer choices while supporting either # integer or string input, but still get the correct datatype out. self.choice_strings_to_values = { - six.text_type(key): key for key in self.choices + str(key): key for key in self.choices } choices = property(_get_choices, _set_choices) @@ -1473,7 +1444,7 @@ class MultipleChoiceField(ChoiceField): def __init__(self, *args, **kwargs): self.allow_empty = kwargs.pop('allow_empty', True) - super(MultipleChoiceField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_value(self, dictionary): if self.field_name not in dictionary: @@ -1486,7 +1457,7 @@ class MultipleChoiceField(ChoiceField): return dictionary.get(self.field_name, empty) def to_internal_value(self, data): - if isinstance(data, six.text_type) or not hasattr(data, '__iter__'): + if isinstance(data, str) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') @@ -1498,7 +1469,7 @@ class MultipleChoiceField(ChoiceField): def to_representation(self, value): return { - self.choice_strings_to_values.get(six.text_type(item), item) for item in value + self.choice_strings_to_values.get(str(item), item) for item in value } @@ -1516,7 +1487,7 @@ class FilePathField(ChoiceField): allow_folders=allow_folders, required=required ) kwargs['choices'] = field.choices - super(FilePathField, self).__init__(**kwargs) + super().__init__(**kwargs) # File types... @@ -1535,7 +1506,7 @@ class FileField(Field): self.allow_empty_file = kwargs.pop('allow_empty_file', False) if 'use_url' in kwargs: self.use_url = kwargs.pop('use_url') - super(FileField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def to_internal_value(self, data): try: @@ -1581,13 +1552,13 @@ class ImageField(FileField): def __init__(self, *args, **kwargs): self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField) - super(ImageField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def to_internal_value(self, data): # Image validation is a bit grungy, so we'll just outright # defer to Django's implementation so we don't need to # consider it, or treat PIL as a test dependency. - file_object = super(ImageField, self).to_internal_value(data) + file_object = super().to_internal_value(data) django_field = self._DjangoImageField() django_field.error_messages = self.error_messages return django_field.clean(file_object) @@ -1597,7 +1568,7 @@ class ImageField(FileField): class _UnvalidatedField(Field): def __init__(self, *args, **kwargs): - super(_UnvalidatedField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.allow_blank = True self.allow_null = True @@ -1630,7 +1601,7 @@ class ListField(Field): "Remove `source=` from the field declaration." ) - super(ListField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) if self.max_length is not None: message = self.error_messages['max_length'].format(max_length=self.max_length) @@ -1660,7 +1631,7 @@ class ListField(Field): """ if html.is_html_input(data): data = html.parse_html_list(data, default=[]) - if isinstance(data, (six.text_type, Mapping)) or not hasattr(data, '__iter__'): + if isinstance(data, (str, Mapping)) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') @@ -1703,7 +1674,7 @@ class DictField(Field): "Remove `source=` from the field declaration." ) - super(DictField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) def get_value(self, dictionary): @@ -1725,7 +1696,7 @@ class DictField(Field): def to_representation(self, value): return { - six.text_type(key): self.child.to_representation(val) if val is not None else None + str(key): self.child.to_representation(val) if val is not None else None for key, val in value.items() } @@ -1734,7 +1705,7 @@ class DictField(Field): errors = OrderedDict() for key, value in data.items(): - key = six.text_type(key) + key = str(key) try: result[key] = self.child.run_validation(value) @@ -1750,7 +1721,7 @@ class HStoreField(DictField): child = CharField(allow_blank=True, allow_null=True) def __init__(self, *args, **kwargs): - super(HStoreField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) assert isinstance(self.child, CharField), ( "The `child` argument must be an instance of `CharField`, " "as the hstore extension stores values as strings." @@ -1764,15 +1735,15 @@ class JSONField(Field): def __init__(self, *args, **kwargs): self.binary = kwargs.pop('binary', False) - super(JSONField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_value(self, dictionary): if html.is_html_input(dictionary) and self.field_name in dictionary: # When HTML form input is used, mark up the input # as being a JSON string, rather than a JSON primitive. - class JSONString(six.text_type): + class JSONString(str): def __new__(self, value): - ret = six.text_type.__new__(self, value) + ret = str.__new__(self, value) ret.is_json_string = True return ret return JSONString(dictionary[self.field_name]) @@ -1795,7 +1766,7 @@ class JSONField(Field): value = json.dumps(value) # On python 2.x the return type for json.dumps() is underspecified. # On python 3.x json.dumps() returns unicode strings. - if isinstance(value, six.text_type): + if isinstance(value, str): value = bytes(value.encode('utf-8')) return value @@ -1817,7 +1788,7 @@ class ReadOnlyField(Field): def __init__(self, **kwargs): kwargs['read_only'] = True - super(ReadOnlyField, self).__init__(**kwargs) + super().__init__(**kwargs) def to_representation(self, value): return value @@ -1834,7 +1805,7 @@ class HiddenField(Field): def __init__(self, **kwargs): assert 'default' in kwargs, 'default is a required argument.' kwargs['write_only'] = True - super(HiddenField, self).__init__(**kwargs) + super().__init__(**kwargs) def get_value(self, dictionary): # We always use the default value for `HiddenField`. @@ -1864,7 +1835,7 @@ class SerializerMethodField(Field): self.method_name = method_name kwargs['source'] = '*' kwargs['read_only'] = True - super(SerializerMethodField, self).__init__(**kwargs) + super().__init__(**kwargs) def bind(self, field_name, parent): # In order to enforce a consistent style, we error if a redundant @@ -1882,7 +1853,7 @@ class SerializerMethodField(Field): if self.method_name is None: self.method_name = default_method_name - super(SerializerMethodField, self).bind(field_name, parent) + super().bind(field_name, parent) def to_representation(self, value): method = getattr(self.parent, self.method_name) @@ -1905,11 +1876,10 @@ class ModelField(Field): # The `max_length` option is supported by Django's base `Field` class, # so we'd better support it here. max_length = kwargs.pop('max_length', None) - super(ModelField, self).__init__(**kwargs) + super().__init__(**kwargs) if max_length is not None: message = lazy( - self.error_messages['max_length'].format, - six.text_type)(max_length=self.max_length) + self.error_messages['max_length'].format, str)(max_length=self.max_length) self.validators.append( MaxLengthValidator(self.max_length, message=message)) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index bb1b86586..b77069ddc 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -2,8 +2,6 @@ Provides generic filtering backends that can be used to filter the results returned by list views. """ -from __future__ import unicode_literals - import operator import warnings from functools import reduce @@ -13,7 +11,6 @@ from django.db import models from django.db.models.constants import LOOKUP_SEP from django.db.models.sql.constants import ORDER_PATTERN from django.template import loader -from django.utils import six from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ @@ -24,7 +21,7 @@ from rest_framework.compat import ( from rest_framework.settings import api_settings -class BaseFilterBackend(object): +class BaseFilterBackend: """ A base class from which all filter backend classes should inherit. """ @@ -109,7 +106,7 @@ class SearchFilter(BaseFilterBackend): return queryset orm_lookups = [ - self.construct_search(six.text_type(search_field)) + self.construct_search(str(search_field)) for search_field in search_fields ] @@ -188,7 +185,7 @@ class OrderingFilter(BaseFilterBackend): def get_default_ordering(self, view): ordering = getattr(view, 'ordering', None) - if isinstance(ordering, six.string_types): + if isinstance(ordering, str): return (ordering,) return ordering @@ -237,7 +234,7 @@ class OrderingFilter(BaseFilterBackend): ] else: valid_fields = [ - (item, item) if isinstance(item, six.string_types) else item + (item, item) if isinstance(item, str) else item for item in valid_fields ] diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 8d0bf284a..c39b02ab7 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -1,8 +1,6 @@ """ Generic views that provide commonly needed behaviour. """ -from __future__ import unicode_literals - from django.core.exceptions import ValidationError from django.db.models.query import QuerySet from django.http import Http404 diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py index 9f9324469..42442f91c 100644 --- a/rest_framework/metadata.py +++ b/rest_framework/metadata.py @@ -6,8 +6,6 @@ some fairly ad-hoc information about the view. Future implementations might use JSON schema or other definitions in order to return this information in a more standardized way. """ -from __future__ import unicode_literals - from collections import OrderedDict from django.core.exceptions import PermissionDenied @@ -19,7 +17,7 @@ from rest_framework.request import clone_request from rest_framework.utils.field_mapping import ClassLookupDict -class BaseMetadata(object): +class BaseMetadata: def determine_metadata(self, request, view): """ Return a dictionary of metadata about the view. diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index de10d6930..7fa8947cb 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -4,14 +4,12 @@ Basic building blocks for generic class based views. We don't bind behaviour to http method handlers yet, which allows mixin classes to be composed in interesting ways. """ -from __future__ import unicode_literals - from rest_framework import status from rest_framework.response import Response from rest_framework.settings import api_settings -class CreateModelMixin(object): +class CreateModelMixin: """ Create a model instance. """ @@ -32,7 +30,7 @@ class CreateModelMixin(object): return {} -class ListModelMixin(object): +class ListModelMixin: """ List a queryset. """ @@ -48,7 +46,7 @@ class ListModelMixin(object): return Response(serializer.data) -class RetrieveModelMixin(object): +class RetrieveModelMixin: """ Retrieve a model instance. """ @@ -58,7 +56,7 @@ class RetrieveModelMixin(object): return Response(serializer.data) -class UpdateModelMixin(object): +class UpdateModelMixin: """ Update a model instance. """ @@ -84,7 +82,7 @@ class UpdateModelMixin(object): return self.update(request, *args, **kwargs) -class DestroyModelMixin(object): +class DestroyModelMixin: """ Destroy a model instance. """ diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index ca1b59f12..76113a827 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -2,8 +2,6 @@ Content negotiation deals with selecting an appropriate renderer given the incoming request. Typically this will be based on the request's Accept header. """ -from __future__ import unicode_literals - from django.http import Http404 from rest_framework import HTTP_HEADER_ENCODING, exceptions @@ -13,7 +11,7 @@ from rest_framework.utils.mediatypes import ( ) -class BaseContentNegotiation(object): +class BaseContentNegotiation: def select_parser(self, request, parsers): raise NotImplementedError('.select_parser() must be implemented') @@ -66,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/pagination.py b/rest_framework/pagination.py index b11d7cdf3..fcc78da43 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -1,19 +1,15 @@ -# coding: utf-8 """ Pagination serializers determine the structure of the output that should be used for paginated responses. """ -from __future__ import unicode_literals - from base64 import b64decode, b64encode from collections import OrderedDict, namedtuple +from urllib import parse from django.core.paginator import InvalidPage from django.core.paginator import Paginator as DjangoPaginator from django.template import loader -from django.utils import six from django.utils.encoding import force_text -from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import coreapi, coreschema @@ -133,7 +129,7 @@ PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break']) PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True) -class BasePagination(object): +class BasePagination: display_page_controls = False def paginate_queryset(self, queryset, request, view=None): # pragma: no cover @@ -204,7 +200,7 @@ class PageNumberPagination(BasePagination): self.page = paginator.page(page_number) except InvalidPage as exc: msg = self.invalid_page_message.format( - page_number=page_number, message=six.text_type(exc) + page_number=page_number, message=str(exc) ) raise NotFound(msg) @@ -716,13 +712,13 @@ class CursorPagination(BasePagination): 'nearly-unique field on the model, such as "-created" or "pk".' ) - assert isinstance(ordering, (six.string_types, list, tuple)), ( + assert isinstance(ordering, (str, list, tuple)), ( 'Invalid ordering. Expected string or tuple, but got {type}'.format( type=type(ordering).__name__ ) ) - if isinstance(ordering, six.string_types): + if isinstance(ordering, str): return (ordering,) return tuple(ordering) @@ -737,7 +733,7 @@ class CursorPagination(BasePagination): try: querystring = b64decode(encoded.encode('ascii')).decode('ascii') - tokens = urlparse.parse_qs(querystring, keep_blank_values=True) + tokens = parse.parse_qs(querystring, keep_blank_values=True) offset = tokens.get('o', ['0'])[0] offset = _positive_int(offset, cutoff=self.offset_cutoff) @@ -763,7 +759,7 @@ class CursorPagination(BasePagination): if cursor.position is not None: tokens['p'] = cursor.position - querystring = urlparse.urlencode(tokens, doseq=True) + querystring = parse.urlencode(tokens, doseq=True) encoded = b64encode(querystring.encode('ascii')).decode('ascii') return replace_query_param(self.base_url, self.cursor_query_param, encoded) @@ -773,7 +769,7 @@ class CursorPagination(BasePagination): attr = instance[field_name] else: attr = getattr(instance, field_name) - return six.text_type(attr) + return str(attr) def get_paginated_response(self, data): return Response(OrderedDict([ diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 35d0d1aa7..5b5e3f158 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -4,9 +4,8 @@ Parsers are used to parse the content of incoming HTTP requests. They give us a generic way of being able to handle various media types on the request, such as form content or json encoded data. """ -from __future__ import unicode_literals - import codecs +from urllib import parse from django.conf import settings from django.core.files.uploadhandler import StopFutureHandlers @@ -15,9 +14,7 @@ from django.http.multipartparser import ChunkIter from django.http.multipartparser import \ MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError, parse_header -from django.utils import six from django.utils.encoding import force_text -from django.utils.six.moves.urllib import parse as urlparse from rest_framework import renderers from rest_framework.exceptions import ParseError @@ -25,13 +22,13 @@ from rest_framework.settings import api_settings from rest_framework.utils import json -class DataAndFiles(object): +class DataAndFiles: def __init__(self, data, files): self.data = data self.files = files -class BaseParser(object): +class BaseParser: """ All parsers should extend `BaseParser`, specifying a `media_type` attribute, and overriding the `.parse()` method. @@ -67,7 +64,7 @@ class JSONParser(BaseParser): parse_constant = json.strict_constant if self.strict else None return json.load(decoded_stream, parse_constant=parse_constant) except ValueError as exc: - raise ParseError('JSON parse error - %s' % six.text_type(exc)) + raise ParseError('JSON parse error - %s' % str(exc)) class FormParser(BaseParser): @@ -113,7 +110,7 @@ class MultiPartParser(BaseParser): data, files = parser.parse() return DataAndFiles(data, files) except MultiPartParserError as exc: - raise ParseError('Multipart form parse error - %s' % six.text_type(exc)) + raise ParseError('Multipart form parse error - %s' % str(exc)) class FileUploadParser(BaseParser): @@ -221,7 +218,7 @@ class FileUploadParser(BaseParser): encoded_filename = force_text(filename_parm['filename*']) try: charset, lang, filename = encoded_filename.split('\'', 2) - filename = urlparse.unquote(filename) + filename = parse.unquote(filename) except (ValueError, LookupError): filename = force_text(filename_parm['filename']) return filename diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 5d75f54ba..3a8c58064 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -1,10 +1,7 @@ """ Provides a set of pluggable permission policies. """ -from __future__ import unicode_literals - from django.http import Http404 -from django.utils import six from rest_framework import exceptions @@ -101,8 +98,7 @@ class BasePermissionMetaclass(OperationHolderMixin, type): pass -@six.add_metaclass(BasePermissionMetaclass) -class BasePermission(object): +class BasePermission(metaclass=BasePermissionMetaclass): """ A base class from which all permission classes should inherit. """ diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 31c1e7561..76c4d7008 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -1,18 +1,12 @@ -# coding: utf-8 -from __future__ import unicode_literals - import sys from collections import OrderedDict +from urllib import parse from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.db.models import Manager from django.db.models.query import QuerySet from django.urls import NoReverseMatch, Resolver404, get_script_prefix, resolve -from django.utils import six -from django.utils.encoding import ( - python_2_unicode_compatible, smart_text, uri_to_iri -) -from django.utils.six.moves.urllib import parse as urlparse +from django.utils.encoding import smart_text, uri_to_iri from django.utils.translation import ugettext_lazy as _ from rest_framework.fields import ( @@ -46,14 +40,14 @@ class ObjectTypeError(TypeError): """ -class Hyperlink(six.text_type): +class Hyperlink(str): """ A string like object that additionally has an associated name. We use this for hyperlinked URLs that may render as a named link in some contexts, or render as a plain URL in others. """ def __new__(self, url, obj): - ret = six.text_type.__new__(self, url) + ret = str.__new__(self, url) ret.obj = obj return ret @@ -65,13 +59,12 @@ class Hyperlink(six.text_type): # This ensures that we only called `__str__` lazily, # as in some cases calling __str__ on a model instances *might* # involve a database lookup. - return six.text_type(self.obj) + return str(self.obj) is_hyperlink = True -@python_2_unicode_compatible -class PKOnlyObject(object): +class PKOnlyObject: """ This is a mock object, used for when we only need the pk of the object instance, but still want to return an object with a .pk attribute, @@ -121,14 +114,14 @@ class RelatedField(Field): ) kwargs.pop('many', None) kwargs.pop('allow_empty', None) - super(RelatedField, self).__init__(**kwargs) + super().__init__(**kwargs) def __new__(cls, *args, **kwargs): # We override this method in order to automagically create # `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): @@ -157,7 +150,7 @@ class RelatedField(Field): # We force empty strings to None values for relational fields. if data == '': data = None - return super(RelatedField, self).run_validation(data) + return super().run_validation(data) def get_queryset(self): queryset = self.queryset @@ -189,7 +182,7 @@ class RelatedField(Field): pass # Standard case, return the object instance. - return super(RelatedField, self).get_attribute(instance) + return super().get_attribute(instance) def get_choices(self, cutoff=None): queryset = self.get_queryset() @@ -225,7 +218,7 @@ class RelatedField(Field): ) def display_value(self, instance): - return six.text_type(instance) + return str(instance) class StringRelatedField(RelatedField): @@ -236,10 +229,10 @@ class StringRelatedField(RelatedField): def __init__(self, **kwargs): kwargs['read_only'] = True - super(StringRelatedField, self).__init__(**kwargs) + super().__init__(**kwargs) def to_representation(self, value): - return six.text_type(value) + return str(value) class PrimaryKeyRelatedField(RelatedField): @@ -251,7 +244,7 @@ class PrimaryKeyRelatedField(RelatedField): def __init__(self, **kwargs): self.pk_field = kwargs.pop('pk_field', None) - super(PrimaryKeyRelatedField, self).__init__(**kwargs) + super().__init__(**kwargs) def use_pk_only_optimization(self): return True @@ -297,7 +290,7 @@ class HyperlinkedRelatedField(RelatedField): # implicit `self` argument to be passed. self.reverse = reverse - super(HyperlinkedRelatedField, self).__init__(**kwargs) + super().__init__(**kwargs) def use_pk_only_optimization(self): return self.lookup_field == 'pk' @@ -317,10 +310,10 @@ class HyperlinkedRelatedField(RelatedField): return queryset.get(**lookup_kwargs) except ValueError: exc = ObjectValueError(str(sys.exc_info()[1])) - six.reraise(type(exc), exc, sys.exc_info()[2]) + raise exc.with_traceback(sys.exc_info()[2]) except TypeError: exc = ObjectTypeError(str(sys.exc_info()[1])) - six.reraise(type(exc), exc, sys.exc_info()[2]) + raise exc.with_traceback(sys.exc_info()[2]) def get_url(self, obj, view_name, request, format): """ @@ -346,7 +339,7 @@ class HyperlinkedRelatedField(RelatedField): if http_prefix: # If needed convert absolute URLs to relative path - data = urlparse.urlparse(data).path + data = parse.urlparse(data).path prefix = get_script_prefix() if data.startswith(prefix): data = '/' + data[len(prefix):] @@ -432,7 +425,7 @@ class HyperlinkedIdentityField(HyperlinkedRelatedField): assert view_name is not None, 'The `view_name` argument is required.' kwargs['read_only'] = True kwargs['source'] = '*' - super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs) + super().__init__(view_name, **kwargs) def use_pk_only_optimization(self): # We have the complete object instance already. We don't need @@ -453,7 +446,7 @@ class SlugRelatedField(RelatedField): def __init__(self, slug_field=None, **kwargs): assert slug_field is not None, 'The `slug_field` argument is required.' self.slug_field = slug_field - super(SlugRelatedField, self).__init__(**kwargs) + super().__init__(**kwargs) def to_internal_value(self, data): try: @@ -502,7 +495,7 @@ class ManyRelatedField(Field): self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT) ) assert child_relation is not None, '`child_relation` is a required argument.' - super(ManyRelatedField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.child_relation.bind(field_name='', parent=self) def get_value(self, dictionary): @@ -518,7 +511,7 @@ class ManyRelatedField(Field): return dictionary.get(self.field_name, empty) def to_internal_value(self, data): - if isinstance(data, six.text_type) or not hasattr(data, '__iter__'): + if isinstance(data, str) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index f043e6327..eb5da008b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -6,10 +6,9 @@ on the response, such as JSON encoded data or HTML output. REST framework also provides an HTML renderer that renders the browsable API. """ -from __future__ import unicode_literals - import base64 from collections import OrderedDict +from urllib import parse from django import forms from django.conf import settings @@ -19,9 +18,7 @@ from django.http.multipartparser import parse_header from django.template import engines, loader from django.test.client import encode_multipart from django.urls import NoReverseMatch -from django.utils import six from django.utils.html import mark_safe -from django.utils.six.moves.urllib import parse as urlparse from rest_framework import VERSION, exceptions, serializers, status from rest_framework.compat import ( @@ -40,7 +37,7 @@ def zero_as_none(value): return None if value == 0 else value -class BaseRenderer(object): +class BaseRenderer: """ All renderers should extend this class, setting the `media_type` and `format` attributes, and override the `.render()` method. @@ -111,7 +108,7 @@ class JSONRenderer(BaseRenderer): # but if ensure_ascii=False, the return type is underspecified, # and may (or may not) be unicode. # On python 3.x json.dumps() returns unicode strings. - if isinstance(ret, six.text_type): + if isinstance(ret, str): # We always fully escape \u2028 and \u2029 to ensure we output JSON # that is a strict javascript subset. If bytes were returned # by json.dumps() then we don't have these characters in any case. @@ -349,7 +346,7 @@ class HTMLFormRenderer(BaseRenderer): # Get a clone of the field with text-only value representation. field = field.as_form_field() - if style.get('input_type') == 'datetime-local' and isinstance(field.value, six.text_type): + if style.get('input_type') == 'datetime-local' and isinstance(field.value, str): field.value = field.value.rstrip('Z') if 'template' in style: @@ -791,7 +788,7 @@ class AdminRenderer(BrowsableAPIRenderer): """ Render the HTML for the browsable API representation. """ - context = super(AdminRenderer, self).get_context( + context = super().get_context( data, accepted_media_type, renderer_context ) @@ -995,14 +992,14 @@ class _BaseOpenAPIRenderer: tag = None for name, link in document.links.items(): - path = urlparse.urlparse(link.url).path + path = parse.urlparse(link.url).path method = link.action.lower() paths.setdefault(path, {}) paths[path][method] = self.get_operation(link, name, tag=tag) for tag, section in document.data.items(): for name, link in section.links.items(): - path = urlparse.urlparse(link.url).path + path = parse.urlparse(link.url).path method = link.action.lower() paths.setdefault(path, {}) paths[path][method] = self.get_operation(link, name, tag=tag) diff --git a/rest_framework/request.py b/rest_framework/request.py index a6d92e2bd..ec4b749c2 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -8,8 +8,6 @@ The wrapped request then offers a richer API, in particular : - full support of PUT method, including support for file uploads - form overloading of HTTP method, content type and content """ -from __future__ import unicode_literals - import io import sys from contextlib import contextmanager @@ -18,7 +16,6 @@ from django.conf import settings from django.http import HttpRequest, QueryDict from django.http.multipartparser import parse_header from django.http.request import RawPostDataException -from django.utils import six from django.utils.datastructures import MultiValueDict from rest_framework import HTTP_HEADER_ENCODING, exceptions @@ -34,7 +31,7 @@ def is_form_media_type(media_type): base_media_type == 'multipart/form-data') -class override_method(object): +class override_method: """ A context manager that temporarily overrides the method on a request, additionally setting the `view.request` attribute. @@ -78,10 +75,10 @@ def wrap_attributeerrors(): except AttributeError: info = sys.exc_info() exc = WrappedAttributeError(str(info[1])) - six.reraise(type(exc), exc, info[2]) + raise exc.with_traceback(info[2]) -class Empty(object): +class Empty: """ Placeholder for unset attributes. Cannot use `None`, as that may be a valid value. @@ -126,7 +123,7 @@ def clone_request(request, method): return ret -class ForcedAuthentication(object): +class ForcedAuthentication: """ This authentication class is used if the test client or request factory forcibly authenticated the request. @@ -140,7 +137,7 @@ class ForcedAuthentication(object): return (self.force_user, self.force_token) -class Request(object): +class Request: """ Wrapper allowing to enhance a standard `HttpRequest` instance. diff --git a/rest_framework/response.py b/rest_framework/response.py index bf0663255..db7977770 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -4,11 +4,9 @@ it is initialized with unrendered data, instead of a pre-rendered string. The appropriate renderer is called during Django's template response rendering. """ -from __future__ import unicode_literals +from http.client import responses from django.template.response import SimpleTemplateResponse -from django.utils import six -from django.utils.six.moves.http_client import responses from rest_framework.serializers import Serializer @@ -29,7 +27,7 @@ class Response(SimpleTemplateResponse): Setting 'renderer' and 'media_type' will typically be deferred, For example being set automatically by the `APIView`. """ - super(Response, self).__init__(None, status=status) + super().__init__(None, status=status) if isinstance(data, Serializer): msg = ( @@ -45,7 +43,7 @@ class Response(SimpleTemplateResponse): self.content_type = content_type if headers: - for name, value in six.iteritems(headers): + for name, value in headers.items(): self[name] = value @property @@ -64,13 +62,13 @@ 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 ret = renderer.render(self.data, accepted_media_type, context) - if isinstance(ret, six.text_type): + if isinstance(ret, str): assert charset, ( 'renderer returned unicode, and did not specify ' 'a charset value.' @@ -94,7 +92,7 @@ class Response(SimpleTemplateResponse): """ Remove attributes from the response that shouldn't be cached. """ - state = super(Response, self).__getstate__() + state = super().__getstate__() for key in ( 'accepted_renderer', 'renderer_context', 'resolver_match', 'client', 'request', 'json', 'wsgi_request' diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index e9cf737f1..55bf74af1 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -1,11 +1,8 @@ """ Provide urlresolver functions that return fully qualified URLs or view names """ -from __future__ import unicode_literals - from django.urls import NoReverseMatch from django.urls import reverse as django_reverse -from django.utils import six from django.utils.functional import lazy from rest_framework.settings import api_settings @@ -66,4 +63,4 @@ def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extr return url -reverse_lazy = lazy(reverse, six.text_type) +reverse_lazy = lazy(reverse, str) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 1cacea181..9334706f8 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -13,8 +13,6 @@ For example, you might have a `urls.py` that looks something like this: urlpatterns = router.urls """ -from __future__ import unicode_literals - import itertools import warnings from collections import OrderedDict, namedtuple @@ -22,7 +20,6 @@ from collections import OrderedDict, namedtuple from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured from django.urls import NoReverseMatch -from django.utils import six from django.utils.deprecation import RenameMethodsBase from rest_framework import ( @@ -39,7 +36,7 @@ Route = namedtuple('Route', ['url', 'mapping', 'name', 'detail', 'initkwargs']) DynamicRoute = namedtuple('DynamicRoute', ['url', 'name', 'detail', 'initkwargs']) -class DynamicDetailRoute(object): +class DynamicDetailRoute: def __new__(cls, url, name, initkwargs): warnings.warn( "`DynamicDetailRoute` is deprecated and will be removed in 3.10 " @@ -50,7 +47,7 @@ class DynamicDetailRoute(object): return DynamicRoute(url, name, True, initkwargs) -class DynamicListRoute(object): +class DynamicListRoute: def __new__(cls, url, name, initkwargs): warnings.warn( "`DynamicListRoute` is deprecated and will be removed in 3.10 in " @@ -83,7 +80,7 @@ class RenameRouterMethods(RenameMethodsBase): ) -class BaseRouter(six.with_metaclass(RenameRouterMethods)): +class BaseRouter(metaclass=RenameRouterMethods): def __init__(self): self.registry = [] @@ -173,7 +170,7 @@ class SimpleRouter(BaseRouter): def __init__(self, trailing_slash=True): self.trailing_slash = '/' if trailing_slash else '' - super(SimpleRouter, self).__init__() + super().__init__() def get_default_basename(self, viewset): """ @@ -365,7 +362,7 @@ class DefaultRouter(SimpleRouter): self.root_renderers = kwargs.pop('root_renderers') else: self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) - super(DefaultRouter, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_api_root_view(self, api_urls=None): """ @@ -383,7 +380,7 @@ class DefaultRouter(SimpleRouter): Generate the list of URL patterns, including a default root view for the API, and appending `.json` style format suffixes. """ - urls = super(DefaultRouter, self).get_urls() + urls = super().get_urls() if self.include_root_view: view = self.get_api_root_view(api_urls=urls) diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index db226a6c1..b8da446f7 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -11,7 +11,6 @@ from django.conf import settings from django.contrib.admindocs.views import simplify_regex from django.core.exceptions import PermissionDenied from django.http import Http404 -from django.utils import six from rest_framework import exceptions from rest_framework.compat import ( @@ -68,7 +67,7 @@ class LinkNode(OrderedDict): def __init__(self): self.links = [] self.methods_counter = Counter() - super(LinkNode, self).__init__() + super().__init__() def get_available_key(self, preferred_key): if preferred_key not in self: @@ -140,7 +139,7 @@ _PATH_PARAMETER_COMPONENT_RE = re.compile( ) -class EndpointEnumerator(object): +class EndpointEnumerator: """ A class to determine the available API endpoints that a project exposes. """ @@ -151,7 +150,7 @@ class EndpointEnumerator(object): urlconf = settings.ROOT_URLCONF # Load the given URLconf module - if isinstance(urlconf, six.string_types): + if isinstance(urlconf, str): urls = import_module(urlconf) else: urls = urlconf @@ -232,7 +231,7 @@ class EndpointEnumerator(object): return [method for method in methods if method not in ('OPTIONS', 'HEAD')] -class SchemaGenerator(object): +class SchemaGenerator: # Map HTTP methods onto actions. default_mapping = { 'get': 'retrieve', diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index 85142edce..91d8405eb 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ inspectors.py # Per-endpoint view introspection @@ -7,11 +6,11 @@ See schemas.__init__.py for package overview. import re import warnings from collections import OrderedDict +from urllib import parse from weakref import WeakKeyDictionary from django.db import models from django.utils.encoding import force_text, smart_text -from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ from rest_framework import exceptions, serializers @@ -125,7 +124,7 @@ def get_pk_description(model, model_field): ) -class ViewInspector(object): +class ViewInspector: """ Descriptor class on APIView. @@ -207,7 +206,7 @@ class AutoSchema(ViewInspector): * `manual_fields`: list of `coreapi.Field` instances that will be added to auto-generated fields, overwriting on `Field.name` """ - super(AutoSchema, self).__init__() + super().__init__() if manual_fields is None: manual_fields = [] self._manual_fields = manual_fields @@ -232,7 +231,7 @@ class AutoSchema(ViewInspector): path = path[1:] return coreapi.Link( - url=urlparse.urljoin(base_url, path), + url=parse.urljoin(base_url, path), action=method.lower(), encoding=encoding, fields=fields, @@ -475,7 +474,7 @@ class ManualSchema(ViewInspector): * `fields`: list of `coreapi.Field` instances. * `description`: String description for view. Optional. """ - super(ManualSchema, self).__init__() + super().__init__() assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances" self._fields = fields self._description = description @@ -487,7 +486,7 @@ class ManualSchema(ViewInspector): path = path[1:] return coreapi.Link( - url=urlparse.urljoin(base_url, path), + url=parse.urljoin(base_url, path), action=method.lower(), encoding=self._encoding, fields=self._fields, @@ -498,7 +497,7 @@ class ManualSchema(ViewInspector): class DefaultSchema(ViewInspector): """Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting""" def __get__(self, instance, owner): - result = super(DefaultSchema, self).__get__(instance, owner) + result = super().__get__(instance, owner) if not isinstance(result, DefaultSchema): return result diff --git a/rest_framework/schemas/views.py b/rest_framework/schemas/views.py index f5e327a94..fa5cdbdc7 100644 --- a/rest_framework/schemas/views.py +++ b/rest_framework/schemas/views.py @@ -17,7 +17,7 @@ class SchemaView(APIView): public = False def __init__(self, *args, **kwargs): - super(SchemaView, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.renderer_classes is None: self.renderer_classes = [ renderers.OpenAPIRenderer, @@ -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 9830edb3f..90b31e068 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,8 +10,6 @@ python primitives. 2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ -from __future__ import unicode_literals - import copy import inspect import traceback @@ -23,11 +21,11 @@ from django.db import models from django.db.models import DurationField as ModelDurationField from django.db.models.fields import Field as DjangoModelField from django.db.models.fields import FieldDoesNotExist -from django.utils import six, timezone +from django.utils import timezone from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import Mapping, postgres_fields, unicode_to_repr +from rest_framework.compat import Mapping, postgres_fields from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.fields import get_error_detail, set_value from rest_framework.settings import api_settings @@ -115,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): @@ -315,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): @@ -344,8 +342,7 @@ def as_serializer_error(exc): } -@six.add_metaclass(SerializerMetaclass) -class Serializer(BaseSerializer): +class Serializer(BaseSerializer, metaclass=SerializerMetaclass): default_error_messages = { 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.') } @@ -466,7 +463,7 @@ class Serializer(BaseSerializer): 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): """ @@ -535,7 +532,7 @@ class Serializer(BaseSerializer): return attrs def __repr__(self): - return unicode_to_repr(representation.serializer_repr(self, indent=1)) + return representation.serializer_repr(self, indent=1) # The following are used for accessing `BoundField` instances on the # serializer, for the purposes of presenting a form-like API onto the @@ -560,12 +557,12 @@ class Serializer(BaseSerializer): @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. @@ -591,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): @@ -758,19 +755,19 @@ class ListSerializer(BaseSerializer): return not bool(self._errors) def __repr__(self): - return unicode_to_repr(representation.list_repr(self, indent=1)) + return representation.list_repr(self, indent=1) # Include a backlink to the serializer class on return objects. # Allows renderers such as HTMLFormRenderer to get the full field info. @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/settings.py b/rest_framework/settings.py index 8db9c81ed..5d92d0cb4 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -18,13 +18,10 @@ This module provides the `api_setting` object, that is used to access REST framework settings, checking for user settings first, then falling back to the defaults. """ -from __future__ import unicode_literals - from importlib import import_module from django.conf import settings from django.test.signals import setting_changed -from django.utils import six from rest_framework import ISO_8601 @@ -166,7 +163,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] @@ -187,7 +184,7 @@ def import_from_string(val, setting_name): raise ImportError(msg) -class APISettings(object): +class APISettings: """ A settings object, that allows API settings to be accessed as properties. For example: diff --git a/rest_framework/status.py b/rest_framework/status.py index 4b4561cfc..1489b440c 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -5,7 +5,6 @@ See RFC 2616 - https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html And RFC 6585 - https://tools.ietf.org/html/rfc6585 And RFC 4918 - https://tools.ietf.org/html/rfc4918 """ -from __future__ import unicode_literals def is_informational(code): diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index f48675d5e..56e2994ea 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -1,12 +1,9 @@ -from __future__ import absolute_import, unicode_literals - import re from collections import OrderedDict from django import template from django.template import loader from django.urls import NoReverseMatch, reverse -from django.utils import six from django.utils.encoding import force_text, iri_to_uri from django.utils.html import escape, format_html, smart_urlquote from django.utils.safestring import SafeData, mark_safe @@ -187,7 +184,7 @@ def add_class(value, css_class): In the case of REST Framework, the filter is used to add Bootstrap-specific classes to the forms. """ - html = six.text_type(value) + html = str(value) match = class_re.search(html) if match: m = re.search(r'^%s$|^%s\s|\s%s\s|\s%s$' % (css_class, css_class, @@ -204,7 +201,7 @@ def add_class(value, css_class): @register.filter def format_value(value): if getattr(value, 'is_hyperlink', False): - name = six.text_type(value.obj) + name = str(value.obj) return mark_safe('%s' % (value, escape(name))) if value is None or isinstance(value, bool): return mark_safe('%s' % {True: 'true', False: 'false', None: 'null'}[value]) @@ -219,7 +216,7 @@ def format_value(value): template = loader.get_template('rest_framework/admin/dict_value.html') context = {'value': value} return template.render(context) - elif isinstance(value, six.string_types): + elif isinstance(value, str): if ( (value.startswith('http:') or value.startswith('https:')) and not re.search(r'\s', value) @@ -229,7 +226,7 @@ def format_value(value): return mark_safe('{value}'.format(value=escape(value))) elif '\n' in value: return mark_safe('
%s
' % escape(value)) - return six.text_type(value) + return str(value) @register.filter diff --git a/rest_framework/test.py b/rest_framework/test.py index edacf0066..852d4919e 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -1,9 +1,5 @@ -# -- coding: utf-8 -- - # Note that we import as `DjangoRequestFactory` and `DjangoClient` in order # to make it harder for the user to import the wrong thing without realizing. -from __future__ import unicode_literals - import io from importlib import import_module @@ -14,7 +10,6 @@ from django.test import override_settings, testcases from django.test.client import Client as DjangoClient from django.test.client import ClientHandler from django.test.client import RequestFactory as DjangoRequestFactory -from django.utils import six from django.utils.encoding import force_bytes from django.utils.http import urlencode @@ -32,7 +27,7 @@ if requests is not None: def get_all(self, key, default): return self.getheaders(key) - class MockOriginalResponse(object): + class MockOriginalResponse: def __init__(self, headers): self.msg = HeaderDict(headers) self.closed = False @@ -109,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) @@ -117,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): @@ -129,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): @@ -149,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): """ @@ -171,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, @@ -184,12 +179,12 @@ 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 ) # Coerce text to bytes if required. - if isinstance(ret, six.text_type): + if isinstance(ret, str): ret = bytes(ret.encode(renderer.charset)) return ret, content_type @@ -202,8 +197,7 @@ class APIRequestFactory(DjangoRequestFactory): # Fix to support old behavior where you have the arguments in the # url. See #1461. query_string = force_bytes(path.split('?')[1]) - if six.PY3: - query_string = query_string.decode('iso-8859-1') + query_string = query_string.decode('iso-8859-1') r['QUERY_STRING'] = query_string r.update(extra) return self.generic('GET', path, **r) @@ -234,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 @@ -252,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 = {} @@ -286,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) @@ -304,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) @@ -312,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) @@ -320,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) @@ -328,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) @@ -342,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): @@ -389,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 834ced148..0ba2ba66b 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -1,8 +1,6 @@ """ Provides various throttling policies. """ -from __future__ import unicode_literals - import time from django.core.cache import cache as default_cache @@ -11,7 +9,7 @@ from django.core.exceptions import ImproperlyConfigured from rest_framework.settings import api_settings -class BaseThrottle(object): +class BaseThrottle: """ Rate throttling of requests. """ @@ -232,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/urlpatterns.py b/rest_framework/urlpatterns.py index ab3a74978..831d344dd 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf.urls import include, url from rest_framework.compat import ( diff --git a/rest_framework/urls.py b/rest_framework/urls.py index 0e4c2661b..482a0a364 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -11,8 +11,6 @@ your API requires authentication: You should make sure your authentication settings include `SessionAuthentication`. """ -from __future__ import unicode_literals - from django.conf.urls import url from django.contrib.auth import views diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index e0374ffd0..54990e9f6 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.urls import get_script_prefix, resolve diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index d8f4aeb4e..dee2f942e 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -1,15 +1,13 @@ """ Helper classes for parsers. """ -from __future__ import absolute_import, unicode_literals - import datetime import decimal import json # noqa import uuid from django.db.models.query import QuerySet -from django.utils import six, timezone +from django.utils import timezone from django.utils.encoding import force_text from django.utils.functional import Promise @@ -39,12 +37,12 @@ class JSONEncoder(json.JSONEncoder): representation = obj.isoformat() return representation elif isinstance(obj, datetime.timedelta): - return six.text_type(obj.total_seconds()) + return str(obj.total_seconds()) elif isinstance(obj, decimal.Decimal): # Serializers will coerce decimals to strings by default. return float(obj) elif isinstance(obj, uuid.UUID): - return six.text_type(obj) + return str(obj) elif isinstance(obj, QuerySet): return tuple(obj) elif isinstance(obj, bytes): @@ -65,4 +63,4 @@ class JSONEncoder(json.JSONEncoder): pass elif hasattr(obj, '__iter__'): return tuple(item for item in obj) - return super(JSONEncoder, self).default(obj) + return super().default(obj) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 927d08ff2..1281ee167 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -16,7 +16,7 @@ NUMERIC_FIELD_TYPES = ( ) -class ClassLookupDict(object): +class ClassLookupDict: """ Takes a dictionary with classes as keys. Lookups against this object will traverses the object's inheritance diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index aa805f14e..4e003f614 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -1,8 +1,6 @@ """ Utility functions to return a formatted name and description for a given view. """ -from __future__ import unicode_literals - import re from django.utils.encoding import force_text diff --git a/rest_framework/utils/json.py b/rest_framework/utils/json.py index cb5572380..1c1e69bf1 100644 --- a/rest_framework/utils/json.py +++ b/rest_framework/utils/json.py @@ -5,9 +5,6 @@ REST framework should always import this wrapper module in order to maintain spec-compliant encoding/decoding. Support for non-standard features should be handled by users at the renderer and parser layer. """ - -from __future__ import absolute_import - import functools import json # noqa diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index f4acf4807..40bdf2615 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -3,10 +3,7 @@ Handling of media types, as found in HTTP Content-Type and Accept headers. See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 """ -from __future__ import unicode_literals - from django.http.multipartparser import parse_header -from django.utils.encoding import python_2_unicode_compatible from rest_framework import HTTP_HEADER_ENCODING @@ -46,8 +43,7 @@ def order_by_precedence(media_type_lst): return [media_types for media_types in ret if media_types] -@python_2_unicode_compatible -class _MediaType(object): +class _MediaType: def __init__(self, media_type_str): self.orig = '' if (media_type_str is None) else media_type_str self.full_type, self.params = parse_header(self.orig.encode(HTTP_HEADER_ENCODING)) diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py index deeaf1f63..ebead5d75 100644 --- a/rest_framework/utils/representation.py +++ b/rest_framework/utils/representation.py @@ -2,16 +2,12 @@ Helper functions for creating user-friendly representations of serializer classes and serializer fields. """ -from __future__ import unicode_literals - import re from django.db import models from django.utils.encoding import force_text from django.utils.functional import Promise -from rest_framework.compat import unicode_repr - def manager_repr(value): model = value.model @@ -34,7 +30,7 @@ def smart_repr(value): if isinstance(value, Promise) and value._delegate_text: value = force_text(value) - value = unicode_repr(value) + value = repr(value) # Representations like u'help text' # should simply be presented as 'help text' diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index c24e51d09..8709352f1 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,10 +1,8 @@ -from __future__ import unicode_literals - from collections import OrderedDict from django.utils.encoding import force_text -from rest_framework.compat import MutableMapping, unicode_to_repr +from rest_framework.compat import MutableMapping from rest_framework.utils import json @@ -17,7 +15,7 @@ class ReturnDict(OrderedDict): def __init__(self, *args, **kwargs): self.serializer = kwargs.pop('serializer') - super(ReturnDict, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def copy(self): return ReturnDict(self, serializer=self.serializer) @@ -40,7 +38,7 @@ class ReturnList(list): def __init__(self, *args, **kwargs): self.serializer = kwargs.pop('serializer') - super(ReturnList, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def __repr__(self): return list.__repr__(self) @@ -51,7 +49,7 @@ class ReturnList(list): return (list, (list(self),)) -class BoundField(object): +class BoundField: """ A field object that also includes `.value` and `.error` properties. Returned when iterating over a serializer instance, @@ -73,9 +71,9 @@ class BoundField(object): return self._field.__class__ def __repr__(self): - return unicode_to_repr('<%s value=%s errors=%s>' % ( + return '<%s value=%s errors=%s>' % ( self.__class__.__name__, self.value, self.errors - )) + ) def as_form_field(self): value = '' if (self.value is None or self.value is False) else self.value @@ -103,9 +101,9 @@ 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(NestedBoundField, self).__init__(field, value, errors, prefix) + super().__init__(field, value, errors, prefix) def __iter__(self): for field in self.fields.values(): diff --git a/rest_framework/utils/urls.py b/rest_framework/utils/urls.py index 3766928d4..3534e5f49 100644 --- a/rest_framework/utils/urls.py +++ b/rest_framework/utils/urls.py @@ -1,5 +1,6 @@ +from urllib import parse + from django.utils.encoding import force_str -from django.utils.six.moves.urllib import parse as urlparse def replace_query_param(url, key, val): @@ -7,11 +8,11 @@ def replace_query_param(url, key, val): Given a URL and a key/val pair, set or replace an item in the query parameters of the URL, and return the new URL. """ - (scheme, netloc, path, query, fragment) = urlparse.urlsplit(force_str(url)) - query_dict = urlparse.parse_qs(query, keep_blank_values=True) + (scheme, netloc, path, query, fragment) = parse.urlsplit(force_str(url)) + query_dict = parse.parse_qs(query, keep_blank_values=True) query_dict[force_str(key)] = [force_str(val)] - query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) - return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) + query = parse.urlencode(sorted(list(query_dict.items())), doseq=True) + return parse.urlunsplit((scheme, netloc, path, query, fragment)) def remove_query_param(url, key): @@ -19,8 +20,8 @@ def remove_query_param(url, key): Given a URL and a key/val pair, remove an item in the query parameters of the URL, and return the new URL. """ - (scheme, netloc, path, query, fragment) = urlparse.urlsplit(force_str(url)) - query_dict = urlparse.parse_qs(query, keep_blank_values=True) + (scheme, netloc, path, query, fragment) = parse.urlsplit(force_str(url)) + query_dict = parse.parse_qs(query, keep_blank_values=True) query_dict.pop(key, None) - query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) - return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) + query = parse.urlencode(sorted(list(query_dict.items())), doseq=True) + return parse.urlunsplit((scheme, netloc, path, query, fragment)) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 2ea3e5ac1..a5222fbc6 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -6,12 +6,9 @@ This gives us better separation of concerns, allows us to use single-step object creation, and makes it possible to switch between using the implicit `ModelSerializer` class and an equivalent explicit `Serializer` class. """ -from __future__ import unicode_literals - from django.db import DataError from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import unicode_to_repr from rest_framework.exceptions import ValidationError from rest_framework.utils.representation import smart_repr @@ -33,7 +30,7 @@ def qs_filter(queryset, **kwargs): return queryset.none() -class UniqueValidator(object): +class UniqueValidator: """ Validator that corresponds to `unique=True` on a model field. @@ -82,13 +79,13 @@ class UniqueValidator(object): raise ValidationError(self.message, code='unique') def __repr__(self): - return unicode_to_repr('<%s(queryset=%s)>' % ( + return '<%s(queryset=%s)>' % ( self.__class__.__name__, smart_repr(self.queryset) - )) + ) -class UniqueTogetherValidator(object): +class UniqueTogetherValidator: """ Validator that corresponds to `unique_together = (...)` on a model class. @@ -170,14 +167,14 @@ class UniqueTogetherValidator(object): raise ValidationError(message, code='unique') def __repr__(self): - return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % ( + return '<%s(queryset=%s, fields=%s)>' % ( self.__class__.__name__, smart_repr(self.queryset), smart_repr(self.fields) - )) + ) -class BaseUniqueForValidator(object): +class BaseUniqueForValidator: message = None missing_message = _('This field is required.') @@ -236,12 +233,12 @@ class BaseUniqueForValidator(object): }, code='unique') def __repr__(self): - return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % ( + return '<%s(queryset=%s, field=%s, date_field=%s)>' % ( self.__class__.__name__, smart_repr(self.queryset), smart_repr(self.field), smart_repr(self.date_field) - )) + ) class UniqueForDateValidator(BaseUniqueForValidator): diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index 206ff6c2e..0631a75c9 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -1,6 +1,3 @@ -# coding: utf-8 -from __future__ import unicode_literals - import re from django.utils.translation import ugettext_lazy as _ @@ -13,7 +10,7 @@ from rest_framework.templatetags.rest_framework import replace_query_param from rest_framework.utils.mediatypes import _MediaType -class BaseVersioning(object): +class BaseVersioning: default_version = api_settings.DEFAULT_VERSION allowed_versions = api_settings.ALLOWED_VERSIONS version_param = api_settings.VERSION_PARAM @@ -87,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 ) @@ -133,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 ) @@ -179,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 9d5d959e9..6ef7021d4 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -1,8 +1,6 @@ """ Provides an APIView class that is the base of all views in REST framework. """ -from __future__ import unicode_literals - from django.conf import settings from django.core.exceptions import PermissionDenied from django.db import connection, models, transaction @@ -137,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 7146828d2..ad5633854 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -16,8 +16,6 @@ automatically. router.register(r'users', UserViewSet, 'user') urlpatterns = router.urls """ -from __future__ import unicode_literals - from collections import OrderedDict from functools import update_wrapper from inspect import getmembers @@ -34,7 +32,7 @@ def _is_extra_action(attr): return hasattr(attr, 'mapping') -class ViewSetMixin(object): +class ViewSetMixin: """ This is the magic. @@ -134,7 +132,7 @@ class ViewSetMixin(object): """ 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/runtests.py b/runtests.py index 16b47ce2a..a32dde96c 100755 --- a/runtests.py +++ b/runtests.py @@ -1,6 +1,4 @@ -#! /usr/bin/env python -from __future__ import print_function - +#! /usr/bin/env python3 import subprocess import sys diff --git a/setup.cfg b/setup.cfg index c95134600..b4dee6804 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [metadata] license_file = LICENSE.md diff --git a/setup.py b/setup.py index cb850a3ae..632c7dfd3 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 import os import re import shutil @@ -8,6 +7,34 @@ from io import open from setuptools import find_packages, setup +CURRENT_PYTHON = sys.version_info[:2] +REQUIRED_PYTHON = (3, 4) + +# This check and everything above must remain compatible with Python 2.7. +if CURRENT_PYTHON < REQUIRED_PYTHON: + sys.stderr.write(""" +========================== +Unsupported Python version +========================== + +This version of Django REST Framework requires Python {}.{}, but you're trying +to install it on Python {}.{}. + +This may be because you are using a version of pip that doesn't +understand the python_requires classifier. Make sure you +have pip >= 9.0 and setuptools >= 24.2, then try again: + + $ python -m pip install --upgrade pip setuptools + $ python -m pip install djangorestframework + +This will install the latest version of Django REST Framework which works on +your version of Python. If you can't upgrade your pip (or Python), request +an older version of Django REST Framework: + + $ python -m pip install "django<3.10" +""".format(*(REQUIRED_PYTHON + CURRENT_PYTHON))) + sys.exit(1) + def read(f): return open(f, 'r', encoding='utf-8').read() @@ -52,7 +79,7 @@ setup( packages=find_packages(exclude=['tests*']), include_package_data=True, install_requires=[], - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", + python_requires=">=3.4", zip_safe=False, classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -66,13 +93,12 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - '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 :: Only', 'Topic :: Internet :: WWW/HTTP', ] ) diff --git a/tests/authentication/migrations/0001_initial.py b/tests/authentication/migrations/0001_initial.py index cfc887240..548b3576b 100644 --- a/tests/authentication/migrations/0001_initial.py +++ b/tests/authentication/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models diff --git a/tests/authentication/models.py b/tests/authentication/models.py index b8d1fd5a6..1a721de4d 100644 --- a/tests/authentication/models.py +++ b/tests/authentication/models.py @@ -1,6 +1,3 @@ -# coding: utf-8 -from __future__ import unicode_literals - from django.conf import settings from django.db import models diff --git a/tests/authentication/test_authentication.py b/tests/authentication/test_authentication.py index 793773542..f7e9fcf18 100644 --- a/tests/authentication/test_authentication.py +++ b/tests/authentication/test_authentication.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals - import base64 import pytest @@ -10,7 +6,6 @@ from django.conf.urls import include, url from django.contrib.auth.models import User from django.http import HttpResponse from django.test import TestCase, override_settings -from django.utils import six from rest_framework import ( HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status @@ -253,7 +248,7 @@ class SessionAuthTests(TestCase): assert response.status_code == status.HTTP_403_FORBIDDEN -class BaseTokenAuthTests(object): +class BaseTokenAuthTests: """Token authentication""" model = None path = None @@ -381,7 +376,7 @@ class TokenAuthTests(BaseTokenAuthTests, TestCase): """Ensure generate_key returns a string""" token = self.model() key = token.generate_key() - assert isinstance(key, six.string_types) + assert isinstance(key, str) def test_token_login_json(self): """Ensure token login view using JSON POST works.""" @@ -534,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/browsable_api/auth_urls.py b/tests/browsable_api/auth_urls.py index 0e9379717..7530c5e40 100644 --- a/tests/browsable_api/auth_urls.py +++ b/tests/browsable_api/auth_urls.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf.urls import include, url from .views import MockView diff --git a/tests/browsable_api/no_auth_urls.py b/tests/browsable_api/no_auth_urls.py index 5fc95c727..348bfe1c0 100644 --- a/tests/browsable_api/no_auth_urls.py +++ b/tests/browsable_api/no_auth_urls.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf.urls import url from .views import MockView diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py index 684d7ae14..81090e223 100644 --- a/tests/browsable_api/test_browsable_api.py +++ b/tests/browsable_api/test_browsable_api.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.contrib.auth.models import User from django.test import TestCase, override_settings diff --git a/tests/browsable_api/test_browsable_nested_api.py b/tests/browsable_api/test_browsable_nested_api.py index 8f38b3c4e..3fef74023 100644 --- a/tests/browsable_api/test_browsable_nested_api.py +++ b/tests/browsable_api/test_browsable_nested_api.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf.urls import url from django.test import TestCase from django.test.utils import override_settings diff --git a/tests/browsable_api/views.py b/tests/browsable_api/views.py index 03758f10b..e1cf13a1e 100644 --- a/tests/browsable_api/views.py +++ b/tests/browsable_api/views.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from rest_framework import authentication, renderers from rest_framework.response import Response from rest_framework.views import APIView diff --git a/tests/generic_relations/models.py b/tests/generic_relations/models.py index 55bc243cb..20df3e4a2 100644 --- a/tests/generic_relations/models.py +++ b/tests/generic_relations/models.py @@ -1,14 +1,10 @@ -from __future__ import unicode_literals - from django.contrib.contenttypes.fields import ( GenericForeignKey, GenericRelation ) from django.contrib.contenttypes.models import ContentType from django.db import models -from django.utils.encoding import python_2_unicode_compatible -@python_2_unicode_compatible class Tag(models.Model): """ Tags have a descriptive slug, and are attached to an arbitrary object. @@ -22,7 +18,6 @@ class Tag(models.Model): return self.tag -@python_2_unicode_compatible class Bookmark(models.Model): """ A URL bookmark that may have multiple tags attached. @@ -34,7 +29,6 @@ class Bookmark(models.Model): return 'Bookmark: %s' % self.url -@python_2_unicode_compatible class Note(models.Model): """ A textual note that may have multiple tags attached. diff --git a/tests/generic_relations/test_generic_relations.py b/tests/generic_relations/test_generic_relations.py index c8de332e1..33f8ea1d0 100644 --- a/tests/generic_relations/test_generic_relations.py +++ b/tests/generic_relations/test_generic_relations.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.test import TestCase from rest_framework import serializers diff --git a/tests/models.py b/tests/models.py index 17bf23cda..f389a51a9 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import uuid from django.db import models diff --git a/tests/test_api_client.py b/tests/test_api_client.py index e4354ec60..74a3579e2 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os import tempfile import unittest diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index bddd480a5..de04d2c06 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import unittest from django.conf.urls import url @@ -38,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_authtoken.py b/tests/test_authtoken.py index c8957f978..036e317ef 100644 --- a/tests/test_authtoken.py +++ b/tests/test_authtoken.py @@ -1,9 +1,10 @@ +from io import StringIO + import pytest from django.contrib.admin import site from django.contrib.auth.models import User from django.core.management import CommandError, call_command from django.test import TestCase -from django.utils.six import StringIO from rest_framework.authtoken.admin import TokenAdmin from rest_framework.authtoken.management.commands.drf_create_token import \ diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py index e588ae623..dc5ab542f 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_decorators.py b/tests/test_decorators.py index 13dd41ff3..3f24e7ef0 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import pytest from django.test import TestCase diff --git a/tests/test_description.py b/tests/test_description.py index 702e56332..ae00fe4a9 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -1,9 +1,4 @@ -# -- coding: utf-8 -- - -from __future__ import unicode_literals - from django.test import TestCase -from django.utils.encoding import python_2_unicode_compatible from rest_framework.compat import apply_markdown from rest_framework.utils.formatting import dedent @@ -157,8 +152,8 @@ class TestViewNamesAndDescriptions(TestCase): """ # use a mock object instead of gettext_lazy to ensure that we can't end # up with a test case string in our l10n catalog - @python_2_unicode_compatible - class MockLazyStr(object): + + class MockLazyStr: def __init__(self, string): self.s = string diff --git a/tests/test_encoders.py b/tests/test_encoders.py index 12eca8105..c66954b80 100644 --- a/tests/test_encoders.py +++ b/tests/test_encoders.py @@ -10,7 +10,7 @@ from rest_framework.compat import coreapi from rest_framework.utils.encoders import JSONEncoder -class MockList(object): +class MockList: def tolist(self): return [1, 2, 3] diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index ce0ed8514..13b1b4757 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.test import RequestFactory, TestCase -from django.utils import six, translation +from django.utils import translation from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import ( @@ -46,12 +43,12 @@ class ExceptionTestCase(TestCase): exception = Throttled(wait=2) assert exception.get_full_details() == { - 'message': 'Request was throttled. Expected available in {} seconds.'.format(2 if six.PY3 else 2.), + 'message': 'Request was throttled. Expected available in {} seconds.'.format(2), 'code': 'throttled'} exception = Throttled(wait=2, detail='Slow down!') assert exception.get_full_details() == { - 'message': 'Slow down! Expected available in {} seconds.'.format(2 if six.PY3 else 2.), + 'message': 'Slow down! Expected available in {} seconds.'.format(2), 'code': 'throttled'} @@ -92,7 +89,7 @@ class TranslationTests(TestCase): def test_message(self): # this test largely acts as a sanity test to ensure the translation files are present. self.assertEqual(_('A server error occurred.'), 'Une erreur du serveur est survenue.') - self.assertEqual(six.text_type(APIException()), 'Une erreur du serveur est survenue.') + self.assertEqual(str(APIException()), 'Une erreur du serveur est survenue.') def test_server_error(): diff --git a/tests/test_fields.py b/tests/test_fields.py index 42adedfed..e0833564b 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -10,7 +10,6 @@ import pytz from django.core.exceptions import ValidationError as DjangoValidationError from django.http import QueryDict from django.test import TestCase, override_settings -from django.utils import six from django.utils.timezone import activate, deactivate, override, utc import rest_framework @@ -167,7 +166,7 @@ class TestEmpty: """ field = serializers.IntegerField(default=123) output = field.run_validation() - assert output is 123 + assert output == 123 class TestSource: @@ -193,7 +192,7 @@ class TestSource: class ExampleSerializer(serializers.Serializer): example_field = serializers.CharField(source='example_callable') - class ExampleInstance(object): + class ExampleInstance: def example_callable(self): return 'example callable value' @@ -204,7 +203,7 @@ class TestSource: class ExampleSerializer(serializers.Serializer): example_field = serializers.CharField(source='example_callable', read_only=True) - class ExampleInstance(object): + class ExampleInstance: def example_callable(self): raise AttributeError('method call failed') @@ -754,7 +753,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) @@ -822,7 +821,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 @@ -1148,7 +1147,7 @@ class TestLocalizedDecimalField(TestCase): def test_localize_forces_coerce_to_string(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, coerce_to_string=False, localize=True) - assert isinstance(field.to_representation(Decimal('1.1')), six.string_types) + assert isinstance(field.to_representation(Decimal('1.1')), str) class TestQuantizedValueForDecimal(TestCase): @@ -1219,7 +1218,7 @@ class TestDateField(FieldValues): outputs = { datetime.date(2001, 1, 1): '2001-01-01', '2001-01-01': '2001-01-01', - six.text_type('2016-01-10'): '2016-01-10', + str('2016-01-10'): '2016-01-10', None: None, '': None, } @@ -1286,7 +1285,7 @@ class TestDateTimeField(FieldValues): datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00Z', datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): '2001-01-01T13:00:00Z', '2001-01-01T00:00:00': '2001-01-01T00:00:00', - six.text_type('2016-01-10T00:00:00'): '2016-01-10T00:00:00', + str('2016-01-10T00:00:00'): '2016-01-10T00:00:00', None: None, '': None, } @@ -1628,7 +1627,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 a53fa192a..a52f40103 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,6 +1,5 @@ -from __future__ import unicode_literals - import datetime +from importlib import reload as reload_module import pytest from django.core.exceptions import ImproperlyConfigured @@ -8,7 +7,6 @@ from django.db import models from django.db.models.functions import Concat, Upper from django.test import TestCase from django.test.utils import override_settings -from django.utils.six.moves import reload_module from rest_framework import filters, generics, serializers from rest_framework.compat import coreschema @@ -163,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_generateschema.py b/tests/test_generateschema.py index 915c6ea05..a6a1f2bed 100644 --- a/tests/test_generateschema.py +++ b/tests/test_generateschema.py @@ -1,11 +1,10 @@ -from __future__ import unicode_literals +import io import pytest from django.conf.urls import url from django.core.management import call_command from django.test import TestCase from django.test.utils import override_settings -from django.utils import six from rest_framework.compat import coreapi from rest_framework.utils import formatting, json @@ -28,9 +27,8 @@ class GenerateSchemaTests(TestCase): """Tests for management command generateschema.""" def setUp(self): - self.out = six.StringIO() + self.out = io.StringIO() - @pytest.mark.skipif(six.PY2, reason='PyYAML unicode output is malformed on PY2.') def test_renders_default_schema_with_custom_title_url_and_description(self): expected_out = """info: description: Sample description diff --git a/tests/test_generics.py b/tests/test_generics.py index c0ff1c5c4..f41ebe6da 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -1,11 +1,8 @@ -from __future__ import unicode_literals - import pytest from django.db import models from django.http import Http404 from django.shortcuts import get_object_or_404 from django.test import TestCase -from django.utils import six from rest_framework import generics, renderers, serializers, status from rest_framework.response import Response @@ -245,7 +242,7 @@ class TestInstanceView(TestCase): with self.assertNumQueries(2): response = self.view(request, pk=1).render() assert response.status_code == status.HTTP_204_NO_CONTENT - assert response.content == six.b('') + assert response.content == b'' ids = [obj.id for obj in self.objects.all()] assert ids == [2, 3] @@ -291,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 @@ -446,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') @@ -653,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_htmlrenderer.py b/tests/test_htmlrenderer.py index decd25a3f..e31a9ced5 100644 --- a/tests/test_htmlrenderer.py +++ b/tests/test_htmlrenderer.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import django.template.loader import pytest from django.conf.urls import url @@ -7,7 +5,6 @@ from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.http import Http404 from django.template import TemplateDoesNotExist, engines from django.test import TestCase, override_settings -from django.utils import six from rest_framework import status from rest_framework.decorators import api_view, renderer_classes @@ -47,7 +44,7 @@ urlpatterns = [ @override_settings(ROOT_URLCONF='tests.test_htmlrenderer') class TemplateHTMLRendererTests(TestCase): def setUp(self): - class MockResponse(object): + class MockResponse: template_name = None self.mock_response = MockResponse() self._monkey_patch_get_template() @@ -85,13 +82,13 @@ class TemplateHTMLRendererTests(TestCase): def test_not_found_html_view(self): response = self.client.get('/not_found') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response.content, six.b("404 Not Found")) + self.assertEqual(response.content, b"404 Not Found") self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_permission_denied_html_view(self): response = self.client.get('/permission_denied') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.content, six.b("403 Forbidden")) + self.assertEqual(response.content, b"403 Forbidden") self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') # 2 tests below are based on order of if statements in corresponding method @@ -105,14 +102,14 @@ class TemplateHTMLRendererTests(TestCase): def test_get_template_names_returns_view_template_name(self): renderer = TemplateHTMLRenderer() - class MockResponse(object): + class MockResponse: template_name = None - class MockView(object): + class MockView: def get_template_names(self): return ['template from get_template_names method'] - class MockView2(object): + class MockView2: template_name = 'template from template_name attribute' template_name = renderer.get_template_names(self.mock_response, @@ -156,12 +153,11 @@ class TemplateHTMLRendererExceptionTests(TestCase): response = self.client.get('/not_found') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertTrue(response.content in ( - six.b("404: Not found"), six.b("404 Not Found"))) + b"404: Not found", b"404 Not Found")) self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_permission_denied_html_view_with_template(self): response = self.client.get('/permission_denied') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertTrue(response.content in ( - six.b("403: Permission denied"), six.b("403 Forbidden"))) + self.assertTrue(response.content in (b"403: Permission denied", b"403 Forbidden")) self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') diff --git a/tests/test_metadata.py b/tests/test_metadata.py index fe4ea4b42..e1a1fd352 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import pytest from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 9df7d8e3e..28a5e558a 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -22,7 +22,7 @@ urlpatterns = [ ] -class RequestUserMiddleware(object): +class RequestUserMiddleware: def __init__(self, get_response): self.get_response = get_response @@ -34,7 +34,7 @@ class RequestUserMiddleware(object): return response -class RequestPOSTMiddleware(object): +class RequestPOSTMiddleware: def __init__(self, get_response): self.get_response = get_response diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 898c859a4..413d7885d 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -5,8 +5,6 @@ shortcuts for automatically creating serializers based on a given model class. These tests deal with ensuring that we correctly map the model fields onto an appropriate set of serializer fields for each case. """ -from __future__ import unicode_literals - import datetime import decimal import sys @@ -20,10 +18,9 @@ from django.core.validators import ( ) from django.db import models from django.test import TestCase -from django.utils import six from rest_framework import serializers -from rest_framework.compat import postgres_fields, unicode_repr +from rest_framework.compat import postgres_fields from .models import NestedForeignKeySource @@ -193,7 +190,7 @@ class TestRegularFieldMappings(TestCase): file_path_field = FilePathField(path='/tmp/') """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_field_options(self): class TestSerializer(serializers.ModelSerializer): @@ -212,14 +209,7 @@ class TestRegularFieldMappings(TestCase): descriptive_field = IntegerField(help_text='Some help text', label='A label') choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) """) - if six.PY2: - # This particular case is too awkward to resolve fully across - # both py2 and py3. - expected = expected.replace( - "('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')", - "(u'red', u'Red'), (u'blue', u'Blue'), (u'green', u'Green')" - ) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) # merge this into test_regular_fields / RegularFieldsModel when # Django 2.1 is the minimum supported version @@ -238,7 +228,7 @@ class TestRegularFieldMappings(TestCase): field = BooleanField(allow_null=True, required=False) """) - self.assertEqual(unicode_repr(NullableBooleanSerializer()), expected) + self.assertEqual(repr(NullableBooleanSerializer()), expected) def test_method_field(self): """ @@ -382,7 +372,7 @@ class TestDurationFieldMapping(TestCase): id = IntegerField(label='ID', read_only=True) duration_field = DurationField() """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_duration_field_with_validators(self): class ValidatedDurationFieldModel(models.Model): @@ -407,7 +397,7 @@ class TestDurationFieldMapping(TestCase): id = IntegerField(label='ID', read_only=True) duration_field = DurationField(max_value=datetime.timedelta(days=3), min_value=datetime.timedelta(days=1)) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) class TestGenericIPAddressFieldValidation(TestCase): @@ -424,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') @@ -442,7 +432,7 @@ class TestPosgresFieldsMapping(TestCase): TestSerializer(): hstore_field = HStoreField() """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_array_field(self): class ArrayFieldModel(models.Model): @@ -457,7 +447,7 @@ class TestPosgresFieldsMapping(TestCase): TestSerializer(): array_field = ListField(child=CharField(label='Array field', validators=[])) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_json_field(self): class JSONFieldModel(models.Model): @@ -472,7 +462,7 @@ class TestPosgresFieldsMapping(TestCase): TestSerializer(): json_field = JSONField(style={'base_template': 'textarea.html'}) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) # Tests for relational field mappings. @@ -530,7 +520,7 @@ class TestRelationalFieldMappings(TestCase): many_to_many = PrimaryKeyRelatedField(allow_empty=False, many=True, queryset=ManyToManyTargetModel.objects.all()) through = PrimaryKeyRelatedField(many=True, read_only=True) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_nested_relations(self): class TestSerializer(serializers.ModelSerializer): @@ -555,7 +545,7 @@ class TestRelationalFieldMappings(TestCase): id = IntegerField(label='ID', read_only=True) name = CharField(max_length=100) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_hyperlinked_relations(self): class TestSerializer(serializers.HyperlinkedModelSerializer): @@ -571,7 +561,7 @@ class TestRelationalFieldMappings(TestCase): many_to_many = HyperlinkedRelatedField(allow_empty=False, many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail') through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail') """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_nested_hyperlinked_relations(self): class TestSerializer(serializers.HyperlinkedModelSerializer): @@ -596,7 +586,7 @@ class TestRelationalFieldMappings(TestCase): url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail') name = CharField(max_length=100) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_nested_hyperlinked_relations_starred_source(self): class TestSerializer(serializers.HyperlinkedModelSerializer): @@ -627,7 +617,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) """) self.maxDiff = None - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_nested_unique_together_relations(self): class TestSerializer(serializers.HyperlinkedModelSerializer): @@ -646,14 +636,7 @@ class TestRelationalFieldMappings(TestCase): url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail') name = CharField(max_length=100) """) - if six.PY2: - # This case is also too awkward to resolve fully across both py2 - # and py3. (See above) - expected = expected.replace( - "('foreign_key', 'one_to_one')", - "(u'foreign_key', u'one_to_one')" - ) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_pk_reverse_foreign_key(self): class TestSerializer(serializers.ModelSerializer): @@ -667,7 +650,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all()) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_pk_reverse_one_to_one(self): class TestSerializer(serializers.ModelSerializer): @@ -681,7 +664,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all()) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_pk_reverse_many_to_many(self): class TestSerializer(serializers.ModelSerializer): @@ -695,7 +678,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all()) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) def test_pk_reverse_through(self): class TestSerializer(serializers.ModelSerializer): @@ -709,7 +692,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_through = PrimaryKeyRelatedField(many=True, read_only=True) """) - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) class DisplayValueTargetModel(models.Model): @@ -1078,9 +1061,9 @@ class TestMetaInheritance(TestCase): char_field = CharField(max_length=100) non_model_field = CharField() """) - self.assertEqual(unicode_repr(ChildSerializer()), child_expected) - self.assertEqual(unicode_repr(TestSerializer()), test_expected) - self.assertEqual(unicode_repr(ChildSerializer()), child_expected) + self.assertEqual(repr(ChildSerializer()), child_expected) + self.assertEqual(repr(TestSerializer()), test_expected) + self.assertEqual(repr(ChildSerializer()), child_expected) class OneToOneTargetTestModel(models.Model): @@ -1149,14 +1132,14 @@ class Issue3674Test(TestCase): title = CharField(max_length=64) children = PrimaryKeyRelatedField(many=True, queryset=TestChildModel.objects.all()) """) - self.assertEqual(unicode_repr(TestParentModelSerializer()), parent_expected) + self.assertEqual(repr(TestParentModelSerializer()), parent_expected) child_expected = dedent(""" TestChildModelSerializer(): value = CharField(max_length=64, validators=[]) parent = PrimaryKeyRelatedField(queryset=TestParentModel.objects.all()) """) - self.assertEqual(unicode_repr(TestChildModelSerializer()), child_expected) + self.assertEqual(repr(TestChildModelSerializer()), child_expected) def test_nonID_PK_foreignkey_model_serializer(self): @@ -1248,7 +1231,7 @@ class TestFieldSource(TestCase): number_field = IntegerField(source='integer_field') """) self.maxDiff = None - self.assertEqual(unicode_repr(TestSerializer()), expected) + self.assertEqual(repr(TestSerializer()), expected) class Issue6110TestModel(models.Model): diff --git a/tests/test_multitable_inheritance.py b/tests/test_multitable_inheritance.py index 2ddd37ebb..1e8ab3448 100644 --- a/tests/test_multitable_inheritance.py +++ b/tests/test_multitable_inheritance.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.db import models from django.test import TestCase diff --git a/tests/test_negotiation.py b/tests/test_negotiation.py index 7ce3f92a9..089a86c62 100644 --- a/tests/test_negotiation.py +++ b/tests/test_negotiation.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import pytest from django.http import Http404 from django.test import TestCase @@ -80,7 +78,7 @@ class TestAcceptedMediaType(TestCase): assert str(mediatype) == 'test/*; foo=bar' def test_raise_error_if_no_suitable_renderers_found(self): - class MockRenderer(object): + class MockRenderer: format = 'xml' renderers = [MockRenderer()] with pytest.raises(Http404): diff --git a/tests/test_one_to_one_with_inheritance.py b/tests/test_one_to_one_with_inheritance.py index 789c7fcb9..40793d7ca 100644 --- a/tests/test_one_to_one_with_inheritance.py +++ b/tests/test_one_to_one_with_inheritance.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.db import models from django.test import TestCase diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 6d940fe2b..3c581ddfb 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -1,11 +1,7 @@ -# coding: utf-8 -from __future__ import unicode_literals - import pytest from django.core.paginator import Paginator as DjangoPaginator from django.db import models from django.test import TestCase -from django.utils import six from rest_framework import ( exceptions, filters, generics, pagination, serializers, status @@ -208,7 +204,7 @@ class TestPageNumberPagination: ] } assert self.pagination.display_page_controls - assert isinstance(self.pagination.to_html(), six.text_type) + assert isinstance(self.pagination.to_html(), str) def test_second_page(self): request = Request(factory.get('/', {'page': 2})) @@ -314,7 +310,7 @@ class TestPageNumberPaginationOverride: ] } assert not self.pagination.display_page_controls - assert isinstance(self.pagination.to_html(), six.text_type) + assert isinstance(self.pagination.to_html(), str) def test_invalid_page(self): request = Request(factory.get('/', {'page': 'invalid'})) @@ -369,7 +365,7 @@ class TestLimitOffset: ] } assert self.pagination.display_page_controls - assert isinstance(self.pagination.to_html(), six.text_type) + assert isinstance(self.pagination.to_html(), str) def test_pagination_not_applied_if_limit_or_default_limit_not_set(self): class MockPagination(pagination.LimitOffsetPagination): @@ -503,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 @@ -516,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 @@ -532,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 @@ -632,7 +628,7 @@ class CursorPaginationTestsMixin: assert current == [1, 1, 1, 1, 1] assert next == [1, 2, 3, 4, 4] - assert isinstance(self.pagination.to_html(), six.text_type) + assert isinstance(self.pagination.to_html(), str) def test_cursor_pagination_with_page_size(self): (previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=20') @@ -799,11 +795,11 @@ class TestCursorPagination(CursorPaginationTestsMixin): """ def setup(self): - class MockObject(object): + class MockObject: def __init__(self, idx): self.created = idx - class MockQuerySet(object): + class MockQuerySet: def __init__(self, items): self.items = items diff --git a/tests/test_parsers.py b/tests/test_parsers.py index e793948e3..7cf0c938a 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import io import math @@ -11,7 +8,6 @@ from django.core.files.uploadhandler import ( ) from django.http.request import RawPostDataException from django.test import TestCase -from django.utils.six import StringIO from rest_framework.exceptions import ParseError from rest_framework.parsers import ( @@ -34,7 +30,7 @@ class TestFormParser(TestCase): """ Make sure the `QueryDict` works OK """ parser = FormParser() - stream = StringIO(self.string) + stream = io.StringIO(self.string) data = parser.parse(stream) assert Form(data).is_valid() is True @@ -42,7 +38,7 @@ class TestFormParser(TestCase): class TestFileUploadParser(TestCase): def setUp(self): - class MockRequest(object): + class MockRequest: pass self.stream = io.BytesIO( "Test text file".encode('utf-8') diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 2fabdfa05..9c9300694 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -1,8 +1,7 @@ -from __future__ import unicode_literals - import base64 import unittest import warnings +from unittest import mock import django import pytest @@ -15,7 +14,7 @@ from rest_framework import ( HTTP_HEADER_ENCODING, RemovedInDRF310Warning, authentication, generics, permissions, serializers, status, views ) -from rest_framework.compat import PY36, is_guardian_installed, mock +from rest_framework.compat import PY36, is_guardian_installed from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.routers import DefaultRouter from rest_framework.test import APIRequestFactory @@ -331,14 +330,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 3c4b7d90b..3281b7ea2 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -26,7 +26,7 @@ class TestStringRelatedField(APISimpleTestCase): assert representation == '' -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_hyperlink.py b/tests/test_relations_hyperlink.py index 887a6f423..5ad0e31ff 100644 --- a/tests/test_relations_hyperlink.py +++ b/tests/test_relations_hyperlink.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf.urls import url from django.test import TestCase, override_settings diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 2cffb62e6..0da9da890 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - from django.test import TestCase -from django.utils import six from rest_framework import serializers from tests.models import ( @@ -263,7 +260,7 @@ class PKForeignKeyTests(TestCase): instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) assert not serializer.is_valid() - assert serializer.errors == {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]} + assert serializer.errors == {'target': ['Incorrect type. Expected pk value, received str.']} def test_reverse_foreign_key_update(self): data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]} @@ -562,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_renderers.py b/tests/test_renderers.py index 60a0c0307..54d1cb231 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import re from collections import OrderedDict @@ -11,7 +8,6 @@ from django.db import models from django.http.request import HttpRequest from django.template import loader from django.test import TestCase, override_settings -from django.utils import six from django.utils.safestring import SafeText from django.utils.translation import ugettext_lazy as _ @@ -175,7 +171,7 @@ class RendererEndToEndTests(TestCase): resp = self.client.head('/') self.assertEqual(resp.status_code, DUMMYSTATUS) self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, six.b('')) + self.assertEqual(resp.content, b'') def test_default_renderer_serializes_content_on_accept_any(self): """If the Accept header is set to */* the default renderer should serialize the response.""" @@ -348,7 +344,7 @@ class JSONRendererTests(TestCase): self.assertEqual(data, {'key': 'string value', '2': 3}) def test_render_obj_with_getitem(self): - class DictLike(object): + class DictLike: def __init__(self): self._dict = {} @@ -647,7 +643,7 @@ class BrowsableAPIRendererTests(URLPatternsTestCase): assert self.renderer.get_description({}, status_code=403) == '' def test_get_filter_form_returns_none_if_data_is_not_list_instance(self): - class DummyView(object): + class DummyView: get_queryset = None filter_backends = None diff --git a/tests/test_request.py b/tests/test_request.py index 83d295a12..0f682deb0 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,8 +1,6 @@ """ Tests for content parsing, and form-overloaded content parsing. """ -from __future__ import unicode_literals - import os.path import tempfile @@ -15,7 +13,6 @@ from django.contrib.sessions.middleware import SessionMiddleware from django.core.files.uploadedfile import SimpleUploadedFile from django.http.request import RawPostDataException from django.test import TestCase, override_settings -from django.utils import six from rest_framework import status from rest_framework.authentication import SessionAuthentication @@ -82,7 +79,7 @@ class TestContentParsing(TestCase): Ensure request.data returns content for POST request with non-form content. """ - content = six.b('qwerty') + content = b'qwerty' content_type = 'text/plain' request = Request(factory.post('/', content, content_type=content_type)) request.parsers = (PlainTextParser(),) @@ -121,7 +118,7 @@ class TestContentParsing(TestCase): Ensure request.data returns content for PUT request with non-form content. """ - content = six.b('qwerty') + content = b'qwerty' content_type = 'text/plain' request = Request(factory.put('/', content, content_type=content_type)) request.parsers = (PlainTextParser(), ) @@ -235,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 @@ -249,10 +246,6 @@ class TestUserSetter(TestCase): with pytest.raises(WrappedAttributeError, match=expected): request.user - # python 2 hasattr fails for *any* exception, not just AttributeError - if six.PY2: - return - with pytest.raises(WrappedAttributeError, match=expected): hasattr(request, 'user') diff --git a/tests/test_requests_client.py b/tests/test_requests_client.py index 161429f73..59b388c5a 100644 --- a/tests/test_requests_client.py +++ b/tests/test_requests_client.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import unittest from django.conf.urls import url diff --git a/tests/test_response.py b/tests/test_response.py index e92bf54c1..d3a56d01b 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,8 +1,5 @@ -from __future__ import unicode_literals - from django.conf.urls import include, url from django.test import TestCase, override_settings -from django.utils import six from rest_framework import generics, routers, serializers, status, viewsets from rest_framework.parsers import JSONParser @@ -150,7 +147,7 @@ class RendererIntegrationTests(TestCase): resp = self.client.head('/') self.assertEqual(resp.status_code, DUMMYSTATUS) self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, six.b('')) + self.assertEqual(resp.content, b'') def test_default_renderer_serializes_content_on_accept_any(self): """If the Accept header is set to */* the default renderer should serialize the response.""" @@ -260,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): @@ -270,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_reverse.py b/tests/test_reverse.py index 145b1a54f..9ab1667c5 100644 --- a/tests/test_reverse.py +++ b/tests/test_reverse.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf.urls import url from django.test import TestCase, override_settings from django.urls import NoReverseMatch @@ -19,7 +17,7 @@ urlpatterns = [ ] -class MockVersioningScheme(object): +class MockVersioningScheme: def __init__(self, raise_error=False): self.raise_error = raise_error diff --git a/tests/test_routers.py b/tests/test_routers.py index cca2ea712..adcec8bd6 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import warnings from collections import namedtuple diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 3cb9e0cda..1aad5d1de 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -29,7 +29,7 @@ from .models import BasicModel, ForeignKeySource, ManyToManySource factory = APIRequestFactory() -class MockUser(object): +class MockUser: def is_authenticated(self): return True @@ -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 0f1e81965..8f4d9bf63 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1,16 +1,13 @@ -# coding: utf-8 -from __future__ import unicode_literals - import inspect import pickle import re -import unittest +from collections import ChainMap import pytest from django.db import models from rest_framework import exceptions, fields, relations, serializers -from rest_framework.compat import Mapping, unicode_repr +from rest_framework.compat import Mapping from rest_framework.fields import Field from .models import ( @@ -18,15 +15,9 @@ from .models import ( ) from .utils import MockObject -try: - from collections import ChainMap -except ImportError: - ChainMap = False - # Test serializer fields imports. # ------------------------------- - class TestFieldImports: def is_field(self, name, value): return ( @@ -130,7 +121,6 @@ class TestSerializer: assert not serializer.is_valid() assert serializer.errors == {'non_field_errors': ['No data provided']} - @unittest.skipUnless(ChainMap, 'requires python 3.3') def test_serialize_chainmap(self): data = ChainMap({'char': 'abc'}, {'integer': 123}) serializer = self.Serializer(data=data) @@ -160,7 +150,7 @@ class TestSerializer: to_internal_value() is expected to return a dict, but subclasses may return application specific type. """ - class Point(object): + class Point: def __init__(self, srid, x, y): self.srid = srid self.coords = (x, y) @@ -171,7 +161,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}) @@ -201,7 +191,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() @@ -397,7 +387,7 @@ class TestIncorrectlyConfigured: class TestUnicodeRepr: - def test_unicode_repr(self): + def test_repr(self): class ExampleSerializer(serializers.Serializer): example = serializers.CharField() @@ -406,7 +396,7 @@ class TestUnicodeRepr: self.example = '한국' def __repr__(self): - return unicode_repr(self.example) + return repr(self.example) instance = ExampleObject() serializer = ExampleSerializer(instance) @@ -609,7 +599,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_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py index d9e5d7978..0465578bb 100644 --- a/tests/test_serializer_bulk_update.py +++ b/tests/test_serializer_bulk_update.py @@ -1,10 +1,7 @@ """ Tests to cover bulk create and update using serializers. """ -from __future__ import unicode_literals - from django.test import TestCase -from django.utils import six from rest_framework import serializers @@ -87,8 +84,7 @@ class BulkCreateSerializerTests(TestCase): serializer = self.BookSerializer(data=data, many=True) assert serializer.is_valid() is False - text_type_string = six.text_type.__name__ - message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string + message = 'Invalid data. Expected a dictionary, but got str.' expected_errors = [ {'non_field_errors': [message]}, {'non_field_errors': [message]}, diff --git a/tests/test_settings.py b/tests/test_settings.py index 51e9751b2..b78125ff9 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.test import TestCase, override_settings from rest_framework.settings import APISettings, api_settings diff --git a/tests/test_status.py b/tests/test_status.py index 1cd6e229e..07d893bee 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.test import TestCase from rest_framework.status import ( diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index 45bfd4aeb..128160888 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -1,6 +1,3 @@ -# encoding: utf-8 -from __future__ import unicode_literals - import unittest from django.template import Context, Template @@ -225,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 @@ -340,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( @@ -358,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): @@ -396,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 @@ -444,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 @@ -502,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 @@ -553,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 @@ -622,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 @@ -630,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 7868f724c..8094bfd8d 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,6 +1,3 @@ -# encoding: utf-8 -from __future__ import unicode_literals - from io import BytesIO from django.conf.urls import url @@ -293,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_throttling.py b/tests/test_throttling.py index b220a33a6..b20b6a809 100644 --- a/tests/test_throttling.py +++ b/tests/test_throttling.py @@ -1,7 +1,6 @@ """ Tests for the throttling implementations in the permissions module. """ -from __future__ import unicode_literals import pytest from django.contrib.auth.models import User @@ -296,7 +295,7 @@ class ScopedRateThrottleTests(TestCase): assert response.status_code == 200 def test_get_cache_key_returns_correct_key_if_user_is_authenticated(self): - class DummyView(object): + class DummyView: throttle_scope = 'user' request = Request(HttpRequest()) diff --git a/tests/test_urlpatterns.py b/tests/test_urlpatterns.py index 59ba395d2..25cc0032e 100644 --- a/tests/test_urlpatterns.py +++ b/tests/test_urlpatterns.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import unittest from collections import namedtuple diff --git a/tests/test_utils.py b/tests/test_utils.py index 28b06b173..a6f8b9d16 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf.urls import url from django.test import TestCase, override_settings diff --git a/tests/test_validation.py b/tests/test_validation.py index 4132a7b00..6e00b48c2 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,11 +1,8 @@ -from __future__ import unicode_literals - import re from django.core.validators import MaxValueValidator, RegexValidator from django.db import models from django.test import TestCase -from django.utils import six from rest_framework import generics, serializers, status from rest_framework.test import APIRequestFactory @@ -112,7 +109,7 @@ class TestAvoidValidation(TestCase): assert not serializer.is_valid() assert serializer.errors == { 'non_field_errors': [ - 'Invalid data. Expected a dictionary, but got %s.' % six.text_type.__name__ + 'Invalid data. Expected a dictionary, but got str.', ] } @@ -151,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 4bbddb64b..fe31ba235 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 7e650e275..d4e269df3 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -319,9 +319,9 @@ class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase): ] def setUp(self): - super(TestHyperlinkedRelatedField, self).setUp() + super().setUp() - class MockQueryset(object): + class MockQueryset: def get(self, pk): return 'object %s' % pk diff --git a/tests/test_views.py b/tests/test_views.py index f0919e846..2648c9fb3 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import copy -import sys from django.test import TestCase @@ -14,10 +11,7 @@ from rest_framework.views import APIView factory = APIRequestFactory() -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' +JSON_ERROR = 'JSON parse error - Expecting value:' class BasicView(APIView): diff --git a/tests/utils.py b/tests/utils.py index 509e6a102..06e5b9abe 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,7 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.urls import NoReverseMatch -class MockObject(object): +class MockObject: def __init__(self, **kwargs): self._kwargs = kwargs for key, val in kwargs.items(): @@ -16,7 +16,7 @@ class MockObject(object): return '' % kwargs_str -class MockQueryset(object): +class MockQueryset: def __init__(self, iterable): self.items = iterable @@ -33,7 +33,7 @@ class MockQueryset(object): raise ObjectDoesNotExist() -class BadType(object): +class BadType: """ When used as a lookup with a `MockQueryset`, these objects will raise a `TypeError`, as occurs in Django when making diff --git a/tox.ini b/tox.ini index 5d7a4987e..fcd32f88a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - {py27,py34,py35,py36}-django111, + {py34,py35,py36}-django111, {py34,py35,py36,py37}-django20, {py35,py36,py37}-django21 {py35,py36,py37}-django22 @@ -44,7 +44,7 @@ deps = -rrequirements/requirements-optionals.txt [testenv:lint] -basepython = python2.7 +basepython = python3.7 commands = ./runtests.py --lintonly deps = -rrequirements/requirements-codestyle.txt @@ -52,6 +52,7 @@ deps = [testenv:docs] basepython = python2.7 +skip_install = true commands = mkdocs build deps = -rrequirements/requirements-testing.txt From ff86f09f74a0f60b657576abc8cf805c308a3974 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 30 Apr 2019 22:44:33 -0700 Subject: [PATCH 23/69] Remove unnecessary compatibility shims from rest_framework/compat.py (#6631) For Python 3, collections.abc.Mapping and collections.abc.MutableMapping are always available from the stdlib. --- rest_framework/compat.py | 1 - rest_framework/fields.py | 3 ++- rest_framework/serializers.py | 3 ++- rest_framework/utils/serializer_helpers.py | 2 +- tests/test_renderers.py | 3 ++- tests/test_serializer.py | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index aad44e342..3068665a8 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -3,7 +3,6 @@ The `compat` module provides support for backwards compatibility with older versions of Django/Python, and compatibility wrappers around optional packages. """ import sys -from collections.abc import Mapping, MutableMapping # noqa from django.conf import settings from django.core import validators diff --git a/rest_framework/fields.py b/rest_framework/fields.py index ad9611e05..1cffdcc2d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -6,6 +6,7 @@ import inspect import re import uuid from collections import OrderedDict +from collections.abc import Mapping from django.conf import settings from django.core.exceptions import ObjectDoesNotExist @@ -30,7 +31,7 @@ from pytz.exceptions import InvalidTimeError from rest_framework import ISO_8601 from rest_framework.compat import ( - Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator, + MaxLengthValidator, MaxValueValidator, MinLengthValidator, MinValueValidator, ProhibitNullCharactersValidator ) from rest_framework.exceptions import ErrorDetail, ValidationError diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 90b31e068..651ca81cf 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,6 +14,7 @@ import copy import inspect import traceback from collections import OrderedDict +from collections.abc import Mapping from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ValidationError as DjangoValidationError @@ -25,7 +26,7 @@ from django.utils import timezone from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import Mapping, postgres_fields +from rest_framework.compat import postgres_fields from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.fields import get_error_detail, set_value from rest_framework.settings import api_settings diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 8709352f1..80aea27d3 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,8 +1,8 @@ from collections import OrderedDict +from collections.abc import MutableMapping from django.utils.encoding import force_text -from rest_framework.compat import MutableMapping from rest_framework.utils import json diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 54d1cb231..bc775547d 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -1,5 +1,6 @@ import re from collections import OrderedDict +from collections.abc import MutableMapping import pytest from django.conf.urls import include, url @@ -12,7 +13,7 @@ from django.utils.safestring import SafeText from django.utils.translation import ugettext_lazy as _ from rest_framework import permissions, serializers, status -from rest_framework.compat import MutableMapping, coreapi +from rest_framework.compat import coreapi from rest_framework.decorators import action from rest_framework.renderers import ( AdminRenderer, BaseRenderer, BrowsableAPIRenderer, DocumentationRenderer, diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 8f4d9bf63..33cc0b60c 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -2,12 +2,12 @@ import inspect import pickle import re from collections import ChainMap +from collections.abc import Mapping import pytest from django.db import models from rest_framework import exceptions, fields, relations, serializers -from rest_framework.compat import Mapping from rest_framework.fields import Field from .models import ( From b4e80ac721958f8cc2931b0e2b4d022946f6ad88 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 30 Apr 2019 22:45:16 -0700 Subject: [PATCH 24/69] Remove unnecessary coerce to str() in test_decorators.py (#6637) Was added only for Python 2 compatibility. --- tests/test_decorators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 3f24e7ef0..bd30449e5 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -202,8 +202,7 @@ class ActionDecoratorTestCase(TestCase): def method(): raise NotImplementedError - # Python 2.x compatibility - cast __name__ to str - method.__name__ = str(name) + method.__name__ = name getattr(test_action.mapping, name)(method) # ensure the mapping returns the correct method name From 734ca7ca8c2ba6f0ca83ede015652720b2a7246d Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 30 Apr 2019 22:46:30 -0700 Subject: [PATCH 25/69] Remove unneeded repo() test (#6632) --- tests/test_serializer.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 33cc0b60c..e0acf368b 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -386,23 +386,6 @@ class TestIncorrectlyConfigured: ) -class TestUnicodeRepr: - def test_repr(self): - class ExampleSerializer(serializers.Serializer): - example = serializers.CharField() - - class ExampleObject: - def __init__(self): - self.example = '한국' - - def __repr__(self): - return repr(self.example) - - instance = ExampleObject() - serializer = ExampleSerializer(instance) - repr(serializer) # Should not error. - - class TestNotRequiredOutput: def test_not_required_output_for_dict(self): """ From 513a49d63b6332e373c89fb0737a0745c1f0a734 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 30 Apr 2019 22:49:17 -0700 Subject: [PATCH 26/69] Drop default 'utf-8' to .encode()/.decode() (#6633) A Python 3 cleanup that allows for less noise in the code. https://docs.python.org/3/library/stdtypes.html#bytes.decode https://docs.python.org/3/library/stdtypes.html#str.encode --- rest_framework/fields.py | 7 ++---- .../management/commands/generateschema.py | 2 +- rest_framework/parsers.py | 2 +- rest_framework/renderers.py | 23 +++++++------------ rest_framework/utils/encoders.py | 2 +- tests/authentication/test_authentication.py | 2 +- tests/browsable_api/test_browsable_api.py | 12 +++++----- .../test_browsable_nested_api.py | 2 +- tests/test_generics.py | 6 ++--- tests/test_parsers.py | 6 ++--- tests/test_renderers.py | 22 +++++++++--------- tests/test_routers.py | 10 ++++---- tests/test_templates.py | 4 ++-- 13 files changed, 44 insertions(+), 56 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1cffdcc2d..a41934ac1 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1754,7 +1754,7 @@ class JSONField(Field): try: if self.binary or getattr(data, 'is_json_string', False): if isinstance(data, bytes): - data = data.decode('utf-8') + data = data.decode() return json.loads(data) else: json.dumps(data) @@ -1765,10 +1765,7 @@ class JSONField(Field): def to_representation(self, value): if self.binary: value = json.dumps(value) - # On python 2.x the return type for json.dumps() is underspecified. - # On python 3.x json.dumps() returns unicode strings. - if isinstance(value, str): - value = bytes(value.encode('utf-8')) + value = value.encode() return value diff --git a/rest_framework/management/commands/generateschema.py b/rest_framework/management/commands/generateschema.py index 591073ba0..40909bd04 100644 --- a/rest_framework/management/commands/generateschema.py +++ b/rest_framework/management/commands/generateschema.py @@ -29,7 +29,7 @@ class Command(BaseCommand): renderer = self.get_renderer(options['format']) output = renderer.render(schema, renderer_context={}) - self.stdout.write(output.decode('utf-8')) + self.stdout.write(output.decode()) def get_renderer(self, format): renderer_cls = { diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 5b5e3f158..a48c31631 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -202,7 +202,7 @@ class FileUploadParser(BaseParser): try: meta = parser_context['request'].META - disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8')) + disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode()) filename_parm = disposition[1] if 'filename*' in filename_parm: return self.get_encoded_filename(filename_parm) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index eb5da008b..623702966 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -104,18 +104,11 @@ class JSONRenderer(BaseRenderer): allow_nan=not self.strict, separators=separators ) - # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True, - # but if ensure_ascii=False, the return type is underspecified, - # and may (or may not) be unicode. - # On python 3.x json.dumps() returns unicode strings. - if isinstance(ret, str): - # We always fully escape \u2028 and \u2029 to ensure we output JSON - # that is a strict javascript subset. If bytes were returned - # by json.dumps() then we don't have these characters in any case. - # See: http://timelessrepo.com/json-isnt-a-javascript-subset - ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029') - return bytes(ret.encode('utf-8')) - return ret + # We always fully escape \u2028 and \u2029 to ensure we output JSON + # that is a strict javascript subset. + # See: http://timelessrepo.com/json-isnt-a-javascript-subset + ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029') + return ret.encode() class TemplateHTMLRenderer(BaseRenderer): @@ -574,7 +567,7 @@ class BrowsableAPIRenderer(BaseRenderer): data.pop(name, None) content = renderer.render(data, accepted, context) # Renders returns bytes, but CharField expects a str. - content = content.decode('utf-8') + content = content.decode() else: content = None @@ -1032,7 +1025,7 @@ class OpenAPIRenderer(_BaseOpenAPIRenderer): def render(self, data, media_type=None, renderer_context=None): structure = self.get_structure(data) - return yaml.dump(structure, default_flow_style=False).encode('utf-8') + return yaml.dump(structure, default_flow_style=False).encode() class JSONOpenAPIRenderer(_BaseOpenAPIRenderer): @@ -1045,4 +1038,4 @@ class JSONOpenAPIRenderer(_BaseOpenAPIRenderer): def render(self, data, media_type=None, renderer_context=None): structure = self.get_structure(data) - return json.dumps(structure, indent=4).encode('utf-8') + return json.dumps(structure, indent=4).encode() diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index dee2f942e..a7875a868 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -47,7 +47,7 @@ class JSONEncoder(json.JSONEncoder): return tuple(obj) elif isinstance(obj, bytes): # Best-effort for binary blobs. See #4187. - return obj.decode('utf-8') + return obj.decode() elif hasattr(obj, 'tolist'): # Numpy arrays and array scalars. return obj.tolist() diff --git a/tests/authentication/test_authentication.py b/tests/authentication/test_authentication.py index f7e9fcf18..927989028 100644 --- a/tests/authentication/test_authentication.py +++ b/tests/authentication/test_authentication.py @@ -183,7 +183,7 @@ class SessionAuthTests(TestCase): cf. [#1810](https://github.com/encode/django-rest-framework/pull/1810) """ response = self.csrf_client.get('/auth/login/') - content = response.content.decode('utf8') + content = response.content.decode() assert '' in content def test_post_form_session_auth_failing_csrf(self): diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py index 81090e223..17644c2ac 100644 --- a/tests/browsable_api/test_browsable_api.py +++ b/tests/browsable_api/test_browsable_api.py @@ -24,18 +24,18 @@ class DropdownWithAuthTests(TestCase): def test_name_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - content = response.content.decode('utf8') + content = response.content.decode() assert 'john' in content def test_logout_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - content = response.content.decode('utf8') + content = response.content.decode() assert '>Log out<' in content def test_login_shown_when_logged_out(self): response = self.client.get('/') - content = response.content.decode('utf8') + content = response.content.decode() assert '>Log in<' in content @@ -59,16 +59,16 @@ class NoDropdownWithoutAuthTests(TestCase): def test_name_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - content = response.content.decode('utf8') + content = response.content.decode() assert 'john' in content def test_dropdown_not_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - content = response.content.decode('utf8') + content = response.content.decode() assert '