diff --git a/.github/workflows/schedule_tests.yml b/.github/workflows/schedule_tests.yml index 5e6038fb31..f898941fe3 100644 --- a/.github/workflows/schedule_tests.yml +++ b/.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 diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 7b9db7d064..27ecc58eec 100644 --- a/.github/workflows/screenshots.yml +++ b/.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 diff --git a/INSTALL b/INSTALL index 247b0bcdae..389f887186 100644 --- a/INSTALL +++ b/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 . diff --git a/django/db/migrations/serializer.py b/django/db/migrations/serializer.py index 4f61a69cd4..eabdb24f20 100644 --- a/django/db/migrations/serializer.py +++ b/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 ( diff --git a/django/db/models/enums.py b/django/db/models/enums.py index 54e8bf8fad..cb17fe9756 100644 --- a/django/db/models/enums.py +++ b/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): diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 0716d3599e..03c3939a4e 100644 --- a/django/db/models/fields/files.py +++ b/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 diff --git a/django/test/runner.py b/django/test/runner.py index 097980986d..c8bb16e7b3 100644 --- a/django/test/runner.py +++ b/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): diff --git a/django/test/testcases.py b/django/test/testcases.py index 36366bd777..8f9ba977a3 100644 --- a/django/test/testcases.py +++ b/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 diff --git a/django/views/debug.py b/django/views/debug.py index 10b4d22030..425ad296b2 100644 --- a/django/views/debug.py +++ b/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, diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 446d896abe..f4342f69aa 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/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 -`. For example, to run the tests on Python 3.10 +`. 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt index 5acf8c2b18..8c12a36284 100644 --- a/docs/intro/reusable-apps.txt +++ b/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", diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index 26cc241f35..0a766f9099 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.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 diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index c393154d88..93726fb9d6 100644 --- a/docs/ref/django-admin.txt +++ b/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`` -------------- diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index ccb01d2d1a..1496b1ef0e 100644 --- a/docs/releases/6.0.txt +++ b/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 ======================================================= diff --git a/docs/topics/performance.txt b/docs/topics/performance.txt index 1075d6e0ad..f3342453cc 100644 --- a/docs/topics/performance.txt +++ b/docs/topics/performance.txt @@ -419,7 +419,8 @@ performance gains, typically for heavyweight applications. A key aim of the PyPy project is `compatibility `_ 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 diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index 1b0c172ef9..d3822f0920 100644 --- a/docs/topics/testing/advanced.txt +++ b/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 diff --git a/pyproject.toml b/pyproject.toml index 86ea7393ec..540e64d951 100644 --- a/pyproject.toml +++ b/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] diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 12cbc8e874..2bc1364bed 100644 --- a/tests/mail/tests.py +++ b/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. diff --git a/tests/model_enums/tests.py b/tests/model_enums/tests.py index e60df7c24b..e485a83d8a 100644 --- a/tests/model_enums/tests.py +++ b/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): diff --git a/tests/model_fields/test_filefield.py b/tests/model_fields/test_filefield.py index 478e9edd36..57cc7365da 100644 --- a/tests/model_fields/test_filefield.py +++ b/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") diff --git a/tests/runtests.py b/tests/runtests.py index adf9e1bc29..e9052ca4a9 100755 --- a/tests/runtests.py +++ b/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() diff --git a/tests/test_runner/test_debug_sql.py b/tests/test_runner/test_debug_sql.py index e7e8d14fbd..27fc4001c2 100644 --- a/tests/test_runner/test_debug_sql.py +++ b/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 """ diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index 4f13cceeff..4c4a22397b 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/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) diff --git a/tests/test_runner/test_parallel.py b/tests/test_runner/test_parallel.py index 5026bc36c5..3af0fbf2f9 100644 --- a/tests/test_runner/test_parallel.py +++ b/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) diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index fba8dd3b6f..d66ece4005 100644 --- a/tests/test_runner/tests.py +++ b/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) diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index 359cf07402..16692500e3 100644 --- a/tests/test_utils/tests.py +++ b/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..SkipTestCase%s) " - "as SkippingTestCase.test_skip_unless_db_feature..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..SkipTestCase." + "test_foo) as SkippingTestCase.test_skip_unless_db_feature.." + "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..SkipTestCase%s) " + "SkippingTestCase.test_skip_if_db_feature..SkipTestCase.test_foo) " "as SkippingTestCase.test_skip_if_db_feature..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.", ) diff --git a/tests/utils_tests/test_dateparse.py b/tests/utils_tests/test_dateparse.py index 12611e12c2..a01942bfd0 100644 --- a/tests/utils_tests/test_dateparse.py +++ b/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)) diff --git a/tests/utils_tests/test_functional.py b/tests/utils_tests/test_functional.py index fa23debb4d..8b5c330bcf 100644 --- a/tests/utils_tests/test_functional.py +++ b/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 diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index c65514a170..ba67945d59 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/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: diff --git a/tox.ini b/tox.ini index 7a76693f21..c7d9ac6b86 100644 --- a/tox.ini +++ b/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