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

This commit is contained in:
Mariusz Felisiak 2021-01-19 08:35:16 +01:00 committed by Carlton Gibson
parent 9c6ba87692
commit ec0ff40631
33 changed files with 59 additions and 274 deletions

View File

@ -1,6 +1,6 @@
Thanks for downloading Django. 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: this command from the command prompt:
python -m pip install . python -m pip install .

View File

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

View File

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

View File

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

View File

@ -6,11 +6,5 @@ class DatabaseClient(BaseDatabaseClient):
@classmethod @classmethod
def settings_to_cmd_args_env(cls, settings_dict, parameters): def settings_to_cmd_args_env(cls, settings_dict, parameters):
args = [ args = [cls.executable_name, settings_dict['NAME'], *parameters]
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,
]
return args, None return args, None

View File

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

View File

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

View File

@ -18,19 +18,10 @@ from django.utils.datastructures import (
from django.utils.encoding import escape_uri_path, iri_to_uri from django.utils.encoding import escape_uri_path, iri_to_uri
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.http import is_same_domain 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 django.utils.regex_helper import _lazy_re_compile
from .multipartparser import parse_header 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() RAISE_ERROR = object()
host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(:\d+)?$") host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(:\d+)?$")

View File

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

View File

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

View File

@ -7,7 +7,7 @@ from binascii import Error as BinasciiError
from email.utils import formatdate from email.utils import formatdate
from urllib.parse import ( from urllib.parse import (
ParseResult, SplitResult, _coerce_args, _splitnetloc, _splitparams, 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 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)) (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): def escape_leading_slashes(url):
""" """
If redirecting to an absolute path (two leading slashes), a slash must be If redirecting to an absolute path (two leading slashes), a slash must be

View File

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

View File

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

View File

@ -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 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, suite doesn't bundle a settings file for database backends other than SQLite,
however, you must :ref:`create and provide your own test settings 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: using PostgreSQL:
.. console:: .. 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 test suite dependencies (including those for PostgreSQL), and calls
``runtests.py`` with the supplied arguments (in this case, ``runtests.py`` with the supplied arguments (in this case,
``--settings=my_postgres_settings``). ``--settings=my_postgres_settings``).
@ -110,14 +110,14 @@ set. For example, the following is equivalent to the command above:
.. code-block:: console .. 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: Windows users should use:
.. code-block:: doscon .. code-block:: doscon
...\> set DJANGO_SETTINGS_MODULE=my_postgres_settings ...\> set DJANGO_SETTINGS_MODULE=my_postgres_settings
...\> tox -e py35-postgres ...\> tox -e py39-postgres
Running the JavaScript tests Running the JavaScript tests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -212,16 +212,15 @@ this. For a small app like polls, this process isn't too difficult.
Programming Language :: Python Programming Language :: Python
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Topic :: Internet :: WWW/HTTP Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content Topic :: Internet :: WWW/HTTP :: Dynamic Content
[options] [options]
include_package_data = true include_package_data = true
packages = find: packages = find:
python_requires = >=3.6 python_requires = >=3.8
install_requires = install_requires =
Django >= X.Y # Replace "X.Y" as appropriate Django >= X.Y # Replace "X.Y" as appropriate

View File

@ -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 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". 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 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 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 of this page, or update Django to the newest version. If you're using an older

View File

@ -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 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. :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 .. django-admin-option:: --pdb
Spawns a ``pdb`` debugger at each test error or failure. If you have it Spawns a ``pdb`` debugger at each test error or failure. If you have it

View File

@ -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 expensive ``get_friends()`` method and wanted to allow calling it without
retrieving the cached value, you could write:: retrieving the cached value, you could write::
friends = cached_property(get_friends, name='friends') friends = cached_property(get_friends)
You only need the ``name`` argument for Python < 3.6 support.
While ``person.get_friends()`` will recompute the friends on each call, the 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 value of the cached property will persist until you delete it as described

View File

@ -21,6 +21,8 @@ Python compatibility
Django 4.0 supports Python 3.8, 3.9, and 3.10. We **highly recommend** and only 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. 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: .. _whats-new-4.0:
What's new in Django 4.0 What's new in Django 4.0

View File

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

View File

@ -5,7 +5,7 @@ from distutils.sysconfig import get_python_lib
from setuptools import setup from setuptools import setup
CURRENT_PYTHON = sys.version_info[:2] 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. # This check and everything above must remain compatible with Python 2.7.
if CURRENT_PYTHON < REQUIRED_PYTHON: if CURRENT_PYTHON < REQUIRED_PYTHON:

View File

@ -13,7 +13,7 @@ class SqliteDbshellCommandTestCase(SimpleTestCase):
def test_path_name(self): def test_path_name(self):
self.assertEqual( self.assertEqual(
self.settings_to_cmd_args_env({'NAME': Path('test.db.sqlite3')}), 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): def test_parameters(self):

View File

@ -5,7 +5,6 @@ from django.db import close_old_connections, connection
from django.test import ( from django.test import (
RequestFactory, SimpleTestCase, TransactionTestCase, override_settings, RequestFactory, SimpleTestCase, TransactionTestCase, override_settings,
) )
from django.utils.version import PY37
class HandlerTests(SimpleTestCase): class HandlerTests(SimpleTestCase):
@ -183,7 +182,7 @@ class HandlerRequestTests(SimpleTestCase):
def test_invalid_urls(self): def test_invalid_urls(self):
response = self.client.get('~%A9helloworld') response = self.client.get('~%A9helloworld')
self.assertEqual(response.status_code, 404) 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/') 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') self.assertEqual(response.context['request_path'], '/d%25AAo%25AAw%25AAn%25AAl%25AAo%25AAa%25AAd%25AA')

View File

@ -1,10 +1,7 @@
from unittest import skipUnless
from django.db import models from django.db import models
from django.template import Context, Template from django.template import Context, Template
from django.test import SimpleTestCase, TestCase, override_settings from django.test import SimpleTestCase, TestCase, override_settings
from django.test.utils import isolate_apps from django.test.utils import isolate_apps
from django.utils.version import PY37
from .models import ( from .models import (
AbstractBase1, AbstractBase2, AbstractBase3, Child1, Child2, Child3, 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, (TestModel.custom_manager,))
self.assertEqual(TestModel._meta.managers_map, {'custom_manager': 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): def test_manager_class_getitem(self):
self.assertIs(models.Manager[Child1], models.Manager) self.assertIs(models.Manager[Child1], models.Manager)

View File

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

View File

@ -28,7 +28,6 @@ else:
RemovedInDjango41Warning, RemovedInDjango50Warning, RemovedInDjango41Warning, RemovedInDjango50Warning,
) )
from django.utils.log import DEFAULT_LOGGING from django.utils.log import DEFAULT_LOGGING
from django.utils.version import PY37
try: try:
import MySQLdb import MySQLdb
@ -521,7 +520,6 @@ if __name__ == "__main__":
'--timing', action='store_true', '--timing', action='store_true',
help='Output timings, including database set up and total run time.', help='Output timings, including database set up and total run time.',
) )
if PY37:
parser.add_argument( parser.add_argument(
'-k', dest='test_name_patterns', action='append', '-k', dest='test_name_patterns', action='append',
help=( help=(

View File

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

View File

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

View File

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

View File

@ -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.test.utils import captured_stderr, extend_sys_path, ignore_warnings
from django.utils import translation from django.utils import translation
from django.utils.deprecation import RemovedInDjango41Warning from django.utils.deprecation import RemovedInDjango41Warning
from django.utils.version import PY37
from .management.commands import dance from .management.commands import dance
@ -337,20 +336,9 @@ class CommandTests(SimpleTestCase):
msg = "Error: invalid choice: 'test' (choose from 'foo')" msg = "Error: invalid choice: 'test' (choose from 'foo')"
with self.assertRaisesMessage(CommandError, msg): with self.assertRaisesMessage(CommandError, msg):
management.call_command('subparser', 'test', 12) management.call_command('subparser', 'test', 12)
if PY37:
# "required" option requires Python 3.7 and later.
msg = 'Error: the following arguments are required: subcommand' msg = 'Error: the following arguments are required: subcommand'
with self.assertRaisesMessage(CommandError, msg): with self.assertRaisesMessage(CommandError, msg):
management.call_command('subparser_dest', subcommand='foo', bar=12) 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)
def test_create_parser_kwargs(self): def test_create_parser_kwargs(self):
"""BaseCommand.create_parser() passes kwargs to CommandParser.""" """BaseCommand.create_parser() passes kwargs to CommandParser."""

View File

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

View File

@ -7,7 +7,7 @@ from django.test import SimpleTestCase
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.http import ( from django.utils.http import (
base36_to_int, escape_leading_slashes, http_date, int_to_base36, 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, url_has_allowed_host_and_scheme, urlencode, urlsafe_base64_decode,
urlsafe_base64_encode, urlsafe_base64_encode,
) )
@ -331,68 +331,3 @@ class EscapeLeadingSlashesTests(unittest.TestCase):
for url, expected in tests: for url, expected in tests:
with self.subTest(url=url): with self.subTest(url=url):
self.assertEqual(escape_leading_slashes(url), expected) 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)

View File

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