diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 04f9939425..b986009f9b 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -325,7 +325,7 @@ Corresponds to `django.db.models.fields.DateTimeField`. * `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. +* `default_timezone` - A `tzinfo` subclass (`zoneinfo` or `pytz`) prepresenting 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. diff --git a/rest_framework/test.py b/rest_framework/test.py index e934eff55d..0212348ee0 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -3,6 +3,7 @@ import io from importlib import import_module +import django from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.handlers.wsgi import WSGIHandler @@ -357,6 +358,13 @@ class APILiveServerTestCase(testcases.LiveServerTestCase): client_class = APIClient +def cleanup_url_patterns(cls): + if hasattr(cls, '_module_urlpatterns'): + cls._module.urlpatterns = cls._module_urlpatterns + else: + del cls._module.urlpatterns + + class URLPatternsTestCase(testcases.SimpleTestCase): """ Isolate URL patterns on a per-TestCase basis. For example, @@ -385,14 +393,20 @@ def setUpClass(cls): cls._module.urlpatterns = cls.urlpatterns cls._override.enable() + + if django.VERSION > (4, 0): + cls.addClassCleanup(cls._override.disable) + cls.addClassCleanup(cleanup_url_patterns, cls) + super().setUpClass() - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls._override.disable() + if django.VERSION < (4, 0): + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls._override.disable() - if hasattr(cls, '_module_urlpatterns'): - cls._module.urlpatterns = cls._module_urlpatterns - else: - del cls._module.urlpatterns + if hasattr(cls, '_module_urlpatterns'): + cls._module.urlpatterns = cls._module_urlpatterns + else: + del cls._module.urlpatterns diff --git a/setup.py b/setup.py index d755a00fe2..b8e220cb43 100755 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ def get_version(package): author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=find_packages(exclude=['tests*']), include_package_data=True, - install_requires=["django>=2.2"], + install_requires=["django>=2.2", "pytz"], python_requires=">=3.5", zip_safe=False, classifiers=[ diff --git a/tests/authentication/test_authentication.py b/tests/authentication/test_authentication.py index a73e0d79c7..d771aaf8b4 100644 --- a/tests/authentication/test_authentication.py +++ b/tests/authentication/test_authentication.py @@ -1,5 +1,6 @@ import base64 +import django import pytest from django.conf import settings from django.contrib.auth.models import User @@ -218,7 +219,16 @@ def test_post_form_session_auth_passing_csrf(self): Ensure POSTing form over session authentication with CSRF token succeeds. Regression test for #6088 """ - from django.middleware.csrf import _get_new_csrf_token + # Remove this shim when dropping support for Django 2.2. + if django.VERSION < (3, 0): + from django.middleware.csrf import _get_new_csrf_token + else: + from django.middleware.csrf import ( + _get_new_csrf_string, _mask_cipher_secret + ) + + def _get_new_csrf_token(): + return _mask_cipher_secret(_get_new_csrf_string()) self.csrf_client.login(username=self.username, password=self.password) diff --git a/tests/conftest.py b/tests/conftest.py index cc32cc6373..79cabd5e1a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,8 @@ def pytest_addoption(parser): def pytest_configure(config): from django.conf import settings + # USE_L10N is deprecated, and will be removed in Django 5.0. + use_l10n = {"USE_L10N": True} if django.VERSION < (4, 0) else {} settings.configure( DEBUG_PROPAGATE_EXCEPTIONS=True, DATABASES={ @@ -33,7 +35,6 @@ def pytest_configure(config): SITE_ID=1, SECRET_KEY='not very secret in tests', USE_I18N=True, - USE_L10N=True, STATIC_URL='/static/', ROOT_URLCONF='tests.urls', TEMPLATES=[ @@ -68,6 +69,7 @@ def pytest_configure(config): PASSWORD_HASHERS=( 'django.contrib.auth.hashers.MD5PasswordHasher', ), + **use_l10n, ) # guardian is optional diff --git a/tests/test_fields.py b/tests/test_fields.py index 2d4cc44ae0..7a5304a82a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1220,12 +1220,12 @@ class TestNoStringCoercionDecimalField(FieldValues): class TestLocalizedDecimalField(TestCase): - @override_settings(USE_L10N=True, LANGUAGE_CODE='pl') + @override_settings(LANGUAGE_CODE='pl') def test_to_internal_value(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, localize=True) assert field.to_internal_value('1,1') == Decimal('1.1') - @override_settings(USE_L10N=True, LANGUAGE_CODE='pl') + @override_settings(LANGUAGE_CODE='pl') def test_to_representation(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, localize=True) assert field.to_representation(Decimal('1.1')) == '1,1' @@ -1464,15 +1464,24 @@ def setup_class(cls): cls.field = serializers.DateTimeField() cls.kolkata = pytz.timezone('Asia/Kolkata') + def assertUTC(self, tzinfo): + """ + Check UTC for datetime.timezone, ZoneInfo, and pytz tzinfo instances. + """ + assert ( + tzinfo is utc or + (getattr(tzinfo, "key", None) or getattr(tzinfo, "zone", None)) == "UTC" + ) + def test_default_timezone(self): - assert self.field.default_timezone() == utc + self.assertUTC(self.field.default_timezone()) def test_current_timezone(self): - assert self.field.default_timezone() == utc + self.assertUTC(self.field.default_timezone()) activate(self.kolkata) assert self.field.default_timezone() == self.kolkata deactivate() - assert self.field.default_timezone() == utc + self.assertUTC(self.field.default_timezone()) @pytest.mark.skipif(pytz is None, reason='pytz not installed') diff --git a/tests/test_testing.py b/tests/test_testing.py index cc60e4f003..5066ee142e 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,5 +1,6 @@ from io import BytesIO +import django from django.contrib.auth.models import User from django.shortcuts import redirect from django.test import TestCase, override_settings @@ -282,6 +283,10 @@ def test_empty_request_content_type(self): assert request.META['CONTENT_TYPE'] == 'application/json' +def check_urlpatterns(cls): + assert urlpatterns is not cls.urlpatterns + + class TestUrlPatternTestCase(URLPatternsTestCase): urlpatterns = [ path('', view), @@ -293,11 +298,18 @@ def setUpClass(cls): super().setUpClass() assert urlpatterns is cls.urlpatterns - @classmethod - def tearDownClass(cls): - assert urlpatterns is cls.urlpatterns - super().tearDownClass() - assert urlpatterns is not cls.urlpatterns + if django.VERSION > (4, 0): + cls.addClassCleanup( + check_urlpatterns, + cls + ) + + if django.VERSION < (4, 0): + @classmethod + def tearDownClass(cls): + assert urlpatterns is cls.urlpatterns + super().tearDownClass() + assert urlpatterns is not cls.urlpatterns def test_urlpatterns(self): assert self.client.get('/').status_code == 200 diff --git a/tox.ini b/tox.ini index 25f8418219..6f49d373fc 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = {py35,py36,py37}-django22, {py36,py37,py38,py39}-django31, {py36,py37,py38,py39}-django32, - {py38,py39}-djangomain, + {py38,py39}-{django40,djangomain}, base,dist,docs, [travis:env] @@ -11,6 +11,7 @@ DJANGO = 2.2: django22 3.1: django31 3.2: django32 + 4.0: django40 main: djangomain [testenv] @@ -23,6 +24,7 @@ deps = django22: Django>=2.2,<3.0 django31: Django>=3.1,<3.2 django32: Django>=3.2,<4.0 + django40: Django>=4.0a1,<5.0 djangomain: https://github.com/django/django/archive/main.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt