From 3bbe22dafcc69c5ffa79707f5a74eb1faf466e12 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 18 Jan 2023 09:46:01 +0100 Subject: [PATCH] Fixed #34233 -- Dropped support for Python 3.8 and 3.9. --- .github/workflows/schedule_tests.yml | 2 - INSTALL | 2 +- django/contrib/auth/hashers.py | 5 +- django/contrib/staticfiles/storage.py | 2 +- django/core/cache/backends/filebased.py | 2 +- django/core/cache/utils.py | 2 +- django/core/handlers/asgi.py | 2 +- django/core/validators.py | 10 --- django/db/backends/base/base.py | 9 +-- django/db/backends/sqlite3/_functions.py | 9 +-- django/db/backends/utils.py | 2 +- django/templatetags/tz.py | 6 +- django/test/runner.py | 4 +- django/test/testcases.py | 27 ------- django/utils/asyncio.py | 25 ------ django/utils/cache.py | 2 +- django/utils/crypto.py | 17 ---- django/utils/http.py | 80 +------------------ django/utils/timezone.py | 7 +- docs/howto/windows.txt | 4 +- .../contributing/writing-code/unit-tests.txt | 10 +-- docs/intro/reusable-apps.txt | 6 +- docs/intro/tutorial01.txt | 2 +- docs/releases/5.0.txt | 2 + docs/topics/i18n/timezones.txt | 3 +- setup.cfg | 5 +- tests/admin_scripts/tests.py | 9 +-- tests/admin_views/tests.py | 6 +- tests/admin_widgets/tests.py | 6 +- .../datetime/test_extract_trunc.py | 6 +- tests/migrations/test_writer.py | 6 +- tests/requirements/py3.txt | 1 - tests/test_utils/tests.py | 43 ---------- tests/timezones/tests.py | 6 +- tests/utils_tests/test_autoreload.py | 6 +- tests/utils_tests/test_module_loading.py | 34 ++------ tests/utils_tests/test_timezone.py | 6 +- tox.ini | 2 +- 38 files changed, 51 insertions(+), 327 deletions(-) diff --git a/.github/workflows/schedule_tests.yml b/.github/workflows/schedule_tests.yml index d758642ef778..84183ca39e6d 100644 --- a/.github/workflows/schedule_tests.yml +++ b/.github/workflows/schedule_tests.yml @@ -16,8 +16,6 @@ jobs: strategy: matrix: python-version: - - '3.8' - - '3.9' - '3.10' - '3.11' - '3.12-dev' diff --git a/INSTALL b/INSTALL index cd9dd33274af..247b0bcdae7b 100644 --- a/INSTALL +++ b/INSTALL @@ -1,6 +1,6 @@ Thanks for downloading Django. -To install it, make sure you have Python 3.8 or greater installed. Then run +To install it, make sure you have Python 3.10 or greater installed. Then run this command from the command prompt: python -m pip install . diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index e72a4ebe795e..31f8a309d493 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -14,7 +14,6 @@ RANDOM_STRING_CHARS, constant_time_compare, get_random_string, - md5, pbkdf2, ) from django.utils.deprecation import RemovedInDjango51Warning @@ -684,7 +683,7 @@ class MD5PasswordHasher(BasePasswordHasher): def encode(self, password, salt): self._check_encode_args(password, salt) - hash = md5((salt + password).encode()).hexdigest() + hash = hashlib.md5((salt + password).encode()).hexdigest() return "%s$%s$%s" % (self.algorithm, salt, hash) def decode(self, encoded): @@ -799,7 +798,7 @@ def salt(self): def encode(self, password, salt): if salt != "": raise ValueError("salt must be empty.") - return md5(password.encode()).hexdigest() + return hashlib.md5(password.encode()).hexdigest() def decode(self, encoded): return { diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index 445cf6b954b6..b3ba21f2b2b0 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -2,6 +2,7 @@ import os import posixpath import re +from hashlib import md5 from urllib.parse import unquote, urldefrag, urlsplit, urlunsplit from django.conf import STATICFILES_STORAGE_ALIAS, settings @@ -9,7 +10,6 @@ from django.core.exceptions import ImproperlyConfigured from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage, storages -from django.utils.crypto import md5 from django.utils.functional import LazyObject diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py index 215fefbcc064..29d49c0ede9f 100644 --- a/django/core/cache/backends/filebased.py +++ b/django/core/cache/backends/filebased.py @@ -6,11 +6,11 @@ import tempfile import time import zlib +from hashlib import md5 from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache from django.core.files import locks from django.core.files.move import file_move_safe -from django.utils.crypto import md5 class FileBasedCache(BaseCache): diff --git a/django/core/cache/utils.py b/django/core/cache/utils.py index ff2a23aa6f65..87f0f9cb0905 100644 --- a/django/core/cache/utils.py +++ b/django/core/cache/utils.py @@ -1,4 +1,4 @@ -from django.utils.crypto import md5 +from hashlib import md5 TEMPLATE_FRAGMENT_KEY_TEMPLATE = "template.cache.%s.%s" diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py index f0125e7321e0..998d135691af 100644 --- a/django/core/handlers/asgi.py +++ b/django/core/handlers/asgi.py @@ -2,6 +2,7 @@ import sys import tempfile import traceback +from contextlib import aclosing from asgiref.sync import ThreadSensitiveContext, sync_to_async @@ -19,7 +20,6 @@ parse_cookie, ) from django.urls import set_script_prefix -from django.utils.asyncio import aclosing from django.utils.functional import cached_property logger = logging.getLogger("django.request") diff --git a/django/core/validators.py b/django/core/validators.py index c73490588de4..6c622f57887c 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -275,16 +275,6 @@ def validate_ipv4_address(value): raise ValidationError( _("Enter a valid IPv4 address."), code="invalid", params={"value": value} ) - else: - # Leading zeros are forbidden to avoid ambiguity with the octal - # notation. This restriction is included in Python 3.9.5+. - # TODO: Remove when dropping support for PY39. - if any(octet != "0" and octet[0] == "0" for octet in value.split(".")): - raise ValidationError( - _("Enter a valid IPv4 address."), - code="invalid", - params={"value": value}, - ) def validate_ipv6_address(value): diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py index 5f2e7bcd4d96..84b9974b40b7 100644 --- a/django/db/backends/base/base.py +++ b/django/db/backends/base/base.py @@ -5,22 +5,17 @@ import threading import time import warnings +import zoneinfo from collections import deque from contextlib import contextmanager -from django.db.backends.utils import debug_transaction - -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.db import DEFAULT_DB_ALIAS, DatabaseError, NotSupportedError from django.db.backends import utils from django.db.backends.base.validation import BaseDatabaseValidation from django.db.backends.signals import connection_created +from django.db.backends.utils import debug_transaction from django.db.transaction import TransactionManagementError from django.db.utils import DatabaseErrorWrapper from django.utils.asyncio import async_unsafe diff --git a/django/db/backends/sqlite3/_functions.py b/django/db/backends/sqlite3/_functions.py index c60549f8afa8..9c1ef4a30ad3 100644 --- a/django/db/backends/sqlite3/_functions.py +++ b/django/db/backends/sqlite3/_functions.py @@ -4,8 +4,9 @@ import functools import random import statistics +import zoneinfo from datetime import timedelta -from hashlib import sha1, sha224, sha256, sha384, sha512 +from hashlib import md5, sha1, sha224, sha256, sha384, sha512 from math import ( acos, asin, @@ -32,14 +33,8 @@ typecast_timestamp, ) from django.utils import timezone -from django.utils.crypto import md5 from django.utils.duration import duration_microseconds -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - def register(connection): create_deterministic_function = functools.partial( diff --git a/django/db/backends/utils.py b/django/db/backends/utils.py index df6532e81fd7..71c1f2c7e870 100644 --- a/django/db/backends/utils.py +++ b/django/db/backends/utils.py @@ -4,9 +4,9 @@ import logging import time from contextlib import contextmanager +from hashlib import md5 from django.db import NotSupportedError -from django.utils.crypto import md5 from django.utils.dateparse import parse_time logger = logging.getLogger("django.db.backends") diff --git a/django/templatetags/tz.py b/django/templatetags/tz.py index 92240b2a39d5..f2cee2d3fe87 100644 --- a/django/templatetags/tz.py +++ b/django/templatetags/tz.py @@ -1,12 +1,8 @@ +import zoneinfo from datetime import datetime from datetime import timezone as datetime_timezone from datetime import tzinfo -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - from django.template import Library, Node, TemplateSyntaxError from django.utils import timezone diff --git a/django/test/runner.py b/django/test/runner.py index 4232e82e9b9a..014d4ea46422 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -1,6 +1,7 @@ import argparse import ctypes import faulthandler +import hashlib import io import itertools import logging @@ -27,7 +28,6 @@ from django.test.utils import setup_test_environment from django.test.utils import teardown_databases as _teardown_databases from django.test.utils import teardown_test_environment -from django.utils.crypto import new_hash from django.utils.datastructures import OrderedSet try: @@ -580,7 +580,7 @@ class Shuffler: @classmethod def _hash_text(cls, text): - h = new_hash(cls.hash_algorithm, usedforsecurity=False) + h = hashlib.new(cls.hash_algorithm, usedforsecurity=False) h.update(text.encode("utf-8")) return h.hexdigest() diff --git a/django/test/testcases.py b/django/test/testcases.py index 23148537f517..017c6eefd0d3 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -53,7 +53,6 @@ ) from django.utils.deprecation import RemovedInDjango51Warning from django.utils.functional import classproperty -from django.utils.version import PY310 from django.views.static import serve logger = logging.getLogger("django.test") @@ -795,32 +794,6 @@ def assertWarnsMessage(self, expected_warning, expected_message, *args, **kwargs **kwargs, ) - # A similar method is available in Python 3.10+. - if not PY310: - - @contextmanager - def assertNoLogs(self, logger, level=None): - """ - Assert no messages are logged on the logger, with at least the - given level. - """ - if isinstance(level, int): - level = logging.getLevelName(level) - elif level is None: - level = "INFO" - try: - with self.assertLogs(logger, level) as cm: - yield - except AssertionError as e: - msg = e.args[0] - expected_msg = ( - f"no logs of level {level} or higher triggered on {logger}" - ) - if msg != expected_msg: - raise e - else: - self.fail(f"Unexpected logs found: {cm.output!r}") - def assertFieldOutput( self, fieldclass, diff --git a/django/utils/asyncio.py b/django/utils/asyncio.py index eea2df48e27c..1e79f90c2c1b 100644 --- a/django/utils/asyncio.py +++ b/django/utils/asyncio.py @@ -37,28 +37,3 @@ def inner(*args, **kwargs): return decorator(func) else: return decorator - - -try: - from contextlib import aclosing -except ImportError: - # TODO: Remove when dropping support for PY39. - from contextlib import AbstractAsyncContextManager - - # Backport of contextlib.aclosing() from Python 3.10. Copyright (C) Python - # Software Foundation (see LICENSE.python). - class aclosing(AbstractAsyncContextManager): - """ - Async context manager for safely finalizing an asynchronously - cleaned-up resource such as an async generator, calling its - ``aclose()`` method. - """ - - def __init__(self, thing): - self.thing = thing - - async def __aenter__(self): - return self.thing - - async def __aexit__(self, *exc_info): - await self.thing.aclose() diff --git a/django/utils/cache.py b/django/utils/cache.py index 2dd2c7796c39..d4574217f4c7 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -16,11 +16,11 @@ """ import time from collections import defaultdict +from hashlib import md5 from django.conf import settings from django.core.cache import caches from django.http import HttpResponse, HttpResponseNotModified -from django.utils.crypto import md5 from django.utils.http import http_date, parse_etags, parse_http_date_safe, quote_etag from django.utils.log import log_response from django.utils.regex_helper import _lazy_re_compile diff --git a/django/utils/crypto.py b/django/utils/crypto.py index 341cb742c150..1c0e7001c649 100644 --- a/django/utils/crypto.py +++ b/django/utils/crypto.py @@ -7,7 +7,6 @@ from django.conf import settings from django.utils.encoding import force_bytes -from django.utils.inspect import func_supports_parameter class InvalidAlgorithm(ValueError): @@ -75,19 +74,3 @@ def pbkdf2(password, salt, iterations, dklen=0, digest=None): password = force_bytes(password) salt = force_bytes(salt) return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen) - - -# TODO: Remove when dropping support for PY38. inspect.signature() is used to -# detect whether the usedforsecurity argument is available as this fix may also -# have been applied by downstream package maintainers to other versions in -# their repositories. -if func_supports_parameter(hashlib.md5, "usedforsecurity"): - md5 = hashlib.md5 - new_hash = hashlib.new -else: - - def md5(data=b"", *, usedforsecurity=True): - return hashlib.md5(data) - - def new_hash(hash_algorithm, *, usedforsecurity=True): - return hashlib.new(hash_algorithm) diff --git a/django/utils/http.py b/django/utils/http.py index 3e7acb583570..8fd40c27af80 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -4,18 +4,9 @@ import unicodedata from binascii import Error as BinasciiError from email.utils import formatdate -from urllib.parse import ( - ParseResult, - SplitResult, - _coerce_args, - _splitnetloc, - _splitparams, - quote, - scheme_chars, - unquote, -) +from urllib.parse import quote, unquote from urllib.parse import urlencode as original_urlencode -from urllib.parse import uses_params +from urllib.parse import urlparse from django.utils.datastructures import MultiValueDict from django.utils.regex_helper import _lazy_re_compile @@ -47,10 +38,6 @@ RFC3986_GENDELIMS = ":/?#[]@" RFC3986_SUBDELIMS = "!$&'()*+,;=" -# TODO: Remove when dropping support for PY38. -# Unsafe bytes to be removed per WHATWG spec. -_UNSAFE_URL_BYTES_TO_REMOVE = ["\t", "\r", "\n"] - def urlencode(query, doseq=False): """ @@ -283,74 +270,13 @@ def url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False): ) -# TODO: Remove when dropping support for PY38. -# Copied from urllib.parse.urlparse() but uses fixed urlsplit() function. -def _urlparse(url, scheme="", allow_fragments=True): - """Parse a URL into 6 components: - :///;?# - Return a 6-tuple: (scheme, netloc, path, params, query, fragment). - Note that we don't break the components up in smaller bits - (e.g. netloc is a single string) and we don't expand % escapes.""" - url, scheme, _coerce_result = _coerce_args(url, scheme) - splitresult = _urlsplit(url, scheme, allow_fragments) - scheme, netloc, url, query, fragment = splitresult - if scheme in uses_params and ";" in url: - url, params = _splitparams(url) - else: - params = "" - result = ParseResult(scheme, netloc, url, params, query, fragment) - return _coerce_result(result) - - -# TODO: Remove when dropping support for PY38. -def _remove_unsafe_bytes_from_url(url): - for b in _UNSAFE_URL_BYTES_TO_REMOVE: - url = url.replace(b, "") - return url - - -# TODO: Remove when dropping support for PY38. -# Backport of urllib.parse.urlsplit() from Python 3.9. -def _urlsplit(url, scheme="", allow_fragments=True): - """Parse a URL into 5 components: - :///?# - Return a 5-tuple: (scheme, netloc, path, query, fragment). - Note that we don't break the components up in smaller bits - (e.g. netloc is a single string) and we don't expand % escapes.""" - url, scheme, _coerce_result = _coerce_args(url, scheme) - url = _remove_unsafe_bytes_from_url(url) - scheme = _remove_unsafe_bytes_from_url(scheme) - - netloc = query = fragment = "" - i = url.find(":") - if i > 0: - for c in url[:i]: - if c not in scheme_chars: - break - else: - scheme, url = url[:i].lower(), url[i + 1 :] - - if url[:2] == "//": - netloc, url = _splitnetloc(url, 2) - if ("[" in netloc and "]" not in netloc) or ( - "]" in netloc and "[" not in netloc - ): - raise ValueError("Invalid IPv6 URL") - if allow_fragments and "#" in url: - url, fragment = url.split("#", 1) - if "?" in url: - url, query = url.split("?", 1) - v = SplitResult(scheme, netloc, url, query, fragment) - return _coerce_result(v) - - def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False): # Chrome considers any URL with more than two slashes to be absolute, but # urlparse is not so flexible. Treat any url with three slashes as unsafe. if url.startswith("///"): return False try: - url_info = _urlparse(url) + url_info = urlparse(url) except ValueError: # e.g. invalid IPv6 addresses return False # Forbid URLs like http:///example.com - with a scheme, but without a hostname. diff --git a/django/utils/timezone.py b/django/utils/timezone.py index ca9c81734572..102562b254a3 100644 --- a/django/utils/timezone.py +++ b/django/utils/timezone.py @@ -3,12 +3,7 @@ """ import functools - -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - +import zoneinfo from contextlib import ContextDecorator from datetime import datetime, timedelta, timezone, tzinfo diff --git a/docs/howto/windows.txt b/docs/howto/windows.txt index 5bd5b3594ada..fecb2fd790a6 100644 --- a/docs/howto/windows.txt +++ b/docs/howto/windows.txt @@ -4,7 +4,7 @@ How to install Django on Windows .. highlight:: doscon -This document will guide you through installing Python 3.8 and Django on +This document will guide you through installing Python 3.11 and Django on Windows. It also provides instructions for setting up a virtual environment, which makes it easier to work on Python projects. This is meant as a beginner's guide for users working on Django projects and does not reflect how Django @@ -20,7 +20,7 @@ Install Python ============== Django is a Python web framework, thus requiring Python to be installed on your -machine. At the time of writing, Python 3.8 is the latest version. +machine. At the time of writing, Python 3.11 is the latest version. To install Python on your machine go to https://www.python.org/downloads/. The website should offer you a download button for the latest Python version. diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index ba7209991c15..eb0506e5eebc 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -91,14 +91,14 @@ In addition to the default environments, ``tox`` supports running unit tests for other versions of Python and other database backends. Since Django's test suite doesn't bundle a settings file for database backends other than SQLite, however, you must :ref:`create and provide your own test settings -`. For example, to run the tests on Python 3.9 +`. For example, to run the tests on Python 3.10 using PostgreSQL: .. console:: - $ tox -e py39-postgres -- --settings=my_postgres_settings + $ tox -e py310-postgres -- --settings=my_postgres_settings -This command sets up a Python 3.9 virtual environment, installs Django's +This command sets up a Python 3.10 virtual environment, installs Django's test suite dependencies (including those for PostgreSQL), and calls ``runtests.py`` with the supplied arguments (in this case, ``--settings=my_postgres_settings``). @@ -113,14 +113,14 @@ above: .. code-block:: console - $ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py39-postgres + $ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py310-postgres Windows users should use: .. code-block:: doscon ...\> set DJANGO_SETTINGS_MODULE=my_postgres_settings - ...\> tox -e py39-postgres + ...\> tox -e py310-postgres Running the JavaScript tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt index f0fa4b130648..ed05f88a7514 100644 --- a/docs/intro/reusable-apps.txt +++ b/docs/intro/reusable-apps.txt @@ -220,15 +220,15 @@ this. For a small app like polls, this process isn't too difficult. Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Topic :: Internet :: WWW/HTTP Topic :: Internet :: WWW/HTTP :: Dynamic Content [options] include_package_data = true packages = find: - python_requires = >=3.8 + python_requires = >=3.10 install_requires = Django >= X.Y # Replace "X.Y" as appropriate diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index a308339fa9cc..4f9dc67da50c 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -23,7 +23,7 @@ in a shell prompt (indicated by the $ prefix): If Django is installed, you should see the version of your installation. If it isn't, you'll get an error telling "No module named django". -This tutorial is written for Django |version|, which supports Python 3.8 and +This tutorial is written for Django |version|, which supports Python 3.10 and later. If the Django version doesn't match, you can refer to the tutorial for your version of Django by using the version switcher at the bottom right corner of this page, or update Django to the newest version. If you're using an older diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index cd54b03cf725..78148c79e862 100644 --- a/docs/releases/5.0.txt +++ b/docs/releases/5.0.txt @@ -21,6 +21,8 @@ Python compatibility Django 5.0 supports Python 3.10, 3.11, and 3.12. We **highly recommend** and only officially support the latest release of each series. +The Django 4.2.x series is the last to support Python 3.8 and 3.9. + Third-party library support for older version of Django ======================================================= diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index 10170fcdea76..19415a13b6f3 100644 --- a/docs/topics/i18n/timezones.txt +++ b/docs/topics/i18n/timezones.txt @@ -32,8 +32,7 @@ False ` in your settings file. In older version, time zone support was disabled by default. Time zone support uses :mod:`zoneinfo`, which is part of the Python standard -library from Python 3.9. The ``backports.zoneinfo`` package is automatically -installed alongside Django if you are using Python 3.8. +library from Python 3.9. If you're wrestling with a particular problem, start with the :ref:`time zone FAQ `. diff --git a/setup.cfg b/setup.cfg index afef79c2abcf..529bc6044f3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,8 +17,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Topic :: Internet :: WWW/HTTP @@ -34,13 +32,12 @@ project_urls = Tracker = https://code.djangoproject.com/ [options] -python_requires = >=3.8 +python_requires = >=3.10 packages = find: include_package_data = true zip_safe = false install_requires = asgiref >= 3.6.0 - backports.zoneinfo; python_version<"3.9" sqlparse >= 0.2.2 tzdata; sys_platform == 'win32' diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 4149a31e2195..51d0498ff793 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -32,7 +32,6 @@ from django.test import LiveServerTestCase, SimpleTestCase, TestCase, override_settings from django.test.utils import captured_stderr, captured_stdout from django.urls import path -from django.utils.version import PY39 from django.views.static import serve from . import urls @@ -107,7 +106,7 @@ def _ext_backend_paths(self): paths.append(os.path.dirname(backend_dir)) return paths - def run_test(self, args, settings_file=None, apps=None, umask=None): + def run_test(self, args, settings_file=None, apps=None, umask=-1): base_dir = os.path.dirname(self.test_dir) # The base dir for Django's tests is one level up. tests_dir = os.path.dirname(os.path.dirname(__file__)) @@ -136,12 +135,11 @@ def run_test(self, args, settings_file=None, apps=None, umask=None): cwd=self.test_dir, env=test_environ, text=True, - # subprocess.run()'s umask was added in Python 3.9. - **({"umask": umask} if umask and PY39 else {}), + umask=umask, ) return p.stdout, p.stderr - def run_django_admin(self, args, settings_file=None, umask=None): + def run_django_admin(self, args, settings_file=None, umask=-1): return self.run_test(["-m", "django", *args], settings_file, umask=umask) def run_manage(self, args, settings_file=None, manage_py=None): @@ -2812,7 +2810,6 @@ def test_custom_project_template_exclude_directory(self): sys.platform == "win32", "Windows only partially supports umasks and chmod.", ) - @unittest.skipUnless(PY39, "subprocess.run()'s umask was added in Python 3.9.") def test_honor_umask(self): _, err = self.run_django_admin(["startproject", "testproject"], umask=0o077) self.assertNoOutput(err) diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index f7764a7f360a..86fb9740a508 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -2,14 +2,10 @@ import os import re import unittest +import zoneinfo from unittest import mock from urllib.parse import parse_qsl, urljoin, urlparse -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - from django.contrib import admin from django.contrib.admin import AdminSite, ModelAdmin from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 70b1233ef4fd..4948a60ee095 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -1,15 +1,11 @@ import gettext import os import re +import zoneinfo from datetime import datetime, timedelta from importlib import import_module from unittest import skipUnless -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - from django import forms from django.conf import settings from django.contrib import admin diff --git a/tests/db_functions/datetime/test_extract_trunc.py b/tests/db_functions/datetime/test_extract_trunc.py index 80043fe3f4e2..29212b6e2440 100644 --- a/tests/db_functions/datetime/test_extract_trunc.py +++ b/tests/db_functions/datetime/test_extract_trunc.py @@ -1,11 +1,7 @@ +import zoneinfo from datetime import datetime, timedelta from datetime import timezone as datetime_timezone -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - from django.conf import settings from django.db import DataError, OperationalError from django.db.models import ( diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 9c026ffb8206..0762c43dde5e 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -8,13 +8,9 @@ import re import sys import uuid +import zoneinfo from unittest import mock -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - import custom_migration_operations.more_operations import custom_migration_operations.operations diff --git a/tests/requirements/py3.txt b/tests/requirements/py3.txt index 397aaa06b181..3cc35b8c613f 100644 --- a/tests/requirements/py3.txt +++ b/tests/requirements/py3.txt @@ -1,7 +1,6 @@ aiosmtpd asgiref >= 3.6.0 argon2-cffi >= 19.2.0 -backports.zoneinfo; python_version < '3.9' bcrypt black docutils diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index aa58b47a94e8..be95f5ec038e 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -1,4 +1,3 @@ -import logging import os import unittest import warnings @@ -47,7 +46,6 @@ ) from django.urls import NoReverseMatch, path, reverse, reverse_lazy from django.utils.deprecation import RemovedInDjango51Warning -from django.utils.log import DEFAULT_LOGGING from django.utils.version import PY311 from .models import Car, Person, PossessedCar @@ -1198,47 +1196,6 @@ def func1(): func1() -# TODO: Remove when dropping support for PY39. -class AssertNoLogsTest(SimpleTestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - logging.config.dictConfig(DEFAULT_LOGGING) - cls.addClassCleanup(logging.config.dictConfig, settings.LOGGING) - - def setUp(self): - self.logger = logging.getLogger("django") - - @override_settings(DEBUG=True) - def test_fails_when_log_emitted(self): - msg = "Unexpected logs found: ['INFO:django:FAIL!']" - with self.assertRaisesMessage(AssertionError, msg): - with self.assertNoLogs("django", "INFO"): - self.logger.info("FAIL!") - - @override_settings(DEBUG=True) - def test_text_level(self): - with self.assertNoLogs("django", "INFO"): - self.logger.debug("DEBUG logs are ignored.") - - @override_settings(DEBUG=True) - def test_int_level(self): - with self.assertNoLogs("django", logging.INFO): - self.logger.debug("DEBUG logs are ignored.") - - @override_settings(DEBUG=True) - def test_default_level(self): - with self.assertNoLogs("django"): - self.logger.debug("DEBUG logs are ignored.") - - @override_settings(DEBUG=True) - def test_does_not_hide_other_failures(self): - msg = "1 != 2" - with self.assertRaisesMessage(AssertionError, msg): - with self.assertNoLogs("django"): - self.assertEqual(1, 2) - - class AssertFieldOutputTests(SimpleTestCase): def test_assert_field_output(self): error_invalid = ["Enter a valid email address."] diff --git a/tests/timezones/tests.py b/tests/timezones/tests.py index 56784db207cf..e3e2ea431ee8 100644 --- a/tests/timezones/tests.py +++ b/tests/timezones/tests.py @@ -1,15 +1,11 @@ import datetime import re import sys +import zoneinfo from contextlib import contextmanager from unittest import SkipTest, skipIf from xml.dom.minidom import parseString -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - from django.contrib.auth.models import User from django.core import serializers from django.db import connection diff --git a/tests/utils_tests/test_autoreload.py b/tests/utils_tests/test_autoreload.py index b6286925b766..e33276ba6121 100644 --- a/tests/utils_tests/test_autoreload.py +++ b/tests/utils_tests/test_autoreload.py @@ -9,16 +9,12 @@ import types import weakref import zipfile +import zoneinfo from importlib import import_module from pathlib import Path from subprocess import CompletedProcess from unittest import mock, skip, skipIf -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - import django.__main__ from django.apps.registry import Apps from django.test import SimpleTestCase diff --git a/tests/utils_tests/test_module_loading.py b/tests/utils_tests/test_module_loading.py index 736d456d2db5..80ada3abd788 100644 --- a/tests/utils_tests/test_module_loading.py +++ b/tests/utils_tests/test_module_loading.py @@ -11,7 +11,6 @@ import_string, module_has_submodule, ) -from django.utils.version import PY310 class DefaultLoader(unittest.TestCase): @@ -205,35 +204,12 @@ def test_validate_registry_resets_after_missing_module(self): self.assertEqual(site._registry, {"lorem": "ipsum"}) -if PY310: +class TestFinder: + def __init__(self, *args, **kwargs): + self.importer = zipimporter(*args, **kwargs) - class TestFinder: - def __init__(self, *args, **kwargs): - self.importer = zipimporter(*args, **kwargs) - - def find_spec(self, path, target=None): - return self.importer.find_spec(path, target) - -else: - - class TestFinder: - def __init__(self, *args, **kwargs): - self.importer = zipimporter(*args, **kwargs) - - def find_module(self, path): - importer = self.importer.find_module(path) - if importer is None: - return - return TestLoader(importer) - - class TestLoader: - def __init__(self, importer): - self.importer = importer - - def load_module(self, name): - mod = self.importer.load_module(name) - mod.__loader__ = self - return mod + def find_spec(self, path, target=None): + return self.importer.find_spec(path, target) class CustomLoader(EggLoader): diff --git a/tests/utils_tests/test_timezone.py b/tests/utils_tests/test_timezone.py index 931347ad46b4..43bb2bc7a342 100644 --- a/tests/utils_tests/test_timezone.py +++ b/tests/utils_tests/test_timezone.py @@ -1,11 +1,7 @@ import datetime +import zoneinfo from unittest import mock -try: - import zoneinfo -except ImportError: - from backports import zoneinfo - from django.test import SimpleTestCase, override_settings from django.utils import timezone diff --git a/tox.ini b/tox.ini index e3db75bf4a8d..98cab3abf444 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ passenv = DJANGO_SETTINGS_MODULE PYTHONPATH HOME DISPLAY OBJC_DISABLE_INITIALIZE setenv = PYTHONDONTWRITEBYTECODE=1 deps = - py{3,38,39,310,311}: -rtests/requirements/py3.txt + py{3,310,311}: -rtests/requirements/py3.txt postgres: -rtests/requirements/postgres.txt mysql: -rtests/requirements/mysql.txt oracle: -rtests/requirements/oracle.txt