From f54a220d8f3c8649b84188c7e2a6dc2ef3be6f4c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 31 Jan 2019 11:36:40 +0100 Subject: [PATCH 092/185] Corrected coreapi CLI code example generation. (#6428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove “> “ when rendering template. Closes #6333. --- .../templates/rest_framework/docs/langs/shell.html | 2 +- tests/test_renderers.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/docs/langs/shell.html b/rest_framework/templates/rest_framework/docs/langs/shell.html index 24137e4ae..e5f2a0322 100644 --- a/rest_framework/templates/rest_framework/docs/langs/shell.html +++ b/rest_framework/templates/rest_framework/docs/langs/shell.html @@ -3,4 +3,4 @@ $ coreapi get {{ document.url }}{% if schema_format %} --format {{ schema_format }}{% endif %} # Interact with the API endpoint -$ coreapi action {% if section_key %}{{ section_key }} {% endif %}{{ link_key }}{% for field in link.fields %} -p {{ field.name }}=...{% endfor %}{% endcode %} +$ coreapi action {% if section_key %}{{ section_key }} {% endif %}{{ link_key|cut:"> " }}{% for field in link.fields %} -p {{ field.name }}=...{% endfor %}{% endcode %} diff --git a/tests/test_renderers.py b/tests/test_renderers.py index a68ece734..8518a3f7c 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -9,6 +9,7 @@ from django.conf.urls import include, url from django.core.cache import cache 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 @@ -827,6 +828,16 @@ class TestDocumentationRenderer(TestCase): html = renderer.render(document, accepted_media_type="text/html", renderer_context={"request": request}) assert '

Data Endpoint API

' in html + def test_shell_code_example_rendering(self): + template = loader.get_template('rest_framework/docs/langs/shell.html') + context = { + 'document': coreapi.Document(url='https://api.example.org/'), + 'link_key': 'testcases > list', + 'link': coreapi.Link(url='/data/', action='get', fields=[]), + } + html = template.render(context) + assert 'testcases list' in html + @pytest.mark.skipif(not coreapi, reason='coreapi is not installed') class TestSchemaJSRenderer(TestCase): From bd9a799e166b1d44c9db444a95765ad0dce2aa8f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 31 Jan 2019 15:28:01 +0100 Subject: [PATCH 093/185] Fixed SchemaView to reset renderer on exception. (#6429) Fixes #6258. --- rest_framework/schemas/views.py | 8 ++++++++ tests/test_schemas.py | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/rest_framework/schemas/views.py b/rest_framework/schemas/views.py index 845b68ea6..f5e327a94 100644 --- a/rest_framework/schemas/views.py +++ b/rest_framework/schemas/views.py @@ -31,3 +31,11 @@ class SchemaView(APIView): if schema is None: raise exceptions.PermissionDenied() return Response(schema) + + def handle_exception(self, exc): + # Schema renderers do not render exceptions, so re-perform content + # negotiation with default renderers. + 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) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 8e097f9f4..d3bd43073 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1304,3 +1304,13 @@ class TestAutoSchemaAllowsFilters(object): def test_FOO(self): assert not self._test('FOO') + + +@pytest.mark.skipif(not coreapi, reason='coreapi is not installed') +def test_schema_handles_exception(): + schema_view = get_schema_view(permission_classes=[DenyAllUsingPermissionDenied]) + request = factory.get('/') + response = schema_view(request) + response.render() + assert response.status_code == 403 + assert "You do not have permission to perform this action." in str(response.content) From 190f6201cbbac18803479020dae922c5ae3e90f2 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 31 Jan 2019 15:59:19 +0100 Subject: [PATCH 094/185] Update Django Guardian dependency. (#6430) * Update Django Guardian dependency. * Skip testing Guardian on PY2. See https://github.com/django-guardian/django-guardian/issues/602 --- requirements/requirements-optionals.txt | 2 +- rest_framework/compat.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index cd0f1f62b..c800a5891 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,7 +1,7 @@ # Optional packages which may be used with REST framework. psycopg2-binary==2.7.5 markdown==2.6.11 -django-guardian==1.4.9 +django-guardian==1.5.0 django-filter==1.1.0 coreapi==2.3.1 coreschema==0.0.4 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index fffc17938..5a4bcdf66 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -161,6 +161,10 @@ def is_guardian_installed(): """ django-guardian is optional and only imported if in INSTALLED_APPS. """ + if six.PY2: + # 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 From 2b62941bb4d48e27960690bdea1a343177dd5d0e Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 31 Jan 2019 21:50:36 +0600 Subject: [PATCH 095/185] Added testing against Django 2.2a1. (#6422) * Added testing against Django 2.2a1. * Allow failures for Django 2.2 --- .travis.yml | 4 ++++ tox.ini | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index c0373077e..c9febbdf9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,15 +12,18 @@ matrix: - { python: "3.5", env: DJANGO=1.11 } - { python: "3.5", env: DJANGO=2.0 } - { python: "3.5", env: DJANGO=2.1 } + - { python: "3.5", env: DJANGO=2.2 } - { python: "3.5", env: DJANGO=master } - { python: "3.6", env: DJANGO=1.11 } - { python: "3.6", env: DJANGO=2.0 } - { python: "3.6", env: DJANGO=2.1 } + - { python: "3.6", env: DJANGO=2.2 } - { python: "3.6", env: DJANGO=master } - { python: "3.7", env: DJANGO=2.0 } - { python: "3.7", env: DJANGO=2.1 } + - { python: "3.7", env: DJANGO=2.2 } - { python: "3.7", env: DJANGO=master } - { python: "3.7", env: TOXENV=base } @@ -37,6 +40,7 @@ matrix: allow_failures: - env: DJANGO=master + - env: DJANGO=2.2 install: - pip install tox tox-venv tox-travis diff --git a/tox.ini b/tox.ini index 968ec1ef1..65b941628 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = {py27,py34,py35,py36}-django111, {py34,py35,py36,py37}-django20, {py35,py36,py37}-django21 + {py35,py36,py37}-django22 {py35,py36,py37}-djangomaster, base,dist,lint,docs, @@ -11,6 +12,7 @@ DJANGO = 1.11: django111 2.0: django20 2.1: django21 + 2.2: django22 master: djangomaster [testenv] @@ -23,6 +25,7 @@ deps = django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 + django22: Django>=2.2a1,<3.0 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 63e352586b679d554e1e9ef708e153d858e7e3bf Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 31 Jan 2019 17:16:43 +0100 Subject: [PATCH 096/185] Drop testing Python 3.5 against Django master. (#6431) Not supported in Django 3.0. --- .travis.yml | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c9febbdf9..796ef0502 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ matrix: - { python: "3.5", env: DJANGO=2.0 } - { python: "3.5", env: DJANGO=2.1 } - { python: "3.5", env: DJANGO=2.2 } - - { python: "3.5", env: DJANGO=master } - { python: "3.6", env: DJANGO=1.11 } - { python: "3.6", env: DJANGO=2.0 } diff --git a/tox.ini b/tox.ini index 65b941628..cf6799a0a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = {py34,py35,py36,py37}-django20, {py35,py36,py37}-django21 {py35,py36,py37}-django22 - {py35,py36,py37}-djangomaster, + {py36,py37}-djangomaster, base,dist,lint,docs, [travis:env] From 7310411533873717305f08fcc5d45426cb0f01d9 Mon Sep 17 00:00:00 2001 From: Daniel Roseman Date: Fri, 1 Feb 2019 18:50:27 +0000 Subject: [PATCH 097/185] Updated example models to use `__str__` in relations docs. (#6433) --- docs/api-guide/relations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 8683347cb..8665e80f6 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -46,12 +46,12 @@ In order to explain the various types of relational fields, we'll use a couple o unique_together = ('album', 'order') ordering = ['order'] - def __unicode__(self): + def __str__(self): return '%d: %s' % (self.order, self.title) ## StringRelatedField -`StringRelatedField` may be used to represent the target of the relationship using its `__unicode__` method. +`StringRelatedField` may be used to represent the target of the relationship using its `__str__` method. For example, the following serializer. @@ -510,7 +510,7 @@ For example, given the following model for a tag, which has a generic relationsh object_id = models.PositiveIntegerField() tagged_object = GenericForeignKey('content_type', 'object_id') - def __unicode__(self): + def __str__(self): return self.tag_name And the following two models, which may have associated tags: From 7c6e34c14f54f1bf0d46ff34dc9a823215987c95 Mon Sep 17 00:00:00 2001 From: jhtimmins Date: Sat, 2 Feb 2019 05:49:58 -0800 Subject: [PATCH 098/185] Fix typo: 'what' to 'that' (#6437) --- docs/api-guide/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 35b976c66..28450f082 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -50,7 +50,7 @@ The request exposes some properties that allow you to determine the result of th ## .accepted_renderer -The renderer instance what was selected by the content negotiation stage. +The renderer instance that was selected by the content negotiation stage. ## .accepted_media_type From 3c5c61f33bd451496385aec237cb5502db8d6a6a Mon Sep 17 00:00:00 2001 From: carlfarrington <33500423+carlfarrington@users.noreply.github.com> Date: Wed, 6 Feb 2019 09:35:04 +0000 Subject: [PATCH 099/185] fix for a couple of missing words (#6444) --- docs/api-guide/permissions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 8a4cb63c6..e04b1199b 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -10,9 +10,9 @@ Together with [authentication] and [throttling], permissions determine whether a Permission checks are always run at the very start of the view, before any other code is allowed to proceed. Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted. -Permissions are used to grant or deny access different classes of users to different parts of the API. +Permissions are used to grant or deny access for different classes of users to different parts of the API. -The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds the `IsAuthenticated` class in REST framework. +The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds to the `IsAuthenticated` class in REST framework. A slightly less strict style of permission would be to allow full access to authenticated users, but allow read-only access to unauthenticated users. This corresponds to the `IsAuthenticatedOrReadOnly` class in REST framework. From abf07e672e94f2ce7d42e690222f527ba918ad40 Mon Sep 17 00:00:00 2001 From: Tanner Prestegard Date: Wed, 6 Feb 2019 14:26:09 -0600 Subject: [PATCH 100/185] Fix throttling documentation for specifying alternate caches (#6446) --- docs/api-guide/throttling.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index de66396a8..dade47460 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -82,8 +82,10 @@ The throttle classes provided by REST framework use Django's cache backend. You If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example: + from django.core.cache import caches + class CustomAnonRateThrottle(AnonRateThrottle): - cache = get_cache('alternate') + cache = caches['alternate'] You'll need to remember to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute. From dc6b3bf42e53c6bfdb597a67de931913c8bd0255 Mon Sep 17 00:00:00 2001 From: briwa Date: Thu, 7 Feb 2019 16:10:11 +0800 Subject: [PATCH 101/185] Fix tutorial instruction to also add pyyaml (#6443) --- docs/tutorial/7-schemas-and-client-libraries.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/7-schemas-and-client-libraries.md b/docs/tutorial/7-schemas-and-client-libraries.md index 412759834..203d81ea5 100644 --- a/docs/tutorial/7-schemas-and-client-libraries.md +++ b/docs/tutorial/7-schemas-and-client-libraries.md @@ -29,9 +29,10 @@ automatically generated schemas. Since we're using viewsets and routers, we can simply use the automatic schema generation. You'll need to install the `coreapi` python package in order to include an -API schema. +API schema, and `pyyaml` to render the schema into the commonly used +YAML-based OpenAPI format. - $ pip install coreapi + $ pip install coreapi pyyaml We can now include a schema for our API, by including an autogenerated schema view in our URL configuration. From 9f66fc9a7ca51ad5535cb48e199cf2fbe9eecec6 Mon Sep 17 00:00:00 2001 From: johnthagen Date: Wed, 13 Feb 2019 19:00:16 -0500 Subject: [PATCH 102/185] Fix typo in caching docs --- docs/api-guide/caching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index ff51aed06..5342345e4 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -13,7 +13,7 @@ provided in Django. Django provides a [`method_decorator`][decorator] to use decorators with class based views. This can be used with -with other cache decorators such as [`cache_page`][page] and +other cache decorators such as [`cache_page`][page] and [`vary_on_cookie`][cookie]. ```python From 3b996c6dc27c95749e1c3c2028e3d6adf3993052 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 12:01:36 +0100 Subject: [PATCH 103/185] Correct 3rd-party-packages link in issue template. Closes #6457 --- ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 8f2391d29..566bf9543 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -3,7 +3,7 @@ - [ ] I have verified that that issue exists against the `master` branch of Django REST framework. - [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate. - [ ] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.) -- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/topics/third-party-resources/#about-third-party-packages) where possible.) +- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.) - [ ] I have reduced the issue to the simplest possible case. - [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.) From 606dd492279a856fa1eaf3487c1e66b36840a9c3 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 08:48:36 +0100 Subject: [PATCH 104/185] Update tox to use Django 2.2b2. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cf6799a0a..4226f1a92 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.2a1,<3.0 + django22: Django>=2.2b1,<3.0 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From cb4cbb61f259b1fad659c714d64681d5add76c10 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 08:50:27 +0100 Subject: [PATCH 105/185] Fix search filter tests against Django 2.2. Django 2.2 enables foreign key constraint checking on SQLite. --- tests/test_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index a7d9a07c1..2d4eb132e 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -221,7 +221,7 @@ class SearchFilterM2MTests(TestCase): # ... for idx in range(3): label = 'w' * (idx + 1) - AttributeModel(label=label) + AttributeModel.objects.create(label=label) for idx in range(10): title = 'z' * (idx + 1) From 481ae69df3b59d628689b03bbf18c6002d11a073 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 09:30:53 +0100 Subject: [PATCH 106/185] Add migration for CustomToken test model. Move authentication tests to sub-app to enable this. --- tests/authentication/__init__.py | 0 .../authentication/migrations/0001_initial.py | 24 +++++++++++++++++++ tests/authentication/migrations/__init__.py | 0 tests/authentication/models.py | 10 ++++++++ .../test_authentication.py | 20 +++++++--------- tests/conftest.py | 1 + 6 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 tests/authentication/__init__.py create mode 100644 tests/authentication/migrations/0001_initial.py create mode 100644 tests/authentication/migrations/__init__.py create mode 100644 tests/authentication/models.py rename tests/{ => authentication}/test_authentication.py (97%) diff --git a/tests/authentication/__init__.py b/tests/authentication/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/authentication/migrations/0001_initial.py b/tests/authentication/migrations/0001_initial.py new file mode 100644 index 000000000..cfc887240 --- /dev/null +++ b/tests/authentication/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='CustomToken', + fields=[ + ('key', models.CharField(max_length=40, primary_key=True, serialize=False)), + ('user', models.OneToOneField(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/tests/authentication/migrations/__init__.py b/tests/authentication/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/authentication/models.py b/tests/authentication/models.py new file mode 100644 index 000000000..b8d1fd5a6 --- /dev/null +++ b/tests/authentication/models.py @@ -0,0 +1,10 @@ +# coding: utf-8 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import models + + +class CustomToken(models.Model): + key = models.CharField(max_length=40, primary_key=True) + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) diff --git a/tests/test_authentication.py b/tests/authentication/test_authentication.py similarity index 97% rename from tests/test_authentication.py rename to tests/authentication/test_authentication.py index f2714acb5..793773542 100644 --- a/tests/test_authentication.py +++ b/tests/authentication/test_authentication.py @@ -8,7 +8,6 @@ import pytest from django.conf import settings from django.conf.urls import include, url from django.contrib.auth.models import User -from django.db import models from django.http import HttpResponse from django.test import TestCase, override_settings from django.utils import six @@ -26,14 +25,11 @@ from rest_framework.response import Response from rest_framework.test import APIClient, APIRequestFactory from rest_framework.views import APIView +from .models import CustomToken + factory = APIRequestFactory() -class CustomToken(models.Model): - key = models.CharField(max_length=40, primary_key=True) - user = models.OneToOneField(User, on_delete=models.CASCADE) - - class CustomTokenAuthentication(TokenAuthentication): model = CustomToken @@ -87,7 +83,7 @@ urlpatterns = [ ] -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class BasicAuthTests(TestCase): """Basic authentication""" def setUp(self): @@ -169,7 +165,7 @@ class BasicAuthTests(TestCase): assert response.status_code == status.HTTP_401_UNAUTHORIZED -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class SessionAuthTests(TestCase): """User session authentication""" def setUp(self): @@ -370,7 +366,7 @@ class BaseTokenAuthTests(object): assert response.status_code == status.HTTP_401_UNAUTHORIZED -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class TokenAuthTests(BaseTokenAuthTests, TestCase): model = Token path = '/token/' @@ -429,13 +425,13 @@ class TokenAuthTests(BaseTokenAuthTests, TestCase): assert response.data['token'] == self.key -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class CustomTokenAuthTests(BaseTokenAuthTests, TestCase): model = CustomToken path = '/customtoken/' -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class CustomKeywordTokenAuthTests(BaseTokenAuthTests, TestCase): model = Token path = '/customkeywordtoken/' @@ -549,7 +545,7 @@ class BasicAuthenticationUnitTests(TestCase): authentication.authenticate = old_authenticate -@override_settings(ROOT_URLCONF='tests.test_authentication', +@override_settings(ROOT_URLCONF=__name__, AUTHENTICATION_BACKENDS=('django.contrib.auth.backends.RemoteUserBackend',)) class RemoteUserAuthenticationUnitTests(TestCase): def setUp(self): diff --git a/tests/conftest.py b/tests/conftest.py index 27558c02b..1c0c6dda7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,7 @@ def pytest_configure(config): 'django.contrib.staticfiles', 'rest_framework', 'rest_framework.authtoken', + 'tests.authentication', 'tests.importable', 'tests', ), From 59fcbc6dd553cbbbd60a3dd7c57fb72fde8e7f68 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 14:29:57 +0100 Subject: [PATCH 107/185] Add migration for generic relations Tag model. --- tests/conftest.py | 1 + tests/generic_relations/__init__.py | 0 .../migrations/0001_initial.py | 36 +++++++++++++++ .../generic_relations/migrations/__init__.py | 0 tests/generic_relations/models.py | 46 +++++++++++++++++++ .../test_generic_relations.py} | 44 +----------------- 6 files changed, 84 insertions(+), 43 deletions(-) create mode 100644 tests/generic_relations/__init__.py create mode 100644 tests/generic_relations/migrations/0001_initial.py create mode 100644 tests/generic_relations/migrations/__init__.py create mode 100644 tests/generic_relations/models.py rename tests/{test_relations_generic.py => generic_relations/test_generic_relations.py} (63%) diff --git a/tests/conftest.py b/tests/conftest.py index 1c0c6dda7..ac29e4a42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,6 +57,7 @@ def pytest_configure(config): 'rest_framework', 'rest_framework.authtoken', 'tests.authentication', + 'tests.generic_relations', 'tests.importable', 'tests', ), diff --git a/tests/generic_relations/__init__.py b/tests/generic_relations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/generic_relations/migrations/0001_initial.py b/tests/generic_relations/migrations/0001_initial.py new file mode 100644 index 000000000..ea04d8d67 --- /dev/null +++ b/tests/generic_relations/migrations/0001_initial.py @@ -0,0 +1,36 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Bookmark', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.URLField()), + ], + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag', models.SlugField()), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=models.CASCADE, to='contenttypes.ContentType')), + ], + ), + ] diff --git a/tests/generic_relations/migrations/__init__.py b/tests/generic_relations/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/generic_relations/models.py b/tests/generic_relations/models.py new file mode 100644 index 000000000..55bc243cb --- /dev/null +++ b/tests/generic_relations/models.py @@ -0,0 +1,46 @@ +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. + """ + tag = models.SlugField() + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + tagged_item = GenericForeignKey('content_type', 'object_id') + + def __str__(self): + return self.tag + + +@python_2_unicode_compatible +class Bookmark(models.Model): + """ + A URL bookmark that may have multiple tags attached. + """ + url = models.URLField() + tags = GenericRelation(Tag) + + def __str__(self): + return 'Bookmark: %s' % self.url + + +@python_2_unicode_compatible +class Note(models.Model): + """ + A textual note that may have multiple tags attached. + """ + text = models.TextField() + tags = GenericRelation(Tag) + + def __str__(self): + return 'Note: %s' % self.text diff --git a/tests/test_relations_generic.py b/tests/generic_relations/test_generic_relations.py similarity index 63% rename from tests/test_relations_generic.py rename to tests/generic_relations/test_generic_relations.py index a3798b0a3..c8de332e1 100644 --- a/tests/test_relations_generic.py +++ b/tests/generic_relations/test_generic_relations.py @@ -1,52 +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.test import TestCase -from django.utils.encoding import python_2_unicode_compatible from rest_framework import serializers - -@python_2_unicode_compatible -class Tag(models.Model): - """ - Tags have a descriptive slug, and are attached to an arbitrary object. - """ - tag = models.SlugField() - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - tagged_item = GenericForeignKey('content_type', 'object_id') - - def __str__(self): - return self.tag - - -@python_2_unicode_compatible -class Bookmark(models.Model): - """ - A URL bookmark that may have multiple tags attached. - """ - url = models.URLField() - tags = GenericRelation(Tag) - - def __str__(self): - return 'Bookmark: %s' % self.url - - -@python_2_unicode_compatible -class Note(models.Model): - """ - A textual note that may have multiple tags attached. - """ - text = models.TextField() - tags = GenericRelation(Tag) - - def __str__(self): - return 'Note: %s' % self.text +from .models import Bookmark, Note, Tag class TestGenericRelations(TestCase): From 1c5466eae772212eaf1df1832870cf64b77c9dc5 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 15:23:15 +0100 Subject: [PATCH 108/185] Remove Django 2.2 from allowed failure. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 796ef0502..9543cb452 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,6 @@ matrix: allow_failures: - env: DJANGO=master - - env: DJANGO=2.2 install: - pip install tox tox-venv tox-travis From 65f5c11a5b8aca5a6aca14c7ad85293fa941c37d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 15:38:21 +0100 Subject: [PATCH 109/185] Document support for Django 2.2. --- README.md | 2 +- docs/index.md | 2 +- setup.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 02f8ca275..0309ee2bd 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,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) -* Django (1.11, 2.0, 2.1) +* Django (1.11, 2.0, 2.1, 2.2) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/docs/index.md b/docs/index.md index b5ef5f5a6..c74b2caf0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: * Python (2.7, 3.4, 3.5, 3.6, 3.7) -* Django (1.11, 2.0, 2.1) +* Django (1.11, 2.0, 2.1, 2.2) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/setup.py b/setup.py index 341f33990..96384ab46 100755 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ setup( 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', 'Framework :: Django :: 2.1', + 'Framework :: Django :: 2.2', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', From e8b4bb1471b9fbd5895a63cc23b3b53ff45b3df1 Mon Sep 17 00:00:00 2001 From: kuter Date: Thu, 14 Feb 2019 17:51:10 +0100 Subject: [PATCH 110/185] Added tests for generateschema management command. (#6442) --- tests/test_generateschema.py | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/test_generateschema.py diff --git a/tests/test_generateschema.py b/tests/test_generateschema.py new file mode 100644 index 000000000..915c6ea05 --- /dev/null +++ b/tests/test_generateschema.py @@ -0,0 +1,88 @@ +from __future__ import unicode_literals + +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 +from rest_framework.views import APIView + + +class FooView(APIView): + def get(self, request): + pass + + +urlpatterns = [ + url(r'^$', FooView.as_view()) +] + + +@override_settings(ROOT_URLCONF='tests.test_generateschema') +@pytest.mark.skipif(not coreapi, reason='coreapi is not installed') +class GenerateSchemaTests(TestCase): + """Tests for management command generateschema.""" + + def setUp(self): + self.out = six.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 + title: SampleAPI + version: '' + openapi: 3.0.0 + paths: + /: + get: + operationId: list + servers: + - url: http://api.sample.com/ + """ + call_command('generateschema', + '--title=SampleAPI', + '--url=http://api.sample.com', + '--description=Sample description', + stdout=self.out) + + self.assertIn(formatting.dedent(expected_out), self.out.getvalue()) + + def test_renders_openapi_json_schema(self): + expected_out = { + "openapi": "3.0.0", + "info": { + "version": "", + "title": "", + "description": "" + }, + "servers": [ + { + "url": "" + } + ], + "paths": { + "/": { + "get": { + "operationId": "list" + } + } + } + } + call_command('generateschema', + '--format=openapi-json', + stdout=self.out) + out_json = json.loads(self.out.getvalue()) + + self.assertDictEqual(out_json, expected_out) + + def test_renders_corejson_schema(self): + expected_out = """{"_type":"document","":{"list":{"_type":"link","url":"/","action":"get"}}}""" + call_command('generateschema', + '--format=corejson', + stdout=self.out) + self.assertIn(expected_out, self.out.getvalue()) From de3929fb3334262bbf57980fff41d3f8c6f2b8b4 Mon Sep 17 00:00:00 2001 From: Rohit Gupta Date: Fri, 15 Feb 2019 15:27:02 +0530 Subject: [PATCH 111/185] Add Python 3.7 to classifiers. (#6458) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 96384ab46..cb850a3ae 100755 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Internet :: WWW/HTTP', ] ) From f9401f5ff0c4738d02ea5d7576435913582006ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Sat, 16 Feb 2019 15:47:13 +0100 Subject: [PATCH 112/185] Fix Python 3 compat in documentation --- docs/api-guide/serializers.md | 12 ++++++------ docs/community/3.0-announcement.md | 2 +- docs/tutorial/1-serialization.md | 14 +++++++------- docs/tutorial/2-requests-and-responses.md | 10 +++++----- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 3ef930c64..e25053936 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -152,7 +152,7 @@ When deserializing data, you always need to call `is_valid()` before attempting serializer.is_valid() # False serializer.errors - # {'email': [u'Enter a valid e-mail address.'], 'created': [u'This field is required.']} + # {'email': ['Enter a valid e-mail address.'], 'created': ['This field is required.']} Each key in the dictionary will be the field name, and the values will be lists of strings of any error messages corresponding to that field. The `non_field_errors` key may also be present, and will list any general validation errors. The name of the `non_field_errors` key may be customized using the `NON_FIELD_ERRORS_KEY` REST framework setting. @@ -253,7 +253,7 @@ When passing data to a serializer instance, the unmodified data will be made ava By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the `partial` argument in order to allow partial updates. # Update `comment` with partial data - serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) + serializer = CommentSerializer(comment, data={'content': 'foo bar'}, partial=True) ## Dealing with nested objects @@ -293,7 +293,7 @@ When dealing with nested representations that support deserializing the data, an serializer.is_valid() # False serializer.errors - # {'user': {'email': [u'Enter a valid e-mail address.']}, 'created': [u'This field is required.']} + # {'user': {'email': ['Enter a valid e-mail address.']}, 'created': ['This field is required.']} Similarly, the `.validated_data` property will include nested data structures. @@ -415,7 +415,7 @@ You can provide arbitrary additional context by passing a `context` argument whe serializer = AccountSerializer(account, context={'request': request}) serializer.data - # {'id': 6, 'owner': u'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'} + # {'id': 6, 'owner': 'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'} The context dictionary can be used within any serializer field logic, such as a custom `.to_representation()` method, by accessing the `self.context` attribute. @@ -1094,10 +1094,10 @@ This would then allow you to do the following: >>> model = User >>> fields = ('id', 'username', 'email') >>> - >>> print UserSerializer(user) + >>> print(UserSerializer(user)) {'id': 2, 'username': 'jonwatts', 'email': 'jon@example.com'} >>> - >>> print UserSerializer(user, fields=('id', 'email')) + >>> print(UserSerializer(user, fields=('id', 'email'))) {'id': 2, 'email': 'jon@example.com'} ## Customizing the default fields diff --git a/docs/community/3.0-announcement.md b/docs/community/3.0-announcement.md index 13be1e3cd..dc118d70c 100644 --- a/docs/community/3.0-announcement.md +++ b/docs/community/3.0-announcement.md @@ -389,7 +389,7 @@ You can include `expiry_date` as a field option on a `ModelSerializer` class. These fields will be mapped to `serializers.ReadOnlyField()` instances. >>> serializer = InvitationSerializer() - >>> print repr(serializer) + >>> print(repr(serializer)) InvitationSerializer(): to_email = EmailField(max_length=75) message = CharField(max_length=1000) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 387f99eda..ec507df05 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -137,20 +137,20 @@ Okay, once we've got a few imports out of the way, let's create a couple of code snippet = Snippet(code='foo = "bar"\n') snippet.save() - snippet = Snippet(code='print "hello, world"\n') + snippet = Snippet(code='print("hello, world")\n') snippet.save() We've now got a few snippet instances to play with. Let's take a look at serializing one of those instances. serializer = SnippetSerializer(snippet) serializer.data - # {'id': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} + # {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'} At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`. content = JSONRenderer().render(serializer.data) content - # '{"id": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}' + # '{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}' Deserialization is similar. First we parse a stream into Python native datatypes... @@ -165,7 +165,7 @@ Deserialization is similar. First we parse a stream into Python native datatype serializer.is_valid() # True serializer.validated_data - # OrderedDict([('title', ''), ('code', 'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]) + # OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]) serializer.save() # @@ -175,7 +175,7 @@ We can also serialize querysets instead of model instances. To do so we simply serializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data - # [OrderedDict([('id', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] + # [OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] ## Using ModelSerializers @@ -338,7 +338,7 @@ Finally, we can get a list of all of the snippets: { "id": 2, "title": "", - "code": "print \"hello, world\"\n", + "code": "print(\"hello, world\")\n", "linenos": false, "language": "python", "style": "friendly" @@ -354,7 +354,7 @@ Or we can get a particular snippet by referencing its id: { "id": 2, "title": "", - "code": "print \"hello, world\"\n", + "code": "print(\"hello, world\")\n", "linenos": false, "language": "python", "style": "friendly" diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 4a9b0dbf7..e3d21e864 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -143,7 +143,7 @@ We can get a list of all of the snippets, as before. { "id": 2, "title": "", - "code": "print \"hello, world\"\n", + "code": "print(\"hello, world\")\n", "linenos": false, "language": "python", "style": "friendly" @@ -163,24 +163,24 @@ Or by appending a format suffix: Similarly, we can control the format of the request that we send, using the `Content-Type` header. # POST using form data - http --form POST http://127.0.0.1:8000/snippets/ code="print 123" + http --form POST http://127.0.0.1:8000/snippets/ code="print(123)" { "id": 3, "title": "", - "code": "print 123", + "code": "print(123)", "linenos": false, "language": "python", "style": "friendly" } # POST using JSON - http --json POST http://127.0.0.1:8000/snippets/ code="print 456" + http --json POST http://127.0.0.1:8000/snippets/ code="print(456)" { "id": 4, "title": "", - "code": "print 456", + "code": "print(456)", "linenos": false, "language": "python", "style": "friendly" From eb3180173ed119192c57a6ab52097025c00148e3 Mon Sep 17 00:00:00 2001 From: jeffrey k eliasen Date: Tue, 19 Feb 2019 03:15:03 -0800 Subject: [PATCH 113/185] Made templates compatible with session-based CSRF. (#6207) --- rest_framework/static/rest_framework/js/csrf.js | 2 +- rest_framework/templates/rest_framework/admin.html | 2 +- rest_framework/templates/rest_framework/base.html | 2 +- tests/test_templates.py | 12 +++++++++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/rest_framework/static/rest_framework/js/csrf.js b/rest_framework/static/rest_framework/js/csrf.js index 97c8d0124..6e4bf39a7 100644 --- a/rest_framework/static/rest_framework/js/csrf.js +++ b/rest_framework/static/rest_framework/js/csrf.js @@ -38,7 +38,7 @@ function sameOrigin(url) { !(/^(\/\/|http:|https:).*/.test(url)); } -var csrftoken = getCookie(window.drf.csrfCookieName); +var csrftoken = window.drf.csrfToken; $.ajaxSetup({ beforeSend: function(xhr, settings) { diff --git a/rest_framework/templates/rest_framework/admin.html b/rest_framework/templates/rest_framework/admin.html index 66d8431f1..f058b2694 100644 --- a/rest_framework/templates/rest_framework/admin.html +++ b/rest_framework/templates/rest_framework/admin.html @@ -247,7 +247,7 @@ diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index e8a13674e..6d740f2b5 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -290,7 +290,7 @@ diff --git a/tests/test_templates.py b/tests/test_templates.py index a296395f6..19f511b96 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -1,7 +1,17 @@ +import re + from django.shortcuts import render +def test_base_template_with_context(): + context = {'request': True, 'csrf_token': 'TOKEN'} + result = render({}, 'rest_framework/base.html', context=context) + assert re.search(r'\bcsrfToken: "TOKEN"', result.content.decode('utf-8')) + + def test_base_template_with_no_context(): # base.html should be renderable with no context, # so it can be easily extended. - render({}, 'rest_framework/base.html') + result = render({}, 'rest_framework/base.html') + # note that this response will not include a valid CSRF token + assert re.search(r'\bcsrfToken: ""', result.content.decode('utf-8')) From 6de33effd6d0e1cc9ad2dc3a2a4d0487583a116f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 19 Feb 2019 16:18:55 +0100 Subject: [PATCH 114/185] =?UTF-8?q?Doc=E2=80=99d=20requirement=20to=20impl?= =?UTF-8?q?ement=20has=5Fobject=5Fpermission()=20(#6462)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …when using provided permission classes. Closes #6402. --- docs/api-guide/permissions.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index e04b1199b..a797da9ac 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -48,6 +48,19 @@ For example: self.check_object_permissions(self.request, obj) return obj +--- + +**Note**: With the exception of `DjangoObjectPermissions`, the provided +permission classes in `rest_framework.permssions` **do not** implement the +methods necessary to check object permissions. + +If you wish to use the provided permission classes in order to check object +permissions, **you must** subclass them and implement the +`has_object_permission()` method described in the [_Custom +permissions_](#custom-permissions) section (below). + +--- + #### Limitations of object level permissions For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects. From 1ece516d2d0d942d9de513f85d601afcccf67ebd Mon Sep 17 00:00:00 2001 From: Si Feng Date: Tue, 19 Feb 2019 07:38:20 -0800 Subject: [PATCH 115/185] Adjusted field `validators` to accept iterables. (#6282) Closes 6280. --- rest_framework/fields.py | 4 ++-- rest_framework/serializers.py | 4 ++-- tests/test_fields.py | 19 +++++++++++++++++++ tests/test_serializer.py | 34 +++++++++++++++++++++++++++++++++- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 562e52b22..2cbfd22bb 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -350,7 +350,7 @@ class Field(object): self.default_empty_html = default if validators is not None: - self.validators = validators[:] + self.validators = list(validators) # These are set up by `.bind()` when the field is added to a serializer. self.field_name = None @@ -410,7 +410,7 @@ class Field(object): self._validators = validators def get_validators(self): - return self.default_validators[:] + return list(self.default_validators) def get_initial(self): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index eae08a34c..9830edb3f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -393,7 +393,7 @@ class Serializer(BaseSerializer): # Used by the lazily-evaluated `validators` property. meta = getattr(self, 'Meta', None) validators = getattr(meta, 'validators', None) - return validators[:] if validators else [] + return list(validators) if validators else [] def get_initial(self): if hasattr(self, 'initial_data'): @@ -1480,7 +1480,7 @@ class ModelSerializer(Serializer): # If the validators have been declared explicitly then use that. validators = getattr(getattr(self, 'Meta', None), 'validators', None) if validators is not None: - return validators[:] + return list(validators) # Otherwise use the default set of validators. return ( diff --git a/tests/test_fields.py b/tests/test_fields.py index 9a1d04979..12c936b22 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -740,6 +740,25 @@ class TestCharField(FieldValues): 'Null characters are not allowed.' ] + def test_iterable_validators(self): + """ + Ensure `validators` parameter is compatible with reasonable iterables. + """ + value = 'example' + + for validators in ([], (), set()): + field = serializers.CharField(validators=validators) + field.run_validation(value) + + def raise_exception(value): + raise exceptions.ValidationError('Raised error') + + for validators in ([raise_exception], (raise_exception,), set([raise_exception])): + field = serializers.CharField(validators=validators) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation(value) + assert exc_info.value.detail == ['Raised error'] + class TestEmailField(FieldValues): """ diff --git a/tests/test_serializer.py b/tests/test_serializer.py index efa1adf0e..6e4ff22b2 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -10,7 +10,7 @@ from collections import Mapping import pytest from django.db import models -from rest_framework import fields, relations, serializers +from rest_framework import exceptions, fields, relations, serializers from rest_framework.compat import unicode_repr from rest_framework.fields import Field @@ -183,6 +183,38 @@ class TestSerializer: assert serializer.validated_data.coords[1] == 50.941357 assert serializer.errors == {} + def test_iterable_validators(self): + """ + Ensure `validators` parameter is compatible with reasonable iterables. + """ + data = {'char': 'abc', 'integer': 123} + + for validators in ([], (), set()): + class ExampleSerializer(serializers.Serializer): + char = serializers.CharField(validators=validators) + integer = serializers.IntegerField() + + serializer = ExampleSerializer(data=data) + assert serializer.is_valid() + assert serializer.validated_data == data + assert serializer.errors == {} + + def raise_exception(value): + raise exceptions.ValidationError('Raised error') + + for validators in ([raise_exception], (raise_exception,), set([raise_exception])): + class ExampleSerializer(serializers.Serializer): + char = serializers.CharField(validators=validators) + integer = serializers.IntegerField() + + serializer = ExampleSerializer(data=data) + assert not serializer.is_valid() + assert serializer.data == data + assert serializer.validated_data == {} + assert serializer.errors == {'char': [ + exceptions.ErrorDetail(string='Raised error', code='invalid') + ]} + class TestValidateMethod: def test_non_field_error_validate_method(self): From d110454d4c15fe6617a112e846b89e09ed6c95b2 Mon Sep 17 00:00:00 2001 From: Allan Reyes Date: Tue, 19 Feb 2019 08:18:14 -0800 Subject: [PATCH 116/185] Added SearchFilter.get_search_fields() hook. (#6279) --- docs/api-guide/filtering.md | 7 +++++++ rest_framework/filters.py | 10 +++++++++- tests/test_filters.py | 25 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 1a04ad5e3..5d1f6e49a 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -218,6 +218,13 @@ For example: By default, the search parameter is named `'search`', but this may be overridden with the `SEARCH_PARAM` setting. +To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request: + + class CustomSearchFilter(self, view, request): + if request.query_params.get('title_only'): + return ('title',) + return super(CustomSearchFilter, self).get_search_fields(view, request) + For more details, see the [Django documentation][search-django-admin]. --- diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 0627bd8c4..53d49ae45 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -53,6 +53,14 @@ class SearchFilter(BaseFilterBackend): search_title = _('Search') search_description = _('A search term.') + def get_search_fields(self, view, request): + """ + Search fields are obtained from the view, but the request is always + passed to this method. Sub-classes can override this method to + dynamically change the search fields based on request content. + """ + return getattr(view, 'search_fields', None) + def get_search_terms(self, request): """ Search terms are set by a ?search=... query parameter, @@ -90,7 +98,7 @@ class SearchFilter(BaseFilterBackend): return False def filter_queryset(self, request, queryset, view): - search_fields = getattr(view, 'search_fields', None) + search_fields = self.get_search_fields(view, request) search_terms = self.get_search_terms(request) if not search_fields or not search_terms: diff --git a/tests/test_filters.py b/tests/test_filters.py index 2d4eb132e..39a96f994 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -156,6 +156,31 @@ class SearchFilterTests(TestCase): reload_module(filters) + def test_search_with_filter_subclass(self): + class CustomSearchFilter(filters.SearchFilter): + # Filter that dynamically changes search fields + 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) + + class SearchListView(generics.ListAPIView): + queryset = SearchFilterModel.objects.all() + serializer_class = SearchFilterSerializer + filter_backends = (CustomSearchFilter,) + search_fields = ('$title', '$text') + + view = SearchListView.as_view() + request = factory.get('/', {'search': '^\w{3}$'}) + response = view(request) + assert len(response.data) == 10 + + request = factory.get('/', {'search': '^\w{3}$', 'title_only': 'true'}) + response = view(request) + assert response.data == [ + {'id': 3, 'title': 'zzz', 'text': 'cde'} + ] + class AttributeModel(models.Model): label = models.CharField(max_length=32) From d932baa64660012cb7e5b86ba0f985fee8d5e57c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 22 Feb 2019 11:11:52 +0100 Subject: [PATCH 117/185] Corrected link to ajax-form library. Closes #6465. --- docs/topics/browser-enhancements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/browser-enhancements.md b/docs/topics/browser-enhancements.md index 0e79a66e1..fa07b6064 100644 --- a/docs/topics/browser-enhancements.md +++ b/docs/topics/browser-enhancements.md @@ -81,7 +81,7 @@ was later [dropped from the spec][html5]. There remains as well as how to support content types other than form-encoded data. [cite]: https://www.amazon.com/RESTful-Web-Services-Leonard-Richardson/dp/0596529260 -[ajax-form]: https://github.com/encode/ajax-form +[ajax-form]: https://github.com/tomchristie/ajax-form [rails]: https://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work [html5]: https://www.w3.org/TR/html5-diff/#changes-2010-06-24 [put_delete]: http://amundsen.com/examples/put-delete-forms/ From 286cf57a8d22aafd51054a40a5cf8a58edfc8226 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 22 Feb 2019 10:58:01 -0800 Subject: [PATCH 118/185] Update filtering docs (#6467) --- docs/api-guide/filtering.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 5d1f6e49a..aff267818 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -127,7 +127,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering """ model = Product serializer_class = ProductSerializer - filter_class = ProductFilter + filterset_class = ProductFilter def get_queryset(self): user = self.request.user @@ -305,9 +305,9 @@ A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectP **permissions.py**: class CustomObjectPermissions(permissions.DjangoObjectPermissions): - """ - Similar to `DjangoObjectPermissions`, but adding 'view' permissions. - """ + """ + Similar to `DjangoObjectPermissions`, but adding 'view' permissions. + """ perms_map = { 'GET': ['%(app_label)s.view_%(model_name)s'], 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], @@ -321,11 +321,11 @@ A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectP **views.py**: class EventViewSet(viewsets.ModelViewSet): - """ - Viewset that only lists events if user has 'view' permissions, and only - allows operations on individual events if user has appropriate 'view', 'add', - 'change' or 'delete' permissions. - """ + """ + Viewset that only lists events if user has 'view' permissions, and only + allows operations on individual events if user has appropriate 'view', 'add', + 'change' or 'delete' permissions. + """ queryset = Event.objects.all() serializer_class = EventSerializer filter_backends = (filters.DjangoObjectPermissionsFilter,) From 07c5c968ce8b06c7ebbcc91a070aa492510611b2 Mon Sep 17 00:00:00 2001 From: Charlie Hornsby Date: Mon, 25 Feb 2019 10:17:04 +0200 Subject: [PATCH 119/185] Fix DeprecationWarning when accessing collections.abc classes via collections (#6268) * Use compat version of collections.abc.Mapping Since the Mapping class will no longer be available to import directly from the collections module in Python 3.8, we should use the compatibility helper introduced in #6154 in the fields module. * Alias and use compat version of collections.abc.MutableMapping Since the MutableMapping class will no longer be available to import directly from the collections module in Python 3.8, we should create an alias for it in the compat module and use that instead. --- rest_framework/compat.py | 4 ++-- rest_framework/fields.py | 7 +++---- rest_framework/utils/serializer_helpers.py | 5 ++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 5a4bcdf66..59217c587 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -12,10 +12,10 @@ from django.views.generic import View try: # Python 3 - from collections.abc import Mapping # noqa + from collections.abc import Mapping, MutableMapping # noqa except ImportError: # Python 2.7 - from collections import Mapping # noqa + from collections import Mapping, MutableMapping # noqa try: from django.urls import ( # noqa diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2cbfd22bb..1b8387714 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import collections import copy import datetime import decimal @@ -33,7 +32,7 @@ from pytz.exceptions import InvalidTimeError from rest_framework import ISO_8601 from rest_framework.compat import ( - MaxLengthValidator, MaxValueValidator, MinLengthValidator, + Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator, MinValueValidator, ProhibitNullCharactersValidator, unicode_repr, unicode_to_repr ) @@ -96,7 +95,7 @@ def get_attribute(instance, attrs): """ for attr in attrs: try: - if isinstance(instance, collections.Mapping): + if isinstance(instance, Mapping): instance = instance[attr] else: instance = getattr(instance, attr) @@ -1661,7 +1660,7 @@ class ListField(Field): """ if html.is_html_input(data): data = html.parse_html_list(data, default=[]) - if isinstance(data, type('')) or isinstance(data, collections.Mapping) or not hasattr(data, '__iter__'): + if isinstance(data, type('')) or isinstance(data, 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') diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 6b662a66c..c24e51d09 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,11 +1,10 @@ from __future__ import unicode_literals -import collections from collections import OrderedDict from django.utils.encoding import force_text -from rest_framework.compat import unicode_to_repr +from rest_framework.compat import MutableMapping, unicode_to_repr from rest_framework.utils import json @@ -130,7 +129,7 @@ class NestedBoundField(BoundField): return self.__class__(self._field, values, self.errors, self._prefix) -class BindingDict(collections.MutableMapping): +class BindingDict(MutableMapping): """ This dict-like object is used to store fields on a serializer. From 8a29c53226f63733775a232fea0c2c65cb50b6b0 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 25 Feb 2019 11:49:29 +0100 Subject: [PATCH 120/185] Allowed Q objects in limit_choices_to introspection. (#6472) Closes #6470. --- rest_framework/utils/field_mapping.py | 4 +++- tests/models.py | 7 +++++++ tests/test_relations_pk.py | 21 +++++++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 991f20f17..f11b4b94e 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -251,7 +251,9 @@ def get_relation_kwargs(field_name, relation_info): limit_choices_to = model_field and model_field.get_limit_choices_to() if limit_choices_to: - kwargs['queryset'] = kwargs['queryset'].filter(**limit_choices_to) + if not isinstance(limit_choices_to, models.Q): + limit_choices_to = models.Q(**limit_choices_to) + kwargs['queryset'] = kwargs['queryset'].filter(limit_choices_to) if has_through_model: kwargs['read_only'] = True diff --git a/tests/models.py b/tests/models.py index 55f250e04..17bf23cda 100644 --- a/tests/models.py +++ b/tests/models.py @@ -59,6 +59,13 @@ class ForeignKeySourceWithLimitedChoices(RESTFrameworkModel): on_delete=models.CASCADE) +class ForeignKeySourceWithQLimitedChoices(RESTFrameworkModel): + target = models.ForeignKey(ForeignKeyTarget, help_text='Target', + verbose_name='Target', + limit_choices_to=models.Q(name__startswith="limited-"), + on_delete=models.CASCADE) + + # Nullable ForeignKey class NullableForeignKeySource(RESTFrameworkModel): name = models.CharField(max_length=100) diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 31b6bb867..2cffb62e6 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -5,10 +5,11 @@ from django.utils import six from rest_framework import serializers from tests.models import ( - ForeignKeySource, ForeignKeySourceWithLimitedChoices, ForeignKeyTarget, - ManyToManySource, ManyToManyTarget, NullableForeignKeySource, - NullableOneToOneSource, NullableUUIDForeignKeySource, OneToOnePKSource, - OneToOneTarget, UUIDForeignKeyTarget + ForeignKeySource, ForeignKeySourceWithLimitedChoices, + ForeignKeySourceWithQLimitedChoices, ForeignKeyTarget, ManyToManySource, + ManyToManyTarget, NullableForeignKeySource, NullableOneToOneSource, + NullableUUIDForeignKeySource, OneToOnePKSource, OneToOneTarget, + UUIDForeignKeyTarget ) @@ -378,6 +379,18 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeySourceWithLimitedChoicesSerializer().fields["target"].get_queryset() assert len(queryset) == 1 + def test_queryset_size_with_Q_limited_choices(self): + limited_target = ForeignKeyTarget(name="limited-target") + limited_target.save() + + class QLimitedChoicesSerializer(serializers.ModelSerializer): + class Meta: + model = ForeignKeySourceWithQLimitedChoices + fields = ("id", "target") + + queryset = QLimitedChoicesSerializer().fields["target"].get_queryset() + assert len(queryset) == 1 + class PKNullableForeignKeyTests(TestCase): def setUp(self): From 94fbfcb6fdc8f9755a9f005bd005b00be3309ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?= Date: Mon, 25 Feb 2019 20:47:02 +0800 Subject: [PATCH 121/185] Added lazy evaluation to composed permissions. (#6463) Refs #6402. --- rest_framework/compat.py | 11 +++++ rest_framework/permissions.py | 8 ++-- tests/test_permissions.py | 87 ++++++++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 59217c587..9422e6ad5 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -5,6 +5,8 @@ versions of Django/Python, and compatibility wrappers around optional packages. from __future__ import unicode_literals +import sys + from django.conf import settings from django.core import validators from django.utils import six @@ -34,6 +36,11 @@ try: except ImportError: ProhibitNullCharactersValidator = None +try: + from unittest import mock +except ImportError: + mock = None + def get_original_route(urlpattern): """ @@ -314,3 +321,7 @@ class MinLengthValidator(CustomValidatorMessage, validators.MinLengthValidator): class MaxLengthValidator(CustomValidatorMessage, validators.MaxLengthValidator): pass + + +# Version Constants. +PY36 = sys.version_info >= (3, 6) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index ac616e202..69432d79a 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -44,13 +44,13 @@ class AND: def has_permission(self, request, view): return ( - self.op1.has_permission(request, view) & + self.op1.has_permission(request, view) and self.op2.has_permission(request, view) ) def has_object_permission(self, request, view, obj): return ( - self.op1.has_object_permission(request, view, obj) & + self.op1.has_object_permission(request, view, obj) and self.op2.has_object_permission(request, view, obj) ) @@ -62,13 +62,13 @@ class OR: def has_permission(self, request, view): return ( - self.op1.has_permission(request, view) | + self.op1.has_permission(request, view) or self.op2.has_permission(request, view) ) def has_object_permission(self, request, view, obj): return ( - self.op1.has_object_permission(request, view, obj) | + self.op1.has_object_permission(request, view, obj) or self.op2.has_object_permission(request, view, obj) ) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 381ec448c..f9d53430f 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -5,6 +5,7 @@ import unittest import warnings import django +import pytest from django.contrib.auth.models import AnonymousUser, Group, Permission, User from django.db import models from django.test import TestCase @@ -14,7 +15,7 @@ from rest_framework import ( HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, status, views ) -from rest_framework.compat import is_guardian_installed +from rest_framework.compat import PY36, is_guardian_installed, mock from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.routers import DefaultRouter from rest_framework.test import APIRequestFactory @@ -600,3 +601,87 @@ class PermissionsCompositionTests(TestCase): permissions.IsAuthenticated ) assert composed_perm().has_permission(request, None) is True + + @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") + def test_or_lazyness(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + + with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: + composed_perm = (permissions.AllowAny | permissions.IsAuthenticated) + hasperm = composed_perm().has_permission(request, None) + self.assertIs(hasperm, True) + mock_allow.assert_called_once() + mock_deny.assert_not_called() + + with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: + composed_perm = (permissions.IsAuthenticated | permissions.AllowAny) + hasperm = composed_perm().has_permission(request, None) + self.assertIs(hasperm, True) + mock_deny.assert_called_once() + mock_allow.assert_called_once() + + @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") + def test_object_or_lazyness(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + + with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: + composed_perm = (permissions.AllowAny | permissions.IsAuthenticated) + hasperm = composed_perm().has_object_permission(request, None, None) + self.assertIs(hasperm, True) + mock_allow.assert_called_once() + mock_deny.assert_not_called() + + with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: + composed_perm = (permissions.IsAuthenticated | permissions.AllowAny) + hasperm = composed_perm().has_object_permission(request, None, None) + self.assertIs(hasperm, True) + mock_deny.assert_called_once() + mock_allow.assert_called_once() + + @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") + def test_and_lazyness(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + + with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: + composed_perm = (permissions.AllowAny & permissions.IsAuthenticated) + hasperm = composed_perm().has_permission(request, None) + self.assertIs(hasperm, False) + mock_allow.assert_called_once() + mock_deny.assert_called_once() + + with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: + composed_perm = (permissions.IsAuthenticated & permissions.AllowAny) + hasperm = composed_perm().has_permission(request, None) + self.assertIs(hasperm, False) + mock_allow.assert_not_called() + mock_deny.assert_called_once() + + @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") + def test_object_and_lazyness(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + + with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: + composed_perm = (permissions.AllowAny & permissions.IsAuthenticated) + hasperm = composed_perm().has_object_permission(request, None, None) + self.assertIs(hasperm, False) + mock_allow.assert_called_once() + mock_deny.assert_called_once() + + with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: + composed_perm = (permissions.IsAuthenticated & permissions.AllowAny) + hasperm = composed_perm().has_object_permission(request, None, None) + self.assertIs(hasperm, False) + mock_allow.assert_not_called() + mock_deny.assert_called_once() From 739b0a272a66d9beb7afb7490f201d71d7cdc910 Mon Sep 17 00:00:00 2001 From: Xtreak Date: Mon, 25 Feb 2019 19:52:45 +0530 Subject: [PATCH 122/185] Fix DeprecationWarning in tests when accessing collections.abc classes via collections (#6473) --- tests/test_renderers.py | 4 ++-- tests/test_serializer.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 8518a3f7c..b4c41b148 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import re -from collections import MutableMapping, OrderedDict +from collections import OrderedDict import pytest from django.conf.urls import include, url @@ -16,7 +16,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 coreapi +from rest_framework.compat import MutableMapping, 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 6e4ff22b2..0f1e81965 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -5,13 +5,12 @@ import inspect import pickle import re import unittest -from collections import Mapping import pytest from django.db import models from rest_framework import exceptions, fields, relations, serializers -from rest_framework.compat import unicode_repr +from rest_framework.compat import Mapping, unicode_repr from rest_framework.fields import Field from .models import ( From 2daf6f13414f1a5d363b5bc4a2ce3ba294a7766c Mon Sep 17 00:00:00 2001 From: Adrien Brunet Date: Mon, 25 Feb 2019 15:33:40 +0100 Subject: [PATCH 123/185] Add negation ~ operator to permissions composition (#6361) --- docs/api-guide/permissions.md | 2 +- rest_framework/permissions.py | 24 ++++++++++++++++++++++++ tests/test_permissions.py | 25 ++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index a797da9ac..6a1297e60 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -134,7 +134,7 @@ Provided they inherit from `rest_framework.permissions.BasePermission`, permissi } return Response(content) -__Note:__ it only supports & -and- and | -or-. +__Note:__ it supports & (and), | (or) and ~ (not). --- diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 69432d79a..5d75f54ba 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -24,6 +24,19 @@ class OperationHolderMixin: def __ror__(self, other): return OperandHolder(OR, other, self) + def __invert__(self): + return SingleOperandHolder(NOT, self) + + +class SingleOperandHolder(OperationHolderMixin): + def __init__(self, operator_class, op1_class): + self.operator_class = operator_class + self.op1_class = op1_class + + def __call__(self, *args, **kwargs): + op1 = self.op1_class(*args, **kwargs) + return self.operator_class(op1) + class OperandHolder(OperationHolderMixin): def __init__(self, operator_class, op1_class, op2_class): @@ -73,6 +86,17 @@ class OR: ) +class NOT: + def __init__(self, op1): + self.op1 = op1 + + def has_permission(self, request, view): + return not self.op1.has_permission(request, view) + + def has_object_permission(self, request, view, obj): + return not self.op1.has_object_permission(request, view, obj) + + class BasePermissionMetaclass(OperationHolderMixin, type): pass diff --git a/tests/test_permissions.py b/tests/test_permissions.py index f9d53430f..807006858 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -580,7 +580,19 @@ class PermissionsCompositionTests(TestCase): composed_perm = permissions.IsAuthenticated | permissions.AllowAny assert composed_perm().has_permission(request, None) is True - def test_several_levels(self): + def test_not_false(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + composed_perm = ~permissions.IsAuthenticated + assert composed_perm().has_permission(request, None) is True + + def test_not_true(self): + request = factory.get('/1', format='json') + request.user = self.user + composed_perm = ~permissions.AllowAny + assert composed_perm().has_permission(request, None) is False + + def test_several_levels_without_negation(self): request = factory.get('/1', format='json') request.user = self.user composed_perm = ( @@ -591,6 +603,17 @@ class PermissionsCompositionTests(TestCase): ) assert composed_perm().has_permission(request, None) is True + def test_several_levels_and_precedence_with_negation(self): + request = factory.get('/1', format='json') + request.user = self.user + composed_perm = ( + permissions.IsAuthenticated & + ~ permissions.IsAdminUser & + permissions.IsAuthenticated & + ~(permissions.IsAdminUser & permissions.IsAdminUser) + ) + assert composed_perm().has_permission(request, None) is True + def test_several_levels_and_precedence(self): request = factory.get('/1', format='json') request.user = self.user From 317174b163d80aaa8be44b8d3bf073d5c50acb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20G=C3=B3rski?= Date: Mon, 25 Feb 2019 16:59:25 +0100 Subject: [PATCH 124/185] Avoided calling distinct on annotated fields in SearchFilter. (#6240) Fixes #6094 --- rest_framework/filters.py | 3 +++ tests/test_filters.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 53d49ae45..7989ace34 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -85,6 +85,9 @@ class SearchFilter(BaseFilterBackend): opts = queryset.model._meta if search_field[0] in self.lookup_prefixes: search_field = search_field[1:] + # Annotated fields do not need to be distinct + if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations: + return False parts = search_field.split(LOOKUP_SEP) for part in parts: field = opts.get_field(part) diff --git a/tests/test_filters.py b/tests/test_filters.py index 39a96f994..088d25436 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -5,6 +5,7 @@ import datetime import pytest from django.core.exceptions import ImproperlyConfigured 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 @@ -329,6 +330,38 @@ class SearchFilterToManyTests(TestCase): assert len(response.data) == 1 +class SearchFilterAnnotatedSerializer(serializers.ModelSerializer): + title_text = serializers.CharField() + + class Meta: + model = SearchFilterModel + fields = ('title', 'text', 'title_text') + + +class SearchFilterAnnotatedFieldTests(TestCase): + @classmethod + def setUpTestData(cls): + SearchFilterModel.objects.create(title='abc', text='def') + SearchFilterModel.objects.create(title='ghi', text='jkl') + + def test_search_in_annotated_field(self): + class SearchListView(generics.ListAPIView): + queryset = SearchFilterModel.objects.annotate( + title_text=Upper( + Concat(models.F('title'), models.F('text')) + ) + ).all() + serializer_class = SearchFilterAnnotatedSerializer + filter_backends = (filters.SearchFilter,) + search_fields = ('title_text',) + + view = SearchListView.as_view() + request = factory.get('/', {'search': 'ABCDEF'}) + response = view(request) + assert len(response.data) == 1 + assert response.data[0]['title_text'] == 'ABCDEF' + + class OrderingFilterModel(models.Model): title = models.CharField(max_length=20, verbose_name='verbose title') text = models.CharField(max_length=100) From 1dc81acb4d351ef954291391b9151fa9c524ea43 Mon Sep 17 00:00:00 2001 From: Ramon de Jezus Date: Thu, 28 Feb 2019 15:18:58 +0100 Subject: [PATCH 125/185] Fixed a typo in pagination docs. (#6475) --- docs/api-guide/pagination.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 7ae351a7f..99612ef46 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -311,7 +311,7 @@ The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagi ## link-header-pagination -The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as desribed in [Github's developer documentation](github-link-pagination). +The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as described in [Github's developer documentation](github-link-pagination). [cite]: https://docs.djangoproject.com/en/stable/topics/pagination/ [link-header]: ../img/link-header-pagination.png From 31bf59708121127994bdbcd038f4f76bb28059d7 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 1 Mar 2019 12:48:12 +0100 Subject: [PATCH 126/185] Updated note on BooleanField required kwarg generation. Closes #6474. --- docs/api-guide/fields.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 8d25d6c78..74ce2251d 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -124,7 +124,14 @@ A boolean representation. When using HTML encoded form input be aware that omitting a value will always be treated as setting a field to `False`, even if it has a `default=True` option specified. This is because HTML checkbox inputs represent the unchecked state by omitting the value, so REST framework treats omission as if it is an empty checkbox input. -Note that default `BooleanField` instances will be generated with a `required=False` option (since Django `models.BooleanField` is always `blank=True`). If you want to change this behaviour explicitly declare the `BooleanField` on the serializer class. +Note that Django 2.1 removed the `blank` kwarg from `models.BooleanField`. +Prior to Django 2.1 `models.BooleanField` fields were always `blank=True`. Thus +since Django 2.1 default `serializers.BooleanField` instances will be generated +without the `required` kwarg (i.e. equivalent to `required=True`) whereas with +previous versions of Django, default `BooleanField` instances will be generated +with a `required=False` option. If you want to control this behaviour manually, +explicitly declare the `BooleanField` on the serializer class, or use the +`extra_kwargs` option to set the `required` flag. Corresponds to `django.db.models.fields.BooleanField`. From a216d02ce0661510d456cfd327fadc3acb9ec960 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 2 Mar 2019 12:48:03 -0800 Subject: [PATCH 127/185] Merge multiple isinstance() calls to one (#6481) https://docs.python.org/3/library/functions.html#isinstance > If classinfo is a tuple of type objects (or recursively, other such > tuples), return true if object is an instance of any of the types. --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1b8387714..b5fafeaa3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1660,7 +1660,7 @@ class ListField(Field): """ if html.is_html_input(data): data = html.parse_html_list(data, default=[]) - if isinstance(data, type('')) or isinstance(data, Mapping) or not hasattr(data, '__iter__'): + if isinstance(data, (type(''), 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') From 94593b3a503472c7d1ca97b8e191ef5c8a3c00ff Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 3 Mar 2019 09:20:45 +0100 Subject: [PATCH 128/185] =?UTF-8?q?Introduce=20RemovedInDRF=E2=80=A6Warnin?= =?UTF-8?q?g=20classes=20to=20simplify=20deprecations.=20(#6480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #6290. --- docs/community/release-notes.md | 4 ++-- rest_framework/__init__.py | 8 ++++++++ rest_framework/decorators.py | 5 +++-- rest_framework/filters.py | 3 ++- rest_framework/routers.py | 12 +++++++----- tests/test_decorators.py | 8 ++++---- tests/test_permissions.py | 6 +++--- tests/test_routers.py | 8 +++++--- 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 0efc7fa6c..62ad95723 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -18,9 +18,9 @@ REST framework releases follow a formal deprecation policy, which is in line wit The timeline for deprecation of a feature present in version 1.0 would work as follows: -* Version 1.1 would remain **fully backwards compatible** with 1.0, but would raise `PendingDeprecationWarning` warnings if you use the feature that are due to be deprecated. These warnings are **silent by default**, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make. +* Version 1.1 would remain **fully backwards compatible** with 1.0, but would raise `RemovedInDRF13Warning` warnings, subclassing `PendingDeprecationWarning`, if you use the feature that are due to be deprecated. These warnings are **silent by default**, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make. -* Version 1.2 would escalate these warnings to `DeprecationWarning`, which is loud by default. +* Version 1.2 would escalate these warnings to subclass `DeprecationWarning`, which is loud by default. * Version 1.3 would remove the deprecated bits of API entirely. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index b9da046ae..eacc8dca0 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -23,3 +23,11 @@ HTTP_HEADER_ENCODING = 'iso-8859-1' ISO_8601 = 'iso-8601' default_app_config = 'rest_framework.apps.RestFrameworkConfig' + + +class RemovedInDRF310Warning(DeprecationWarning): + pass + + +class RemovedInDRF311Warning(PendingDeprecationWarning): + pass diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index f6d557d11..30bfcc4e5 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -14,6 +14,7 @@ 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 @@ -225,7 +226,7 @@ def detail_route(methods=None, **kwargs): warnings.warn( "`detail_route` is deprecated and will be removed in 3.10 in favor of " "`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) def decorator(func): @@ -243,7 +244,7 @@ def list_route(methods=None, **kwargs): warnings.warn( "`list_route` is deprecated and will be removed in 3.10 in favor of " "`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) def decorator(func): diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 7989ace34..bb1b86586 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -17,6 +17,7 @@ from django.utils import six from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ +from rest_framework import RemovedInDRF310Warning from rest_framework.compat import ( coreapi, coreschema, distinct, is_guardian_installed ) @@ -299,7 +300,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend): warnings.warn( "`DjangoObjectPermissionsFilter` has been deprecated and moved to " "the 3rd-party django-rest-framework-guardian package.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) assert is_guardian_installed(), 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed' diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 2c24f9099..1cacea181 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -25,7 +25,9 @@ from django.urls import NoReverseMatch from django.utils import six from django.utils.deprecation import RenameMethodsBase -from rest_framework import views +from rest_framework import ( + RemovedInDRF310Warning, RemovedInDRF311Warning, views +) from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.schemas import SchemaGenerator @@ -43,7 +45,7 @@ class DynamicDetailRoute(object): "`DynamicDetailRoute` is deprecated and will be removed in 3.10 " "in favor of `DynamicRoute`, which accepts a `detail` boolean. Use " "`DynamicRoute(url, name, True, initkwargs)` instead.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) return DynamicRoute(url, name, True, initkwargs) @@ -54,7 +56,7 @@ class DynamicListRoute(object): "`DynamicListRoute` is deprecated and will be removed in 3.10 in " "favor of `DynamicRoute`, which accepts a `detail` boolean. Use " "`DynamicRoute(url, name, False, initkwargs)` instead.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) return DynamicRoute(url, name, False, initkwargs) @@ -77,7 +79,7 @@ def flatten(list_of_lists): class RenameRouterMethods(RenameMethodsBase): renamed_methods = ( - ('get_default_base_name', 'get_default_basename', PendingDeprecationWarning), + ('get_default_base_name', 'get_default_basename', RemovedInDRF311Warning), ) @@ -88,7 +90,7 @@ class BaseRouter(six.with_metaclass(RenameRouterMethods)): def register(self, prefix, viewset, basename=None, base_name=None): if base_name is not None: msg = "The `base_name` argument is pending deprecation in favor of `basename`." - warnings.warn(msg, PendingDeprecationWarning, 2) + warnings.warn(msg, RemovedInDRF311Warning, 2) assert not (basename and base_name), ( "Do not provide both the `basename` and `base_name` arguments.") diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 9c6a899bf..13dd41ff3 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import pytest from django.test import TestCase -from rest_framework import status +from rest_framework import RemovedInDRF310Warning, status from rest_framework.authentication import BasicAuthentication from rest_framework.decorators import ( action, api_view, authentication_classes, detail_route, list_route, @@ -290,7 +290,7 @@ class ActionDecoratorTestCase(TestCase): raise NotImplementedError def test_detail_route_deprecation(self): - with pytest.warns(DeprecationWarning) as record: + with pytest.warns(RemovedInDRF310Warning) as record: @detail_route() def view(request): raise NotImplementedError @@ -303,7 +303,7 @@ class ActionDecoratorTestCase(TestCase): ) def test_list_route_deprecation(self): - with pytest.warns(DeprecationWarning) as record: + with pytest.warns(RemovedInDRF310Warning) as record: @list_route() def view(request): raise NotImplementedError @@ -317,7 +317,7 @@ class ActionDecoratorTestCase(TestCase): def test_route_url_name_from_path(self): # pre-3.8 behavior was to base the `url_name` off of the `url_path` - with pytest.warns(DeprecationWarning): + with pytest.warns(RemovedInDRF310Warning): @list_route(url_path='foo_bar') def view(request): raise NotImplementedError diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 807006858..2fabdfa05 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -12,8 +12,8 @@ from django.test import TestCase from django.urls import ResolverMatch from rest_framework import ( - HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, - status, views + HTTP_HEADER_ENCODING, RemovedInDRF310Warning, authentication, generics, + permissions, serializers, status, views ) from rest_framework.compat import PY36, is_guardian_installed, mock from rest_framework.filters import DjangoObjectPermissionsFilter @@ -427,7 +427,7 @@ class ObjectPermissionsIntegrationTests(TestCase): message = ("`DjangoObjectPermissionsFilter` has been deprecated and moved " "to the 3rd-party django-rest-framework-guardian package.") self.assertEqual(len(w), 1) - self.assertIs(w[-1].category, DeprecationWarning) + self.assertIs(w[-1].category, RemovedInDRF310Warning) self.assertEqual(str(w[-1].message), message) def test_can_read_list_permissions(self): diff --git a/tests/test_routers.py b/tests/test_routers.py index c74055347..a3a731f93 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -10,7 +10,9 @@ from django.db import models from django.test import TestCase, override_settings from django.urls import resolve, reverse -from rest_framework import permissions, serializers, viewsets +from rest_framework import ( + RemovedInDRF311Warning, permissions, serializers, viewsets +) from rest_framework.compat import get_regex_pattern from rest_framework.decorators import action from rest_framework.response import Response @@ -508,7 +510,7 @@ class TestBaseNameRename(TestCase): def test_base_name_argument_deprecation(self): router = SimpleRouter() - with pytest.warns(PendingDeprecationWarning) as w: + with pytest.warns(RemovedInDRF311Warning) as w: warnings.simplefilter('always') router.register('mock', MockViewSet, base_name='mock') @@ -535,7 +537,7 @@ class TestBaseNameRename(TestCase): msg = "`CustomRouter.get_default_base_name` method should be renamed `get_default_basename`." # Class definition should raise a warning - with pytest.warns(PendingDeprecationWarning) as w: + with pytest.warns(RemovedInDRF311Warning) as w: warnings.simplefilter('always') class CustomRouter(SimpleRouter): From 7eac86688a73503c6668568ad564f6b2539049f0 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 3 Mar 2019 10:39:08 -0800 Subject: [PATCH 129/185] Remove executable bit from static assets (#6484) These files are simply static assets and do not require an executable bit. They are never intended to be executed as standalone scripts. --- docs_theme/css/bootstrap-responsive.css | 0 docs_theme/css/bootstrap.css | 0 docs_theme/js/bootstrap-2.1.1-min.js | 0 .../docs/css/jquery.json-view.min.css | 0 .../rest_framework/docs/js/jquery.json-view.min.js | 0 .../rest_framework/fonts/fontawesome-webfont.eot | Bin .../rest_framework/fonts/fontawesome-webfont.svg | 0 .../rest_framework/fonts/fontawesome-webfont.ttf | Bin .../rest_framework/fonts/fontawesome-webfont.woff | Bin 9 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 docs_theme/css/bootstrap-responsive.css mode change 100755 => 100644 docs_theme/css/bootstrap.css mode change 100755 => 100644 docs_theme/js/bootstrap-2.1.1-min.js mode change 100755 => 100644 rest_framework/static/rest_framework/docs/css/jquery.json-view.min.css mode change 100755 => 100644 rest_framework/static/rest_framework/docs/js/jquery.json-view.min.js mode change 100755 => 100644 rest_framework/static/rest_framework/fonts/fontawesome-webfont.eot mode change 100755 => 100644 rest_framework/static/rest_framework/fonts/fontawesome-webfont.svg mode change 100755 => 100644 rest_framework/static/rest_framework/fonts/fontawesome-webfont.ttf mode change 100755 => 100644 rest_framework/static/rest_framework/fonts/fontawesome-webfont.woff diff --git a/docs_theme/css/bootstrap-responsive.css b/docs_theme/css/bootstrap-responsive.css old mode 100755 new mode 100644 diff --git a/docs_theme/css/bootstrap.css b/docs_theme/css/bootstrap.css old mode 100755 new mode 100644 diff --git a/docs_theme/js/bootstrap-2.1.1-min.js b/docs_theme/js/bootstrap-2.1.1-min.js old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/docs/css/jquery.json-view.min.css b/rest_framework/static/rest_framework/docs/css/jquery.json-view.min.css old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/docs/js/jquery.json-view.min.js b/rest_framework/static/rest_framework/docs/js/jquery.json-view.min.js old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/fonts/fontawesome-webfont.eot b/rest_framework/static/rest_framework/fonts/fontawesome-webfont.eot old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/fonts/fontawesome-webfont.svg b/rest_framework/static/rest_framework/fonts/fontawesome-webfont.svg old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/fonts/fontawesome-webfont.ttf b/rest_framework/static/rest_framework/fonts/fontawesome-webfont.ttf old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/fonts/fontawesome-webfont.woff b/rest_framework/static/rest_framework/fonts/fontawesome-webfont.woff old mode 100755 new mode 100644 From ac4c78967ad96ccf6c46e6464b6e9298cfbb7734 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 3 Mar 2019 15:33:16 +0100 Subject: [PATCH 130/185] Update version for v3.9.2 release. --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index eacc8dca0..55c06982d 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.9.1' +__version__ = '3.9.2' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' From 0ab527a3df39e344904f1e6543d76b6ce9b45c61 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 3 Mar 2019 15:46:57 +0100 Subject: [PATCH 131/185] Updated release notes for v3.9.2 --- docs/community/release-notes.md | 39 +++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 62ad95723..288bf6d58 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -40,9 +40,25 @@ You can determine your currently installed version using `pip show`: ## 3.9.x series -### 3.9.2 - IN DEVELOPMENT +### 3.9.2 -... +**Date**: [3rd March 2019][3.9.1-milestone] + +* Routers: invalidate `_urls` cache on `register()` [#6407][gh6407] +* Deferred schema renderer creation to avoid requiring pyyaml. [#6416][gh6416] +* Added 'request_forms' block to base.html [#6340][gh6340] +* Fixed SchemaView to reset renderer on exception. [#6429][gh6429] +* Update Django Guardian dependency. [#6430][gh6430] +* Ensured support for Django 2.2 [#6422][gh6422] & [#6455][gh6455] +* Made templates compatible with session-based CSRF. [#6207][gh6207] +* Adjusted field `validators` to accept non-list iterables. [#6282][gh6282] +* Added SearchFilter.get_search_fields() hook. [#6279][gh6279] +* Fix DeprecationWarning when accessing collections.abc classes via collections [#6268][gh6268] +* Allowed Q objects in limit_choices_to introspection. [#6472][gh6472] +* Added lazy evaluation to composed permissions. [#6463][gh6463] +* Add negation ~ operator to permissions composition [#6361][gh6361] +* Avoided calling distinct on annotated fields in SearchFilter. [#6240][gh6240] +* Introduced `RemovedInDRF…Warning` classes to simplify deprecations. [#6480][gh6480] ### 3.9.1 @@ -1149,6 +1165,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1 [3.9.0-milestone]: https://github.com/encode/django-rest-framework/milestone/66?closed=1 [3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/70?closed=1 +[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/71?closed=1 [gh2013]: https://github.com/encode/django-rest-framework/issues/2013 @@ -2071,3 +2088,21 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh6330]: https://github.com/encode/django-rest-framework/issues/6330 [gh6299]: https://github.com/encode/django-rest-framework/issues/6299 [gh6371]: https://github.com/encode/django-rest-framework/issues/6371 + + +[gh6480]: https://github.com/encode/django-rest-framework/issues/6480 +[gh6240]: https://github.com/encode/django-rest-framework/issues/6240 +[gh6361]: https://github.com/encode/django-rest-framework/issues/6361 +[gh6463]: https://github.com/encode/django-rest-framework/issues/6463 +[gh6472]: https://github.com/encode/django-rest-framework/issues/6472 +[gh6268]: https://github.com/encode/django-rest-framework/issues/6268 +[gh6279]: https://github.com/encode/django-rest-framework/issues/6279 +[gh6282]: https://github.com/encode/django-rest-framework/issues/6282 +[gh6207]: https://github.com/encode/django-rest-framework/issues/6207 +[gh6455]: https://github.com/encode/django-rest-framework/issues/6455 +[gh6422]: https://github.com/encode/django-rest-framework/issues/6422 +[gh6430]: https://github.com/encode/django-rest-framework/issues/6430 +[gh6429]: https://github.com/encode/django-rest-framework/issues/6429 +[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 From ac7b20cca2f7189f44fd3b86a9339d8f1763f17a Mon Sep 17 00:00:00 2001 From: SrdjanCosicPrica Date: Mon, 4 Mar 2019 14:46:14 +0100 Subject: [PATCH 132/185] Fix get_search_fields example (#6487) --- docs/api-guide/filtering.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index aff267818..8a500f386 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -220,10 +220,13 @@ By default, the search parameter is named `'search`', but this may be overridden To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request: - class CustomSearchFilter(self, view, request): - if request.query_params.get('title_only'): - return ('title',) - return super(CustomSearchFilter, self).get_search_fields(view, request) + from rest_framework import filters + + class CustomSearchFilter(filters.SearchFilter): + 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) For more details, see the [Django documentation][search-django-admin]. From dfc277cce669af8c574c5fd0ac0a3a432b6d45e6 Mon Sep 17 00:00:00 2001 From: Luoxzhg Date: Tue, 5 Mar 2019 18:50:13 +0800 Subject: [PATCH 133/185] Corrected tutorial 1 example renderer output to bytes. (#6486) --- docs/tutorial/1-serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index ec507df05..07ee8f208 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -150,7 +150,7 @@ At this point we've translated the model instance into Python native datatypes. content = JSONRenderer().render(serializer.data) content - # '{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}' + # b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}' Deserialization is similar. First we parse a stream into Python native datatypes... From 9d06e43d05abf1ec57f15566b29ad53ac418ae05 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 5 Mar 2019 03:11:46 -0800 Subject: [PATCH 134/185] Replace type('') with six.text_type (#6482) As all source files import unicode_literals, type('') is always equivalent to six.text_type (str on Python 3 and unicode on Python 2). Removes the need to call type(), is more explicit, and will be easier to catch places to change for when it is time to eventually drop Python 2. --- rest_framework/fields.py | 4 ++-- rest_framework/relations.py | 2 +- tests/test_pagination.py | 9 +++++---- tests/test_validation.py | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b5fafeaa3..c8f65db0e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1486,7 +1486,7 @@ class MultipleChoiceField(ChoiceField): return dictionary.get(self.field_name, empty) def to_internal_value(self, data): - if isinstance(data, type('')) or not hasattr(data, '__iter__'): + if isinstance(data, six.text_type) 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') @@ -1660,7 +1660,7 @@ class ListField(Field): """ if html.is_html_input(data): data = html.parse_html_list(data, default=[]) - if isinstance(data, (type(''), Mapping)) or not hasattr(data, '__iter__'): + if isinstance(data, (six.text_type, 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') diff --git a/rest_framework/relations.py b/rest_framework/relations.py index e8a4ec2ac..31c1e7561 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -518,7 +518,7 @@ class ManyRelatedField(Field): return dictionary.get(self.field_name, empty) def to_internal_value(self, data): - if isinstance(data, type('')) or not hasattr(data, '__iter__'): + if isinstance(data, six.text_type) 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/tests/test_pagination.py b/tests/test_pagination.py index d9ad9e6f6..6d940fe2b 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -5,6 +5,7 @@ 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 @@ -207,7 +208,7 @@ class TestPageNumberPagination: ] } assert self.pagination.display_page_controls - assert isinstance(self.pagination.to_html(), type('')) + assert isinstance(self.pagination.to_html(), six.text_type) def test_second_page(self): request = Request(factory.get('/', {'page': 2})) @@ -313,7 +314,7 @@ class TestPageNumberPaginationOverride: ] } assert not self.pagination.display_page_controls - assert isinstance(self.pagination.to_html(), type('')) + assert isinstance(self.pagination.to_html(), six.text_type) def test_invalid_page(self): request = Request(factory.get('/', {'page': 'invalid'})) @@ -368,7 +369,7 @@ class TestLimitOffset: ] } assert self.pagination.display_page_controls - assert isinstance(self.pagination.to_html(), type('')) + assert isinstance(self.pagination.to_html(), six.text_type) def test_pagination_not_applied_if_limit_or_default_limit_not_set(self): class MockPagination(pagination.LimitOffsetPagination): @@ -631,7 +632,7 @@ class CursorPaginationTestsMixin: assert current == [1, 1, 1, 1, 1] assert next == [1, 2, 3, 4, 4] - assert isinstance(self.pagination.to_html(), type('')) + assert isinstance(self.pagination.to_html(), six.text_type) def test_cursor_pagination_with_page_size(self): (previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=20') diff --git a/tests/test_validation.py b/tests/test_validation.py index 8b71693c5..4132a7b00 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -5,6 +5,7 @@ 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 @@ -111,7 +112,7 @@ class TestAvoidValidation(TestCase): assert not serializer.is_valid() assert serializer.errors == { 'non_field_errors': [ - 'Invalid data. Expected a dictionary, but got %s.' % type('').__name__ + 'Invalid data. Expected a dictionary, but got %s.' % six.text_type.__name__ ] } From 9e1e32f678fad5b0222b4cec092abfd34c2d8fa0 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 7 Mar 2019 15:22:00 +0600 Subject: [PATCH 135/185] upgraded pytest dependencies (#6492) --- requirements/requirements-testing.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index fbddc4f20..a2a2fa753 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,4 +1,4 @@ # Pytest for running the tests. -pytest==3.6.2 -pytest-django==3.3.2 -pytest-cov==2.5.1 +pytest==4.3.0 +pytest-django==3.4.8 +pytest-cov==2.6.1 From fd32dd7ca4fa584efc31e1d913bbca7ffbd1c586 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 7 Mar 2019 01:44:20 -0800 Subject: [PATCH 136/185] Explicitly raise exc in 'raise_uncaught_exception' (#6435) --- rest_framework/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 04951ed93..9d5d959e9 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -463,7 +463,7 @@ class APIView(View): renderer_format = getattr(request.accepted_renderer, 'format') use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin') request.force_plaintext_errors(use_plaintext_traceback) - raise + raise exc # Note: Views are made CSRF exempt from within `as_view` as to prevent # accidental removal of this exemption in cases where `dispatch` needs to From 86c72bb2268ad06c503de4fb7caa46c9e495f8d8 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Thu, 7 Mar 2019 05:26:03 -0500 Subject: [PATCH 137/185] 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 138/185] 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 139/185] 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 140/185] 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 141/185] 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 142/185] 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 143/185] 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 144/185] 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 145/185] 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 146/185] 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 147/185] 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 148/185] 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 149/185] 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 150/185] 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 151/185] 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 152/185] 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 153/185] 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 154/185] 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 155/185] 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 156/185] 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 157/185] 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 158/185] 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 159/185] 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 160/185] 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 161/185] 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 162/185] 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 '