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.
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 .

View File

@ -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):

View File

@ -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()
except RuntimeError:
current_task = None
# 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.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('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)

View File

@ -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

View File

@ -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__"):

View File

@ -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):
"""

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.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+)?$")

View File

@ -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,7 +539,6 @@ 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=(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

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
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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 :: 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

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
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

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
: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

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
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

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
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

View File

@ -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

View File

@ -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:

View File

@ -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):

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -28,7 +28,6 @@ else:
RemovedInDjango41Warning, RemovedInDjango50Warning,
)
from django.utils.log import DEFAULT_LOGGING
from django.utils.version import PY37
try:
import MySQLdb
@ -521,7 +520,6 @@ 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=(

View File

@ -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',

View File

@ -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')")

View File

@ -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')

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.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)
def test_create_parser_kwargs(self):
"""BaseCommand.create_parser() passes kwargs to CommandParser."""

View File

@ -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'])

View File

@ -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)

View File

@ -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