Fixed #32355 -- Dropped support for Python 3.6 and 3.7
This commit is contained in:
parent
9c6ba87692
commit
ec0ff40631
2
INSTALL
2
INSTALL
|
@ -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 .
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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'):
|
current_task = 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:
|
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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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__"):
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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+)?$")
|
||||||
|
|
||||||
|
|
|
@ -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,15 +539,14 @@ 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=(
|
'Only run test methods and classes that match the pattern '
|
||||||
'Only run test methods and classes that match the pattern '
|
'or substring. Can be used multiple times. Same as '
|
||||||
'or substring. Can be used multiple times. Same as '
|
'unittest -k option.'
|
||||||
'unittest -k option.'
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def setup_test_environment(self, **kwargs):
|
def setup_test_environment(self, **kwargs):
|
||||||
setup_test_environment(debug=self.debug_mode)
|
setup_test_environment(debug=self.debug_mode)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,14 +520,13 @@ 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=(
|
'Only run test methods and classes matching test name pattern. '
|
||||||
'Only run test methods and classes matching test name pattern. '
|
'Same as unittest -k option. Can be used multiple times.'
|
||||||
'Same as unittest -k option. Can be used multiple times.'
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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')")
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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:
|
msg = 'Error: the following arguments are required: subcommand'
|
||||||
# "required" option requires Python 3.7 and later.
|
with self.assertRaisesMessage(CommandError, msg):
|
||||||
msg = 'Error: the following arguments are required: subcommand'
|
management.call_command('subparser_dest', subcommand='foo', bar=12)
|
||||||
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):
|
def test_create_parser_kwargs(self):
|
||||||
"""BaseCommand.create_parser() passes kwargs to CommandParser."""
|
"""BaseCommand.create_parser() passes kwargs to CommandParser."""
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue