diff --git a/.github/workflows/schedule_tests.yml b/.github/workflows/schedule_tests.yml index d758642ef77..84183ca39e6 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 cd9dd33274a..247b0bcdae7 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 e72a4ebe795..31f8a309d49 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -14,7 +14,6 @@ from django.utils.crypto import ( 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 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher): 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 445cf6b954b..b3ba21f2b2b 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -2,6 +2,7 @@ import json 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.contrib.staticfiles.utils import check_settings, matches_patterns 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 215fefbcc06..29d49c0ede9 100644 --- a/django/core/cache/backends/filebased.py +++ b/django/core/cache/backends/filebased.py @@ -6,11 +6,11 @@ import random 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 ff2a23aa6f6..87f0f9cb090 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 f0125e7321e..998d135691a 100644 --- a/django/core/handlers/asgi.py +++ b/django/core/handlers/asgi.py @@ -2,6 +2,7 @@ import logging import sys import tempfile import traceback +from contextlib import aclosing from asgiref.sync import ThreadSensitiveContext, sync_to_async @@ -19,7 +20,6 @@ from django.http import ( 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 c73490588de..6c622f57887 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 5f2e7bcd4d9..84b9974b40b 100644 --- a/django/db/backends/base/base.py +++ b/django/db/backends/base/base.py @@ -5,22 +5,17 @@ import logging 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 c60549f8afa..9c1ef4a30ad 100644 --- a/django/db/backends/sqlite3/_functions.py +++ b/django/db/backends/sqlite3/_functions.py @@ -4,8 +4,9 @@ Implementations of SQL functions for SQLite. 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 @@ from django.db.backends.utils import ( 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 df6532e81fd..71c1f2c7e87 100644 --- a/django/db/backends/utils.py +++ b/django/db/backends/utils.py @@ -4,9 +4,9 @@ import functools 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 92240b2a39d..f2cee2d3fe8 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 4232e82e9b9..014d4ea4642 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_databases as _setup_databases 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 23148537f51..017c6eefd0d 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -53,7 +53,6 @@ from django.test.utils import ( ) 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 @@ class SimpleTestCase(unittest.TestCase): **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 eea2df48e27..1e79f90c2c1 100644 --- a/django/utils/asyncio.py +++ b/django/utils/asyncio.py @@ -37,28 +37,3 @@ def async_unsafe(message): 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 2dd2c7796c3..d4574217f4c 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -16,11 +16,11 @@ An example: i18n middleware would need to distinguish caches by the """ 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 341cb742c15..1c0e7001c64 100644 --- a/django/utils/crypto.py +++ b/django/utils/crypto.py @@ -7,7 +7,6 @@ import secrets 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 3e7acb58357..8fd40c27af8 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -4,18 +4,9 @@ import re 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 @@ ASCTIME_DATE = _lazy_re_compile(r"^\w{3} %s %s %s %s$" % (__M, __D2, __T, __Y)) 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 ca9c8173457..102562b254a 100644 --- a/django/utils/timezone.py +++ b/django/utils/timezone.py @@ -3,12 +3,7 @@ Timezone-related classes and functions. """ 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 5bd5b3594ad..fecb2fd790a 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 ba7209991c1..eb0506e5eeb 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 f0fa4b13064..ed05f88a751 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 a308339fa9c..4f9dc67da50 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 cd54b03cf72..78148c79e86 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 10170fcdea7..19415a13b6f 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 afef79c2abc..529bc6044f3 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 4149a31e219..51d0498ff79 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -32,7 +32,6 @@ from django.db.migrations.recorder import MigrationRecorder 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 @@ class AdminScriptTestCase(SimpleTestCase): 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 @@ class AdminScriptTestCase(SimpleTestCase): 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 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): 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 f7764a7f360..86fb9740a50 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -2,14 +2,10 @@ import datetime 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 70b1233ef4f..4948a60ee09 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 80043fe3f4e..29212b6e244 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 9c026ffb820..0762c43dde5 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -8,13 +8,9 @@ import pathlib 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 397aaa06b18..3cc35b8c613 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 aa58b47a94e..be95f5ec038 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.test.utils import ( ) 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 @@ class AssertWarnsMessageTests(SimpleTestCase): 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 56784db207c..e3e2ea431ee 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 b6286925b76..e33276ba612 100644 --- a/tests/utils_tests/test_autoreload.py +++ b/tests/utils_tests/test_autoreload.py @@ -9,16 +9,12 @@ import time 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 736d456d2db..80ada3abd78 100644 --- a/tests/utils_tests/test_module_loading.py +++ b/tests/utils_tests/test_module_loading.py @@ -11,7 +11,6 @@ from django.utils.module_loading import ( import_string, module_has_submodule, ) -from django.utils.version import PY310 class DefaultLoader(unittest.TestCase): @@ -205,35 +204,12 @@ class AutodiscoverModulesTestCase(SimpleTestCase): 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 931347ad46b..43bb2bc7a34 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 e3db75bf4a8..98cab3abf44 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