Browse Source

Fixed #36005 -- Dropped support for Python 3.10 and 3.11.

Mariusz Felisiak 2 months ago
parent
commit
f5772de696

+ 0 - 60
.github/workflows/schedule_tests.yml

@@ -16,8 +16,6 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - '3.10'
-          - '3.11'
           - '3.12'
           - '3.13'
           - '3.14-dev'
@@ -64,64 +62,6 @@ jobs:
       - name: Run tests
         run: python -Wall tests/runtests.py --verbosity=2
 
-  pypy-sqlite:
-    runs-on: ubuntu-latest
-    name: Ubuntu, SQLite, PyPy3.10
-    continue-on-error: true
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-      - name: Set up Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: pypy-3.10-nightly
-          cache: 'pip'
-          cache-dependency-path: 'tests/requirements/py3.txt'
-      - name: Install libmemcached-dev for pylibmc
-        run: sudo apt-get install libmemcached-dev
-      - name: Install and upgrade packaging tools
-        run: python -m pip install --upgrade pip setuptools wheel
-      - run: python -m pip install -r tests/requirements/py3.txt -e .
-      - name: Run tests
-        run: python -Wall tests/runtests.py --verbosity=2
-
-  pypy-postgresql:
-    runs-on: ubuntu-latest
-    name: Ubuntu, PostgreSQL, PyPy3.10
-    continue-on-error: true
-    services:
-      postgres:
-        image: postgres:14-alpine
-        env:
-          POSTGRES_DB: django
-          POSTGRES_USER: user
-          POSTGRES_PASSWORD: postgres
-        ports:
-          - 5432:5432
-        options: >-
-          --health-cmd pg_isready
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-      - name: Set up Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: pypy-3.10-nightly
-          cache: 'pip'
-          cache-dependency-path: 'tests/requirements/py3.txt'
-      - name: Install libmemcached-dev for pylibmc
-        run: sudo apt-get install libmemcached-dev
-      - name: Install and upgrade packaging tools
-        run: python -m pip install --upgrade pip setuptools wheel
-      - run: python -m pip install -r tests/requirements/py3.txt -r tests/requirements/postgres.txt -e .
-      - name: Create PostgreSQL settings file
-        run: mv ./.github/workflows/data/test_postgres.py.tpl ./tests/test_postgres.py
-      - name: Run tests
-        run: python -Wall tests/runtests.py --settings=test_postgres --verbosity=2
-
   javascript-tests:
     runs-on: ubuntu-latest
     name: JavaScript tests

+ 1 - 1
.github/workflows/screenshots.yml

@@ -24,7 +24,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v5
         with:
-          python-version: '3.11'
+          python-version: '3.13'
           cache: 'pip'
           cache-dependency-path: 'tests/requirements/py3.txt'
       - name: Install and upgrade packaging tools

+ 1 - 1
INSTALL

@@ -1,6 +1,6 @@
 Thanks for downloading Django.
 
-To install it, make sure you have Python 3.10 or greater installed. Then run
+To install it, make sure you have Python 3.12 or greater installed. Then run
 this command from the command prompt:
 
     python -m pip install .

+ 2 - 6
django/db/migrations/serializer.py

@@ -16,7 +16,7 @@ from django.db import models
 from django.db.migrations.operations.base import Operation
 from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject
 from django.utils.functional import LazyObject, Promise
-from django.utils.version import PY311, get_docs_version
+from django.utils.version import get_docs_version
 
 FUNCTION_TYPES = (types.FunctionType, types.BuiltinFunctionType, types.MethodType)
 
@@ -140,11 +140,7 @@ class EnumSerializer(BaseSerializer):
         enum_class = self.value.__class__
         module = enum_class.__module__
         if issubclass(enum_class, enum.Flag):
-            if PY311:
-                members = list(self.value)
-            else:
-                members, _ = enum._decompose(enum_class, self.value)
-                members = reversed(members)
+            members = list(self.value)
         else:
             members = (self.value,)
         return (

+ 3 - 34
django/db/models/enums.py

@@ -1,25 +1,8 @@
 import enum
+from enum import EnumType, IntEnum, StrEnum
+from enum import property as enum_property
 
 from django.utils.functional import Promise
-from django.utils.version import PY311, PY312
-
-if PY311:
-    from enum import EnumType, IntEnum, StrEnum
-    from enum import property as enum_property
-else:
-    from enum import EnumMeta as EnumType
-    from types import DynamicClassAttribute as enum_property
-
-    class ReprEnum(enum.Enum):
-        def __str__(self):
-            return str(self.value)
-
-    class IntEnum(int, ReprEnum):
-        pass
-
-    class StrEnum(str, ReprEnum):
-        pass
-
 
 __all__ = ["Choices", "IntegerChoices", "TextChoices"]
 
@@ -49,14 +32,6 @@ class ChoicesType(EnumType):
             member._label_ = label
         return enum.unique(cls)
 
-    if not PY312:
-
-        def __contains__(cls, member):
-            if not isinstance(member, enum.Enum):
-                # Allow non-enums to match against member values.
-                return any(x.value == member for x in cls)
-            return super().__contains__(member)
-
     @property
     def names(cls):
         empty = ["__empty__"] if hasattr(cls, "__empty__") else []
@@ -79,13 +54,7 @@ class ChoicesType(EnumType):
 class Choices(enum.Enum, metaclass=ChoicesType):
     """Class for creating enumerated choices."""
 
-    if PY311:
-        do_not_call_in_templates = enum.nonmember(True)
-    else:
-
-        @property
-        def do_not_call_in_templates(self):
-            return True
+    do_not_call_in_templates = enum.nonmember(True)
 
     @enum_property
     def label(self):

+ 1 - 2
django/db/models/fields/files.py

@@ -14,7 +14,6 @@ from django.db.models.fields import Field
 from django.db.models.query_utils import DeferredAttribute
 from django.db.models.utils import AltersData
 from django.utils.translation import gettext_lazy as _
-from django.utils.version import PY311
 
 
 class FieldFile(File, AltersData):
@@ -329,7 +328,7 @@ class FileField(Field):
                 f"File for {self.name} must have "
                 "the name attribute specified to be saved."
             )
-            if PY311 and isinstance(file._file, ContentFile):
+            if isinstance(file._file, ContentFile):
                 exc.add_note("Pass a 'name' argument to ContentFile.")
             raise exc
 

+ 10 - 12
django/test/runner.py

@@ -28,7 +28,7 @@ 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.datastructures import OrderedSet
-from django.utils.version import PY312, PY313
+from django.utils.version import PY313
 
 try:
     import ipdb as pdb
@@ -829,15 +829,14 @@ class DiscoverRunner:
                 "unittest -k option."
             ),
         )
-        if PY312:
-            parser.add_argument(
-                "--durations",
-                dest="durations",
-                type=int,
-                default=None,
-                metavar="N",
-                help="Show the N slowest test cases (N=0 for all).",
-            )
+        parser.add_argument(
+            "--durations",
+            dest="durations",
+            type=int,
+            default=None,
+            metavar="N",
+            help="Show the N slowest test cases (N=0 for all).",
+        )
 
     @property
     def shuffle_seed(self):
@@ -1005,9 +1004,8 @@ class DiscoverRunner:
             "resultclass": self.get_resultclass(),
             "verbosity": self.verbosity,
             "buffer": self.buffer,
+            "durations": self.durations,
         }
-        if PY312:
-            kwargs["durations"] = self.durations
         return kwargs
 
     def run_checks(self, databases):

+ 0 - 25
django/test/testcases.py

@@ -54,7 +54,6 @@ from django.test.utils import (
     override_settings,
 )
 from django.utils.functional import classproperty
-from django.utils.version import PY311
 from django.views.static import serve
 
 logger = logging.getLogger("django.test")
@@ -71,24 +70,6 @@ __all__ = (
 __unittest = True
 
 
-if not PY311:
-    # Backport of unittest.case._enter_context() from Python 3.11.
-    def _enter_context(cm, addcleanup):
-        # Look up the special methods on the type to match the with statement.
-        cls = type(cm)
-        try:
-            enter = cls.__enter__
-            exit = cls.__exit__
-        except AttributeError:
-            raise TypeError(
-                f"'{cls.__module__}.{cls.__qualname__}' object does not support the "
-                f"context manager protocol"
-            ) from None
-        result = enter(cm)
-        addcleanup(exit, cm, None, None, None)
-        return result
-
-
 def to_list(value):
     """Put value into a list if it's not already one."""
     if not isinstance(value, list):
@@ -398,12 +379,6 @@ class SimpleTestCase(unittest.TestCase):
         """Perform post-test things."""
         pass
 
-    if not PY311:
-        # Backport of unittest.TestCase.enterClassContext() from Python 3.11.
-        @classmethod
-        def enterClassContext(cls, cm):
-            return _enter_context(cm, cls.addClassCleanup)
-
     def settings(self, **kwargs):
         """
         A context manager that temporarily sets a setting and reverts to the

+ 13 - 16
django/views/debug.py

@@ -17,7 +17,7 @@ from django.utils.datastructures import MultiValueDict
 from django.utils.encoding import force_str
 from django.utils.module_loading import import_string
 from django.utils.regex_helper import _lazy_re_compile
-from django.utils.version import PY311, get_docs_version
+from django.utils.version import get_docs_version
 from django.views.decorators.debug import coroutine_functions_to_sensitive_variables
 
 # Minimal Django templates engine to render the error templates
@@ -567,22 +567,19 @@ class ExceptionReporter:
                 post_context = []
 
             colno = tb_area_colno = ""
-            if PY311:
-                _, _, start_column, end_column = next(
-                    itertools.islice(
-                        tb.tb_frame.f_code.co_positions(), tb.tb_lasti // 2, None
-                    )
+            _, _, start_column, end_column = next(
+                itertools.islice(
+                    tb.tb_frame.f_code.co_positions(), tb.tb_lasti // 2, None
                 )
-                if start_column and end_column:
-                    underline = "^" * (end_column - start_column)
-                    spaces = " " * (start_column + len(str(lineno + 1)) + 2)
-                    colno = f"\n{spaces}{underline}"
-                    tb_area_spaces = " " * (
-                        4
-                        + start_column
-                        - (len(context_line) - len(context_line.lstrip()))
-                    )
-                    tb_area_colno = f"\n{tb_area_spaces}{underline}"
+            )
+            if start_column and end_column:
+                underline = "^" * (end_column - start_column)
+                spaces = " " * (start_column + len(str(lineno + 1)) + 2)
+                colno = f"\n{spaces}{underline}"
+                tb_area_spaces = " " * (
+                    4 + start_column - (len(context_line) - len(context_line.lstrip()))
+                )
+                tb_area_colno = f"\n{tb_area_spaces}{underline}"
             yield {
                 "exc_cause": exc_cause,
                 "exc_cause_explicit": exc_cause_explicit,

+ 5 - 5
docs/internals/contributing/writing-code/unit-tests.txt

@@ -92,14 +92,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
-<running-unit-tests-settings>`. For example, to run the tests on Python 3.10
+<running-unit-tests-settings>`. For example, to run the tests on Python 3.12
 using PostgreSQL:
 
 .. console::
 
-    $ tox -e py310-postgres -- --settings=my_postgres_settings
+    $ tox -e py312-postgres -- --settings=my_postgres_settings
 
-This command sets up a Python 3.10 virtual environment, installs Django's
+This command sets up a Python 3.12 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``).
@@ -114,14 +114,14 @@ above:
 
 .. code-block:: console
 
-    $ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py310-postgres
+    $ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py312-postgres
 
 Windows users should use:
 
 .. code-block:: doscon
 
     ...\> set DJANGO_SETTINGS_MODULE=my_postgres_settings
-    ...\> tox -e py310-postgres
+    ...\> tox -e py312-postgres
 
 Running the JavaScript tests
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 1 - 3
docs/intro/reusable-apps.txt

@@ -220,7 +220,7 @@ this. For a small app like polls, this process isn't too difficult.
        ]
        description = "A Django app to conduct web-based polls."
        readme = "README.rst"
-       requires-python = ">= 3.10"
+       requires-python = ">= 3.12"
        authors = [
            {name = "Your Name", email = "yourname@example.com"},
        ]
@@ -234,8 +234,6 @@ 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.10",
-           "Programming Language :: Python :: 3.11",
            "Programming Language :: Python :: 3.12",
            "Programming Language :: Python :: 3.13",
            "Topic :: Internet :: WWW/HTTP",

+ 1 - 1
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.10 and
+This tutorial is written for Django |version|, which supports Python 3.12 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

+ 0 - 4
docs/ref/django-admin.txt

@@ -1583,10 +1583,6 @@ Outputs timings, including database setup and total run time.
 
 Shows the N slowest test cases (N=0 for all).
 
-.. admonition:: Python 3.12 and later
-
-    This feature is only available for Python 3.12 and later.
-
 ``testserver``
 --------------
 

+ 2 - 0
docs/releases/6.0.txt

@@ -21,6 +21,8 @@ Python compatibility
 Django 6.0 supports Python 3.12 and 3.13. We **highly recommend** and only
 officially support the latest release of each series.
 
+The Django 5.2.x series is the last to support Python 3.10 and 3.11.
+
 Third-party library support for older version of Django
 =======================================================
 

+ 2 - 1
docs/topics/performance.txt

@@ -419,7 +419,8 @@ performance gains, typically for heavyweight applications.
 
 A key aim of the PyPy project is `compatibility
 <https://www.pypy.org/compat.html>`_ with existing Python APIs and libraries.
-Django is compatible, but you will need to check the compatibility of other
+Django is compatible with versions of PyPy corresponding to the supported
+Python versions, but you will need to check the compatibility of other
 libraries you rely on.
 
 C implementations of Python libraries

+ 0 - 1
docs/topics/testing/advanced.txt

@@ -608,7 +608,6 @@ and tear down the test suite.
 
     ``durations`` will show a list of the N slowest test cases. Setting this
     option to ``0`` will result in the duration for all tests being shown.
-    Requires Python 3.12+.
 
     Django may, from time to time, extend the capabilities of the test runner
     by adding new arguments. The ``**kwargs`` declaration allows for this

+ 2 - 4
pyproject.toml

@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
 [project]
 name = "Django"
 dynamic = ["version"]
-requires-python = ">= 3.10"
+requires-python = ">= 3.12"
 dependencies = [
     "asgiref>=3.8.1",
     "sqlparse>=0.3.1",
@@ -27,8 +27,6 @@ classifiers = [
     "Programming Language :: Python",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3 :: Only",
-    "Programming Language :: Python :: 3.10",
-    "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",
     "Topic :: Internet :: WWW/HTTP",
@@ -54,7 +52,7 @@ Source = "https://github.com/django/django"
 Tracker = "https://code.djangoproject.com/"
 
 [tool.black]
-target-version = ["py310"]
+target-version = ["py312"]
 force-exclude = "tests/test_runner_apps/tagged/tests_syntax_error.py"
 
 [tool.isort]

+ 1 - 1
tests/mail/tests.py

@@ -184,7 +184,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
         """Line length check should encode the payload supporting `surrogateescape`.
 
         Following https://github.com/python/cpython/issues/76511, newer
-        versions of Python (3.11.9, 3.12.3 and 3.13) ensure that a message's
+        versions of Python (3.12.3 and 3.13) ensure that a message's
         payload is encoded with the provided charset and `surrogateescape` is
         used as the error handling strategy.
 

+ 1 - 8
tests/model_enums/tests.py

@@ -8,7 +8,6 @@ from django.template import Context, Template
 from django.test import SimpleTestCase
 from django.utils.functional import Promise
 from django.utils.translation import gettext_lazy as _
-from django.utils.version import PY311
 
 
 class Suit(models.IntegerChoices):
@@ -200,13 +199,7 @@ class ChoicesTests(SimpleTestCase):
 
     def test_do_not_call_in_templates_nonmember(self):
         self.assertNotIn("do_not_call_in_templates", Suit.__members__)
-        if PY311:
-            self.assertIs(Suit.do_not_call_in_templates, True)
-        else:
-            # Using @property on an enum does not behave as expected.
-            self.assertTrue(Suit.do_not_call_in_templates)
-            self.assertIsNot(Suit.do_not_call_in_templates, True)
-            self.assertIsInstance(Suit.do_not_call_in_templates, property)
+        self.assertIs(Suit.do_not_call_in_templates, True)
 
 
 class Separator(bytes, models.Choices):

+ 3 - 5
tests/model_fields/test_filefield.py

@@ -12,7 +12,6 @@ from django.core.files.uploadedfile import TemporaryUploadedFile
 from django.db import IntegrityError, models
 from django.test import TestCase, override_settings
 from django.test.utils import isolate_apps
-from django.utils.version import PY311
 
 from .models import Document
 
@@ -80,10 +79,9 @@ class FileFieldTests(TestCase):
         with self.assertRaisesMessage(FieldError, msg) as cm:
             d.save()
 
-        if PY311:
-            self.assertEqual(
-                cm.exception.__notes__, ["Pass a 'name' argument to ContentFile."]
-            )
+        self.assertEqual(
+            cm.exception.__notes__, ["Pass a 'name' argument to ContentFile."]
+        )
 
     def test_delete_content_file(self):
         file = ContentFile(b"", name="foo")

+ 9 - 10
tests/runtests.py

@@ -34,7 +34,7 @@ else:
     )
     from django.utils.functional import classproperty
     from django.utils.log import DEFAULT_LOGGING
-    from django.utils.version import PY312, PYPY
+    from django.utils.version import PYPY
 
 
 try:
@@ -691,15 +691,14 @@ if __name__ == "__main__":
             "Same as unittest -k option. Can be used multiple times."
         ),
     )
-    if PY312:
-        parser.add_argument(
-            "--durations",
-            dest="durations",
-            type=int,
-            default=None,
-            metavar="N",
-            help="Show the N slowest test cases (N=0 for all).",
-        )
+    parser.add_argument(
+        "--durations",
+        dest="durations",
+        type=int,
+        default=None,
+        metavar="N",
+        help="Show the N slowest test cases (N=0 for all).",
+    )
 
     options = parser.parse_args()
 

+ 5 - 8
tests/test_runner/test_debug_sql.py

@@ -4,7 +4,6 @@ from io import StringIO
 from django.db import connection
 from django.test import TestCase
 from django.test.runner import DiscoverRunner
-from django.utils.version import PY311
 
 from .models import Person
 
@@ -114,17 +113,15 @@ class TestDebugSQL(unittest.TestCase):
         ),
     ]
 
-    # Python 3.11 uses fully qualified test name in the output.
-    method_name = ".runTest" if PY311 else ""
     test_class_path = "test_runner.test_debug_sql.TestDebugSQL"
     verbose_expected_outputs = [
-        f"runTest ({test_class_path}.FailingTest{method_name}) ... FAIL",
-        f"runTest ({test_class_path}.ErrorTest{method_name}) ... ERROR",
-        f"runTest ({test_class_path}.PassingTest{method_name}) ... ok",
+        f"runTest ({test_class_path}.FailingTest.runTest) ... FAIL",
+        f"runTest ({test_class_path}.ErrorTest.runTest) ... ERROR",
+        f"runTest ({test_class_path}.PassingTest.runTest) ... ok",
         # If there are errors/failures in subtests but not in test itself,
         # the status is not written. That behavior comes from Python.
-        f"runTest ({test_class_path}.FailingSubTest{method_name}) ...",
-        f"runTest ({test_class_path}.ErrorSubTest{method_name}) ...",
+        f"runTest ({test_class_path}.FailingSubTest.runTest) ...",
+        f"runTest ({test_class_path}.ErrorSubTest.runTest) ...",
         (
             """SELECT COUNT(*) AS "__count"\n"""
             """FROM "test_runner_person"\nWHERE """

+ 0 - 3
tests/test_runner/test_discover_runner.py

@@ -16,7 +16,6 @@ from django.test.utils import (
     captured_stderr,
     captured_stdout,
 )
-from django.utils.version import PY312
 
 
 @contextmanager
@@ -768,7 +767,6 @@ class DiscoverRunnerTests(SimpleTestCase):
                 failures = runner.suite_result(suite, result)
                 self.assertEqual(failures, expected_failures)
 
-    @unittest.skipUnless(PY312, "unittest --durations option requires Python 3.12")
     def test_durations(self):
         with captured_stderr() as stderr, captured_stdout():
             runner = DiscoverRunner(durations=10)
@@ -776,7 +774,6 @@ class DiscoverRunnerTests(SimpleTestCase):
             runner.run_suite(suite)
         self.assertIn("Slowest test durations", stderr.getvalue())
 
-    @unittest.skipUnless(PY312, "unittest --durations option requires Python 3.12")
     def test_durations_debug_sql(self):
         with captured_stderr() as stderr, captured_stdout():
             runner = DiscoverRunner(durations=10, debug_sql=True)

+ 3 - 10
tests/test_runner/test_parallel.py

@@ -7,7 +7,6 @@ from unittest.suite import TestSuite, _ErrorHolder
 
 from django.test import SimpleTestCase
 from django.test.runner import ParallelTestSuite, RemoteTestResult
-from django.utils.version import PY311, PY312
 
 try:
     import tblib.pickling_support
@@ -193,27 +192,21 @@ class RemoteTestResultTest(SimpleTestCase):
         subtest_test.run(result=result)
 
         events = result.events
-        # addDurations added in Python 3.12.
-        if PY312:
-            self.assertEqual(len(events), 5)
-        else:
-            self.assertEqual(len(events), 4)
+        self.assertEqual(len(events), 5)
         self.assertIs(result.wasSuccessful(), False)
 
         event = events[1]
         self.assertEqual(event[0], "addSubTest")
         self.assertEqual(
             str(event[2]),
-            "dummy_test (test_runner.test_parallel.SampleFailingSubtest%s) (index=0)"
-            # Python 3.11 uses fully qualified test name in the output.
-            % (".dummy_test" if PY311 else ""),
+            "dummy_test (test_runner.test_parallel.SampleFailingSubtest.dummy_test) "
+            "(index=0)",
         )
         self.assertEqual(repr(event[3][1]), "AssertionError('0 != 1')")
 
         event = events[2]
         self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1')")
 
-    @unittest.skipUnless(PY312, "unittest --durations option requires Python 3.12")
     def test_add_duration(self):
         result = RemoteTestResult()
         result.addDuration(None, 2.3)

+ 1 - 14
tests/test_runner/tests.py

@@ -15,7 +15,7 @@ from django import db
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.core.management import call_command
-from django.core.management.base import CommandError, SystemCheckError
+from django.core.management.base import SystemCheckError
 from django.test import SimpleTestCase, TransactionTestCase, skipUnlessDBFeature
 from django.test.runner import (
     DiscoverRunner,
@@ -32,7 +32,6 @@ from django.test.utils import (
     get_unique_databases_and_mirrors,
     iter_test_cases,
 )
-from django.utils.version import PY312
 
 from .models import B, Person, Through
 
@@ -479,7 +478,6 @@ class ManageCommandTests(unittest.TestCase):
             )
         self.assertIn("Total run took", stderr.getvalue())
 
-    @unittest.skipUnless(PY312, "unittest --durations option requires Python 3.12")
     def test_durations(self):
         with captured_stderr() as stderr:
             call_command(
@@ -490,17 +488,6 @@ class ManageCommandTests(unittest.TestCase):
             )
         self.assertIn("durations=10", stderr.getvalue())
 
-    @unittest.skipIf(PY312, "unittest --durations option requires Python 3.12")
-    def test_durations_lt_py312(self):
-        msg = "Error: unrecognized arguments: --durations=10"
-        with self.assertRaises(CommandError, msg=msg):
-            call_command(
-                "test",
-                "--durations=10",
-                "sites",
-                testrunner="test_runner.tests.MockTestRunner",
-            )
-
 
 # Isolate from the real environment.
 @mock.patch.dict(os.environ, {}, clear=True)

+ 5 - 10
tests/test_utils/tests.py

@@ -49,7 +49,6 @@ from django.test.utils import (
 )
 from django.urls import NoReverseMatch, path, reverse, reverse_lazy
 from django.utils.html import VOID_ELEMENTS
-from django.utils.version import PY311
 
 from .models import Car, Person, PossessedCar
 from .views import empty_response
@@ -103,11 +102,9 @@ class SkippingTestCase(SimpleTestCase):
             SkipTestCase("test_foo").test_foo,
             ValueError,
             "skipUnlessDBFeature cannot be used on test_foo (test_utils.tests."
-            "SkippingTestCase.test_skip_unless_db_feature.<locals>.SkipTestCase%s) "
-            "as SkippingTestCase.test_skip_unless_db_feature.<locals>.SkipTestCase "
-            "doesn't allow queries against the 'default' database."
-            # Python 3.11 uses fully qualified test name in the output.
-            % (".test_foo" if PY311 else ""),
+            "SkippingTestCase.test_skip_unless_db_feature.<locals>.SkipTestCase."
+            "test_foo) as SkippingTestCase.test_skip_unless_db_feature.<locals>."
+            "SkipTestCase doesn't allow queries against the 'default' database.",
         )
 
     def test_skip_if_db_feature(self):
@@ -150,11 +147,9 @@ class SkippingTestCase(SimpleTestCase):
             SkipTestCase("test_foo").test_foo,
             ValueError,
             "skipIfDBFeature cannot be used on test_foo (test_utils.tests."
-            "SkippingTestCase.test_skip_if_db_feature.<locals>.SkipTestCase%s) "
+            "SkippingTestCase.test_skip_if_db_feature.<locals>.SkipTestCase.test_foo) "
             "as SkippingTestCase.test_skip_if_db_feature.<locals>.SkipTestCase "
-            "doesn't allow queries against the 'default' database."
-            # Python 3.11 uses fully qualified test name in the output.
-            % (".test_foo" if PY311 else ""),
+            "doesn't allow queries against the 'default' database.",
         )
 
 

+ 2 - 5
tests/utils_tests/test_dateparse.py

@@ -8,7 +8,6 @@ from django.utils.dateparse import (
     parse_time,
 )
 from django.utils.timezone import get_fixed_timezone
-from django.utils.version import PY311
 
 
 class DateParseTests(unittest.TestCase):
@@ -16,8 +15,7 @@ class DateParseTests(unittest.TestCase):
         # Valid inputs
         self.assertEqual(parse_date("2012-04-23"), date(2012, 4, 23))
         self.assertEqual(parse_date("2012-4-9"), date(2012, 4, 9))
-        if PY311:
-            self.assertEqual(parse_date("20120423"), date(2012, 4, 23))
+        self.assertEqual(parse_date("20120423"), date(2012, 4, 23))
         # Invalid inputs
         self.assertIsNone(parse_date("2012423"))
         with self.assertRaises(ValueError):
@@ -26,8 +24,7 @@ class DateParseTests(unittest.TestCase):
     def test_parse_time(self):
         # Valid inputs
         self.assertEqual(parse_time("09:15:00"), time(9, 15))
-        if PY311:
-            self.assertEqual(parse_time("091500"), time(9, 15))
+        self.assertEqual(parse_time("091500"), time(9, 15))
         self.assertEqual(parse_time("10:10"), time(10, 10))
         self.assertEqual(parse_time("10:20:30.400"), time(10, 20, 30, 400000))
         self.assertEqual(parse_time("10:20:30,400"), time(10, 20, 30, 400000))

+ 3 - 11
tests/utils_tests/test_functional.py

@@ -1,6 +1,5 @@
 from django.test import SimpleTestCase
 from django.utils.functional import cached_property, classproperty, lazy
-from django.utils.version import PY312
 
 
 class FunctionalTests(SimpleTestCase):
@@ -133,14 +132,10 @@ class FunctionalTests(SimpleTestCase):
             "Cannot assign the same cached_property to two different names ('a' and "
             "'b')."
         )
-        if PY312:
-            error_type = TypeError
-            msg = type_msg
-        else:
-            error_type = RuntimeError
-            msg = "Error calling __set_name__"
+        error_type = TypeError
+        msg = type_msg
 
-        with self.assertRaisesMessage(error_type, msg) as ctx:
+        with self.assertRaisesMessage(error_type, msg):
 
             class ReusedCachedProperty:
                 @cached_property
@@ -149,9 +144,6 @@ class FunctionalTests(SimpleTestCase):
 
                 b = a
 
-        if not PY312:
-            self.assertEqual(str(ctx.exception.__context__), str(TypeError(type_msg)))
-
     def test_cached_property_reuse_same_name(self):
         """
         Reusing a cached_property on different classes under the same name is

+ 1 - 4
tests/view_tests/tests/test_debug.py

@@ -7,7 +7,7 @@ import tempfile
 import threading
 from io import StringIO
 from pathlib import Path
-from unittest import mock, skipIf, skipUnless
+from unittest import mock, skipIf
 
 from asgiref.sync import async_to_sync, iscoroutinefunction
 
@@ -24,7 +24,6 @@ from django.urls.converters import IntConverter
 from django.utils.functional import SimpleLazyObject
 from django.utils.regex_helper import _lazy_re_compile
 from django.utils.safestring import mark_safe
-from django.utils.version import PY311
 from django.views.debug import (
     CallableSettingWrapper,
     ExceptionCycleWarning,
@@ -695,7 +694,6 @@ class ExceptionReporterTests(SimpleTestCase):
             text,
         )
 
-    @skipUnless(PY311, "Exception notes were added in Python 3.11.")
     def test_exception_with_notes(self):
         request = self.rf.get("/test_view/")
         try:
@@ -806,7 +804,6 @@ class ExceptionReporterTests(SimpleTestCase):
         or os.environ.get("PYTHONNODEBUGRANGES", False),
         "Fine-grained error locations are disabled.",
     )
-    @skipUnless(PY311, "Fine-grained error locations were added in Python 3.11.")
     def test_highlight_error_position(self):
         request = self.rf.get("/test_view/")
         try:

+ 1 - 1
tox.ini

@@ -26,7 +26,7 @@ setenv =
     PYTHONDONTWRITEBYTECODE=1
 deps =
     -e .
-    py{3,310,311,312,313,py3}: -rtests/requirements/py3.txt
+    py{3,312,313}: -rtests/requirements/py3.txt
     postgres: -rtests/requirements/postgres.txt
     mysql: -rtests/requirements/mysql.txt
     oracle: -rtests/requirements/oracle.txt