Browse Source

Fixed #32355 -- Dropped support for Python 3.6 and 3.7

Mariusz Felisiak 4 years ago
parent
commit
ec0ff40631

+ 1 - 1
INSTALL

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

+ 1 - 3
django/core/management/commands/compilemessages.py

@@ -154,9 +154,7 @@ class Command(BaseCommand):
                     self.has_errors = True
                     return
 
-                # PY37: Remove str() when dropping support for PY37.
-                # https://bugs.python.org/issue31961
-                args = [self.program, *self.program_options, '-o', str(mo_path), str(po_path)]
+                args = [self.program, *self.program_options, '-o', mo_path, po_path]
                 futures.append(executor.submit(popen_wrapper, args))
 
             for future in concurrent.futures.as_completed(futures):

+ 1 - 6
django/db/backends/postgresql/base.py

@@ -261,12 +261,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         # For now, it's here so that every use of "threading" is
         # also async-compatible.
         try:
-            if hasattr(asyncio, 'current_task'):
-                # Python 3.7 and up
-                current_task = asyncio.current_task()
-            else:
-                # Python 3.6
-                current_task = asyncio.Task.current_task()
+            current_task = asyncio.current_task()
         except RuntimeError:
             current_task = None
         # Current task can be none even if the current_task call didn't error

+ 5 - 11
django/db/backends/sqlite3/base.py

@@ -25,7 +25,6 @@ from django.utils.asyncio import async_unsafe
 from django.utils.dateparse import parse_datetime, parse_time
 from django.utils.duration import duration_microseconds
 from django.utils.regex_helper import _lazy_re_compile
-from django.utils.version import PY38
 
 from .client import DatabaseClient
 from .creation import DatabaseCreation
@@ -180,9 +179,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
                 "settings.DATABASES is improperly configured. "
                 "Please supply the NAME value.")
         kwargs = {
-            # TODO: Remove str() when dropping support for PY36.
-            # https://bugs.python.org/issue33496
-            'database': str(settings_dict['NAME']),
+            'database': settings_dict['NAME'],
             'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES,
             **settings_dict['OPTIONS'],
         }
@@ -206,13 +203,10 @@ class DatabaseWrapper(BaseDatabaseWrapper):
     @async_unsafe
     def get_new_connection(self, conn_params):
         conn = Database.connect(**conn_params)
-        if PY38:
-            create_deterministic_function = functools.partial(
-                conn.create_function,
-                deterministic=True,
-            )
-        else:
-            create_deterministic_function = conn.create_function
+        create_deterministic_function = functools.partial(
+            conn.create_function,
+            deterministic=True,
+        )
         create_deterministic_function('django_date_extract', 2, _sqlite_datetime_extract)
         create_deterministic_function('django_date_trunc', 4, _sqlite_date_trunc)
         create_deterministic_function('django_datetime_cast_date', 3, _sqlite_datetime_cast_date)

+ 1 - 7
django/db/backends/sqlite3/client.py

@@ -6,11 +6,5 @@ class DatabaseClient(BaseDatabaseClient):
 
     @classmethod
     def settings_to_cmd_args_env(cls, settings_dict, parameters):
-        args = [
-            cls.executable_name,
-            # TODO: Remove str() when dropping support for PY37. args
-            # parameter accepts path-like objects on Windows since Python 3.8.
-            str(settings_dict['NAME']),
-            *parameters,
-        ]
+        args = [cls.executable_name, settings_dict['NAME'], *parameters]
         return args, None

+ 0 - 1
django/db/migrations/questioner.py

@@ -44,7 +44,6 @@ class MigrationQuestioner:
         except ImportError:
             return self.defaults.get("ask_initial", False)
         else:
-            # getattr() needed on PY36 and older (replace with attribute access).
             if getattr(migrations_module, "__file__", None):
                 filenames = os.listdir(os.path.dirname(migrations_module.__file__))
             elif hasattr(migrations_module, "__path__"):

+ 0 - 3
django/http/cookie.py

@@ -3,9 +3,6 @@ from http import cookies
 # For backwards compatibility in Django 2.1.
 SimpleCookie = cookies.SimpleCookie
 
-# Add support for the SameSite attribute (obsolete when PY37 is unsupported).
-cookies.Morsel._reserved.setdefault('samesite', 'SameSite')
-
 
 def parse_cookie(cookie):
     """

+ 0 - 9
django/http/request.py

@@ -18,19 +18,10 @@ from django.utils.datastructures import (
 from django.utils.encoding import escape_uri_path, iri_to_uri
 from django.utils.functional import cached_property
 from django.utils.http import is_same_domain
-from django.utils.inspect import func_supports_parameter
 from django.utils.regex_helper import _lazy_re_compile
 
 from .multipartparser import parse_header
 
-# TODO: Remove when dropping support for PY37. inspect.signature() is used to
-# detect whether the max_num_fields argument is available as this security fix
-# was backported to Python 3.6.8 and 3.7.2, and may also have been applied by
-# downstream package maintainers to other versions in their repositories.
-if not func_supports_parameter(parse_qsl, 'max_num_fields'):
-    from django.utils.http import parse_qsl
-
-
 RAISE_ERROR = object()
 host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(:\d+)?$")
 

+ 10 - 12
django/test/runner.py

@@ -21,7 +21,6 @@ from django.test.utils import (
     teardown_test_environment,
 )
 from django.utils.datastructures import OrderedSet
-from django.utils.version import PY37
 
 try:
     import ipdb as pdb
@@ -240,8 +239,8 @@ failure and get a correct traceback.
         self.stop_if_failfast()
 
     def addSubTest(self, test, subtest, err):
-        # Follow Python 3.5's implementation of unittest.TestResult.addSubTest()
-        # by not doing anything when a subtest is successful.
+        # Follow Python's implementation of unittest.TestResult.addSubTest() by
+        # not doing anything when a subtest is successful.
         if err is not None:
             # Call check_picklable() before check_subtest_picklable() since
             # check_picklable() performs the tblib check.
@@ -540,15 +539,14 @@ class DiscoverRunner:
                 'Output timings, including database set up and total run time.'
             ),
         )
-        if PY37:
-            parser.add_argument(
-                '-k', action='append', dest='test_name_patterns',
-                help=(
-                    'Only run test methods and classes that match the pattern '
-                    'or substring. Can be used multiple times. Same as '
-                    'unittest -k option.'
-                ),
-            )
+        parser.add_argument(
+            '-k', action='append', dest='test_name_patterns',
+            help=(
+                'Only run test methods and classes that match the pattern '
+                'or substring. Can be used multiple times. Same as '
+                'unittest -k option.'
+            ),
+        )
 
     def setup_test_environment(self, **kwargs):
         setup_test_environment(debug=self.debug_mode)

+ 2 - 6
django/utils/autoreload.py

@@ -231,15 +231,11 @@ def get_child_arguments():
         exe_entrypoint = py_script.with_suffix('.exe')
         if exe_entrypoint.exists():
             # Should be executed directly, ignoring sys.executable.
-            # TODO: Remove str() when dropping support for PY37.
-            # args parameter accepts path-like on Windows from Python 3.8.
-            return [str(exe_entrypoint), *sys.argv[1:]]
+            return [exe_entrypoint, *sys.argv[1:]]
         script_entrypoint = py_script.with_name('%s-script.py' % py_script.name)
         if script_entrypoint.exists():
             # Should be executed as usual.
-            # TODO: Remove str() when dropping support for PY37.
-            # args parameter accepts path-like on Windows from Python 3.8.
-            return [*args, str(script_entrypoint), *sys.argv[1:]]
+            return [*args, script_entrypoint, *sys.argv[1:]]
         raise RuntimeError('Script %s does not exist.' % py_script)
     else:
         args += sys.argv

+ 1 - 73
django/utils/http.py

@@ -7,7 +7,7 @@ from binascii import Error as BinasciiError
 from email.utils import formatdate
 from urllib.parse import (
     ParseResult, SplitResult, _coerce_args, _splitnetloc, _splitparams,
-    scheme_chars, unquote, urlencode as original_urlencode, uses_params,
+    scheme_chars, urlencode as original_urlencode, uses_params,
 )
 
 from django.utils.datastructures import MultiValueDict
@@ -343,78 +343,6 @@ def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
             (not scheme or scheme in valid_schemes))
 
 
-# TODO: Remove when dropping support for PY37.
-def parse_qsl(
-    qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8',
-    errors='replace', max_num_fields=None,
-):
-    """
-    Return a list of key/value tuples parsed from query string.
-
-    Backport of urllib.parse.parse_qsl() from Python 3.8.
-    Copyright (C) 2020 Python Software Foundation (see LICENSE.python).
-
-    ----
-
-    Parse a query given as a string argument.
-
-    Arguments:
-
-    qs: percent-encoded query string to be parsed
-
-    keep_blank_values: flag indicating whether blank values in
-        percent-encoded queries should be treated as blank strings. A
-        true value indicates that blanks should be retained as blank
-        strings. The default false value indicates that blank values
-        are to be ignored and treated as if they were  not included.
-
-    strict_parsing: flag indicating what to do with parsing errors. If false
-        (the default), errors are silently ignored. If true, errors raise a
-        ValueError exception.
-
-    encoding and errors: specify how to decode percent-encoded sequences
-        into Unicode characters, as accepted by the bytes.decode() method.
-
-    max_num_fields: int. If set, then throws a ValueError if there are more
-        than n fields read by parse_qsl().
-
-    Returns a list, as G-d intended.
-    """
-    qs, _coerce_result = _coerce_args(qs)
-
-    # If max_num_fields is defined then check that the number of fields is less
-    # than max_num_fields. This prevents a memory exhaustion DOS attack via
-    # post bodies with many fields.
-    if max_num_fields is not None:
-        num_fields = 1 + qs.count('&') + qs.count(';')
-        if max_num_fields < num_fields:
-            raise ValueError('Max number of fields exceeded')
-
-    pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
-    r = []
-    for name_value in pairs:
-        if not name_value and not strict_parsing:
-            continue
-        nv = name_value.split('=', 1)
-        if len(nv) != 2:
-            if strict_parsing:
-                raise ValueError("bad query field: %r" % (name_value,))
-            # Handle case of a control-name with no equal sign.
-            if keep_blank_values:
-                nv.append('')
-            else:
-                continue
-        if len(nv[1]) or keep_blank_values:
-            name = nv[0].replace('+', ' ')
-            name = unquote(name, encoding=encoding, errors=errors)
-            name = _coerce_result(name)
-            value = nv[1].replace('+', ' ')
-            value = unquote(value, encoding=encoding, errors=errors)
-            value = _coerce_result(value)
-            r.append((name, value))
-    return r
-
-
 def escape_leading_slashes(url):
     """
     If redirecting to an absolute path (two leading slashes), a slash must be

+ 2 - 3
django/utils/module_loading.py

@@ -72,10 +72,9 @@ def module_has_submodule(package, module_name):
     full_module_name = package_name + '.' + module_name
     try:
         return importlib_find(full_module_name, package_path) is not None
-    except (ModuleNotFoundError, AttributeError):
+    except ModuleNotFoundError:
         # When module_name is an invalid dotted path, Python raises
-        # ModuleNotFoundError. AttributeError is raised on PY36 (fixed in PY37)
-        # if the penultimate part of the path is not a package.
+        # ModuleNotFoundError.
         return False
 
 

+ 0 - 2
django/utils/version.py

@@ -9,8 +9,6 @@ from distutils.version import LooseVersion
 # or later". So that third-party apps can use these values, each constant
 # should remain as long as the oldest supported Django version supports that
 # Python version.
-PY36 = sys.version_info >= (3, 6)
-PY37 = sys.version_info >= (3, 7)
 PY38 = sys.version_info >= (3, 8)
 PY39 = sys.version_info >= (3, 9)
 

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

@@ -89,14 +89,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.7
+<running-unit-tests-settings>`. For example, to run the tests on Python 3.9
 using PostgreSQL:
 
 .. console::
 
-    $ tox -e py37-postgres -- --settings=my_postgres_settings
+    $ tox -e py39-postgres -- --settings=my_postgres_settings
 
-This command sets up a Python 3.7 virtual environment, installs Django's
+This command sets up a Python 3.9 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``).
@@ -110,14 +110,14 @@ set. For example, the following is equivalent to the command above:
 
 .. code-block:: console
 
-    $ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py35-postgres
+    $ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py39-postgres
 
 Windows users should use:
 
 .. code-block:: doscon
 
     ...\> set DJANGO_SETTINGS_MODULE=my_postgres_settings
-    ...\> tox -e py35-postgres
+    ...\> tox -e py39-postgres
 
 Running the JavaScript tests
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

@@ -212,16 +212,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.6
-            Programming Language :: Python :: 3.7
             Programming Language :: Python :: 3.8
+            Programming Language :: Python :: 3.9
             Topic :: Internet :: WWW/HTTP
             Topic :: Internet :: WWW/HTTP :: Dynamic Content
 
         [options]
         include_package_data = true
         packages = find:
-        python_requires = >=3.6
+        python_requires = >=3.8
         install_requires =
             Django >= X.Y  # Replace "X.Y" as appropriate
 

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

@@ -1507,10 +1507,6 @@ May be specified multiple times and combined with :option:`test --tag`.
 Runs test methods and classes matching test name patterns, in the same way as
 :option:`unittest's -k option<unittest.-k>`. Can be specified multiple times.
 
-.. admonition:: Python 3.7 and later
-
-    This feature is only available for Python 3.7 and later.
-
 .. django-admin-option:: --pdb
 
 Spawns a ``pdb`` debugger at each test error or failure. If you have it

+ 1 - 3
docs/ref/utils.txt

@@ -493,9 +493,7 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004
     expensive ``get_friends()`` method and wanted to allow calling it without
     retrieving the cached value, you could write::
 
-        friends = cached_property(get_friends, name='friends')
-
-    You only need the ``name`` argument for Python < 3.6 support.
+        friends = cached_property(get_friends)
 
     While ``person.get_friends()`` will recompute the friends on each call, the
     value of the cached property will persist until you delete it as described

+ 2 - 0
docs/releases/4.0.txt

@@ -21,6 +21,8 @@ Python compatibility
 Django 4.0 supports Python 3.8, 3.9, and 3.10. We **highly recommend** and only
 officially support the latest release of each series.
 
+The Django 3.2.x series is the last to support Python 3.6 and 3.7.
+
 .. _whats-new-4.0:
 
 What's new in Django 4.0

+ 1 - 3
setup.cfg

@@ -17,8 +17,6 @@ classifiers =
     Programming Language :: Python
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3 :: Only
-    Programming Language :: Python :: 3.6
-    Programming Language :: Python :: 3.7
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
     Topic :: Internet :: WWW/HTTP
@@ -34,7 +32,7 @@ project_urls =
     Tracker = https://code.djangoproject.com/
 
 [options]
-python_requires = >=3.6
+python_requires = >=3.8
 packages = find:
 include_package_data = true
 zip_safe = false

+ 1 - 1
setup.py

@@ -5,7 +5,7 @@ from distutils.sysconfig import get_python_lib
 from setuptools import setup
 
 CURRENT_PYTHON = sys.version_info[:2]
-REQUIRED_PYTHON = (3, 6)
+REQUIRED_PYTHON = (3, 8)
 
 # This check and everything above must remain compatible with Python 2.7.
 if CURRENT_PYTHON < REQUIRED_PYTHON:

+ 1 - 1
tests/dbshell/test_sqlite.py

@@ -13,7 +13,7 @@ class SqliteDbshellCommandTestCase(SimpleTestCase):
     def test_path_name(self):
         self.assertEqual(
             self.settings_to_cmd_args_env({'NAME': Path('test.db.sqlite3')}),
-            (['sqlite3', 'test.db.sqlite3'], None),
+            (['sqlite3', Path('test.db.sqlite3')], None),
         )
 
     def test_parameters(self):

+ 1 - 2
tests/handlers/tests.py

@@ -5,7 +5,6 @@ from django.db import close_old_connections, connection
 from django.test import (
     RequestFactory, SimpleTestCase, TransactionTestCase, override_settings,
 )
-from django.utils.version import PY37
 
 
 class HandlerTests(SimpleTestCase):
@@ -183,7 +182,7 @@ class HandlerRequestTests(SimpleTestCase):
     def test_invalid_urls(self):
         response = self.client.get('~%A9helloworld')
         self.assertEqual(response.status_code, 404)
-        self.assertEqual(response.context['request_path'], '/~%25A9helloworld' if PY37 else '/%7E%25A9helloworld')
+        self.assertEqual(response.context['request_path'], '/~%25A9helloworld')
 
         response = self.client.get('d%aao%aaw%aan%aal%aao%aaa%aad%aa/')
         self.assertEqual(response.context['request_path'], '/d%25AAo%25AAw%25AAn%25AAl%25AAo%25AAa%25AAd%25AA')

+ 0 - 4
tests/managers_regress/tests.py

@@ -1,10 +1,7 @@
-from unittest import skipUnless
-
 from django.db import models
 from django.template import Context, Template
 from django.test import SimpleTestCase, TestCase, override_settings
 from django.test.utils import isolate_apps
-from django.utils.version import PY37
 
 from .models import (
     AbstractBase1, AbstractBase2, AbstractBase3, Child1, Child2, Child3,
@@ -287,6 +284,5 @@ class TestManagerInheritance(SimpleTestCase):
         self.assertEqual(TestModel._meta.managers, (TestModel.custom_manager,))
         self.assertEqual(TestModel._meta.managers_map, {'custom_manager': TestModel.custom_manager})
 
-    @skipUnless(PY37, '__class_getitem__() was added in Python 3.7')
     def test_manager_class_getitem(self):
         self.assertIs(models.Manager[Child1], models.Manager)

+ 0 - 3
tests/model_inheritance/tests.py

@@ -1,11 +1,9 @@
 from operator import attrgetter
-from unittest import skipUnless
 
 from django.core.exceptions import FieldError, ValidationError
 from django.db import connection, models
 from django.test import SimpleTestCase, TestCase
 from django.test.utils import CaptureQueriesContext, isolate_apps
-from django.utils.version import PY37
 
 from .models import (
     Base, Chef, CommonInfo, GrandChild, GrandParent, ItalianRestaurant,
@@ -219,7 +217,6 @@ class ModelInheritanceTests(TestCase):
         self.assertSequenceEqual(qs, [p2, p1])
         self.assertIn(expected_order_by_sql, str(qs.query))
 
-    @skipUnless(PY37, '__class_getitem__() was added in Python 3.7')
     def test_queryset_class_getitem(self):
         self.assertIs(models.QuerySet[Post], models.QuerySet)
         self.assertIs(models.QuerySet[Post, Post], models.QuerySet)

+ 7 - 9
tests/runtests.py

@@ -28,7 +28,6 @@ else:
         RemovedInDjango41Warning, RemovedInDjango50Warning,
     )
     from django.utils.log import DEFAULT_LOGGING
-    from django.utils.version import PY37
 
 try:
     import MySQLdb
@@ -521,14 +520,13 @@ if __name__ == "__main__":
         '--timing', action='store_true',
         help='Output timings, including database set up and total run time.',
     )
-    if PY37:
-        parser.add_argument(
-            '-k', dest='test_name_patterns', action='append',
-            help=(
-                'Only run test methods and classes matching test name pattern. '
-                'Same as unittest -k option. Can be used multiple times.'
-            ),
-        )
+    parser.add_argument(
+        '-k', dest='test_name_patterns', action='append',
+        help=(
+            'Only run test methods and classes matching test name pattern. '
+            'Same as unittest -k option. Can be used multiple times.'
+        ),
+    )
 
     options = parser.parse_args()
 

+ 1 - 5
tests/test_runner/test_discover_runner.py

@@ -1,9 +1,7 @@
 import os
 from argparse import ArgumentParser
 from contextlib import contextmanager
-from unittest import (
-    TestSuite, TextTestRunner, defaultTestLoader, mock, skipUnless,
-)
+from unittest import TestSuite, TextTestRunner, defaultTestLoader, mock
 
 from django.db import connections
 from django.test import SimpleTestCase
@@ -11,7 +9,6 @@ from django.test.runner import DiscoverRunner
 from django.test.utils import (
     NullTimeKeeper, TimeKeeper, captured_stderr, captured_stdout,
 )
-from django.utils.version import PY37
 
 
 @contextmanager
@@ -83,7 +80,6 @@ class DiscoverRunnerTests(SimpleTestCase):
 
         self.assertEqual(count, 1)
 
-    @skipUnless(PY37, 'unittest -k option requires Python 3.7 and later')
     def test_name_patterns(self):
         all_test_1 = [
             'DjangoCase1.test_1', 'DjangoCase2.test_1',

+ 2 - 4
tests/test_runner/test_parallel.py

@@ -2,7 +2,6 @@ import unittest
 
 from django.test import SimpleTestCase
 from django.test.runner import RemoteTestResult
-from django.utils.version import PY37
 
 try:
     import tblib
@@ -80,8 +79,7 @@ class RemoteTestResultTest(SimpleTestCase):
         event = events[1]
         self.assertEqual(event[0], 'addSubTest')
         self.assertEqual(str(event[2]), 'dummy_test (test_runner.test_parallel.SampleFailingSubtest) (index=0)')
-        trailing_comma = '' if PY37 else ','
-        self.assertEqual(repr(event[3][1]), "AssertionError('0 != 1'%s)" % trailing_comma)
+        self.assertEqual(repr(event[3][1]), "AssertionError('0 != 1')")
 
         event = events[2]
-        self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1'%s)" % trailing_comma)
+        self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1')")

+ 1 - 3
tests/user_commands/management/commands/subparser_dest.py

@@ -1,11 +1,9 @@
 from django.core.management.base import BaseCommand
-from django.utils.version import PY37
 
 
 class Command(BaseCommand):
     def add_arguments(self, parser):
-        kwargs = {'required': True} if PY37 else {}
-        subparsers = parser.add_subparsers(dest='subcommand', **kwargs)
+        subparsers = parser.add_subparsers(dest='subcommand', required=True)
         parser_foo = subparsers.add_parser('foo')
         parser_foo.add_argument('--bar')
 

+ 3 - 15
tests/user_commands/tests.py

@@ -17,7 +17,6 @@ from django.test import SimpleTestCase, override_settings
 from django.test.utils import captured_stderr, extend_sys_path, ignore_warnings
 from django.utils import translation
 from django.utils.deprecation import RemovedInDjango41Warning
-from django.utils.version import PY37
 
 from .management.commands import dance
 
@@ -337,20 +336,9 @@ class CommandTests(SimpleTestCase):
         msg = "Error: invalid choice: 'test' (choose from 'foo')"
         with self.assertRaisesMessage(CommandError, msg):
             management.call_command('subparser', 'test', 12)
-        if PY37:
-            # "required" option requires Python 3.7 and later.
-            msg = 'Error: the following arguments are required: subcommand'
-            with self.assertRaisesMessage(CommandError, msg):
-                management.call_command('subparser_dest', subcommand='foo', bar=12)
-        else:
-            msg = (
-                'Unknown option(s) for subparser_dest command: subcommand. '
-                'Valid options are: bar, force_color, help, no_color, '
-                'pythonpath, settings, skip_checks, stderr, stdout, '
-                'traceback, verbosity, version.'
-            )
-            with self.assertRaisesMessage(TypeError, msg):
-                management.call_command('subparser_dest', subcommand='foo', bar=12)
+        msg = 'Error: the following arguments are required: subcommand'
+        with self.assertRaisesMessage(CommandError, msg):
+            management.call_command('subparser_dest', subcommand='foo', bar=12)
 
     def test_create_parser_kwargs(self):
         """BaseCommand.create_parser() passes kwargs to CommandParser."""

+ 4 - 4
tests/utils_tests/test_autoreload.py

@@ -195,10 +195,10 @@ class TestChildArguments(SimpleTestCase):
         with tempfile.TemporaryDirectory() as tmpdir:
             exe_path = Path(tmpdir) / 'django-admin.exe'
             exe_path.touch()
-            with mock.patch('sys.argv', [str(exe_path.with_suffix('')), 'runserver']):
+            with mock.patch('sys.argv', [exe_path.with_suffix(''), 'runserver']):
                 self.assertEqual(
                     autoreload.get_child_arguments(),
-                    [str(exe_path), 'runserver']
+                    [exe_path, 'runserver']
                 )
 
     @mock.patch('sys.warnoptions', [])
@@ -206,10 +206,10 @@ class TestChildArguments(SimpleTestCase):
         with tempfile.TemporaryDirectory() as tmpdir:
             script_path = Path(tmpdir) / 'django-admin-script.py'
             script_path.touch()
-            with mock.patch('sys.argv', [str(script_path.with_name('django-admin')), 'runserver']):
+            with mock.patch('sys.argv', [script_path.with_name('django-admin'), 'runserver']):
                 self.assertEqual(
                     autoreload.get_child_arguments(),
-                    [sys.executable, str(script_path), 'runserver']
+                    [sys.executable, script_path, 'runserver']
                 )
 
     @mock.patch('sys.argv', ['does-not-exist', 'runserver'])

+ 1 - 66
tests/utils_tests/test_http.py

@@ -7,7 +7,7 @@ from django.test import SimpleTestCase
 from django.utils.datastructures import MultiValueDict
 from django.utils.http import (
     base36_to_int, escape_leading_slashes, http_date, int_to_base36,
-    is_same_domain, parse_etags, parse_http_date, parse_qsl, quote_etag,
+    is_same_domain, parse_etags, parse_http_date, quote_etag,
     url_has_allowed_host_and_scheme, urlencode, urlsafe_base64_decode,
     urlsafe_base64_encode,
 )
@@ -331,68 +331,3 @@ class EscapeLeadingSlashesTests(unittest.TestCase):
         for url, expected in tests:
             with self.subTest(url=url):
                 self.assertEqual(escape_leading_slashes(url), expected)
-
-
-# TODO: Remove when dropping support for PY37. Backport of unit tests for
-# urllib.parse.parse_qsl() from Python 3.8. Copyright (C) 2020 Python Software
-# Foundation (see LICENSE.python).
-class ParseQSLBackportTests(unittest.TestCase):
-    def test_parse_qsl(self):
-        tests = [
-            ('', []),
-            ('&', []),
-            ('&&', []),
-            ('=', [('', '')]),
-            ('=a', [('', 'a')]),
-            ('a', [('a', '')]),
-            ('a=', [('a', '')]),
-            ('&a=b', [('a', 'b')]),
-            ('a=a+b&b=b+c', [('a', 'a b'), ('b', 'b c')]),
-            ('a=1&a=2', [('a', '1'), ('a', '2')]),
-            (b'', []),
-            (b'&', []),
-            (b'&&', []),
-            (b'=', [(b'', b'')]),
-            (b'=a', [(b'', b'a')]),
-            (b'a', [(b'a', b'')]),
-            (b'a=', [(b'a', b'')]),
-            (b'&a=b', [(b'a', b'b')]),
-            (b'a=a+b&b=b+c', [(b'a', b'a b'), (b'b', b'b c')]),
-            (b'a=1&a=2', [(b'a', b'1'), (b'a', b'2')]),
-            (';', []),
-            (';;', []),
-            (';a=b', [('a', 'b')]),
-            ('a=a+b;b=b+c', [('a', 'a b'), ('b', 'b c')]),
-            ('a=1;a=2', [('a', '1'), ('a', '2')]),
-            (b';', []),
-            (b';;', []),
-            (b';a=b', [(b'a', b'b')]),
-            (b'a=a+b;b=b+c', [(b'a', b'a b'), (b'b', b'b c')]),
-            (b'a=1;a=2', [(b'a', b'1'), (b'a', b'2')]),
-        ]
-        for original, expected in tests:
-            with self.subTest(original):
-                result = parse_qsl(original, keep_blank_values=True)
-                self.assertEqual(result, expected, 'Error parsing %r' % original)
-                expect_without_blanks = [v for v in expected if len(v[1])]
-                result = parse_qsl(original, keep_blank_values=False)
-                self.assertEqual(result, expect_without_blanks, 'Error parsing %r' % original)
-
-    def test_parse_qsl_encoding(self):
-        result = parse_qsl('key=\u0141%E9', encoding='latin-1')
-        self.assertEqual(result, [('key', '\u0141\xE9')])
-        result = parse_qsl('key=\u0141%C3%A9', encoding='utf-8')
-        self.assertEqual(result, [('key', '\u0141\xE9')])
-        result = parse_qsl('key=\u0141%C3%A9', encoding='ascii')
-        self.assertEqual(result, [('key', '\u0141\ufffd\ufffd')])
-        result = parse_qsl('key=\u0141%E9-', encoding='ascii')
-        self.assertEqual(result, [('key', '\u0141\ufffd-')])
-        result = parse_qsl('key=\u0141%E9-', encoding='ascii', errors='ignore')
-        self.assertEqual(result, [('key', '\u0141-')])
-
-    def test_parse_qsl_max_num_fields(self):
-        with self.assertRaises(ValueError):
-            parse_qsl('&'.join(['a=a'] * 11), max_num_fields=10)
-        with self.assertRaises(ValueError):
-            parse_qsl(';'.join(['a=a'] * 11), max_num_fields=10)
-        parse_qsl('&'.join(['a=a'] * 10), max_num_fields=10)

+ 1 - 1
tox.ini

@@ -23,7 +23,7 @@ passenv = DJANGO_SETTINGS_MODULE PYTHONPATH HOME DISPLAY OBJC_DISABLE_INITIALIZE
 setenv =
     PYTHONDONTWRITEBYTECODE=1
 deps =
-    py{3,36,37,38,39}: -rtests/requirements/py3.txt
+    py{3,38,39}: -rtests/requirements/py3.txt
     postgres: -rtests/requirements/postgres.txt
     mysql: -rtests/requirements/mysql.txt
     oracle: -rtests/requirements/oracle.txt