mirror of
https://github.com/django/django.git
synced 2024-12-22 09:05:43 +00:00
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.
|
||||
|
||||
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 .
|
||||
|
@ -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):
|
||||
|
@ -261,12 +261,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
# For now, it's here so that every use of "threading" is
|
||||
# also async-compatible.
|
||||
try:
|
||||
if hasattr(asyncio, 'current_task'):
|
||||
# Python 3.7 and up
|
||||
current_task = asyncio.current_task()
|
||||
else:
|
||||
# Python 3.6
|
||||
current_task = asyncio.Task.current_task()
|
||||
current_task = asyncio.current_task()
|
||||
except RuntimeError:
|
||||
current_task = None
|
||||
# Current task can be none even if the current_task call didn't error
|
||||
|
@ -25,7 +25,6 @@ from django.utils.asyncio import async_unsafe
|
||||
from django.utils.dateparse import parse_datetime, parse_time
|
||||
from django.utils.duration import duration_microseconds
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.version import PY38
|
||||
|
||||
from .client import DatabaseClient
|
||||
from .creation import DatabaseCreation
|
||||
@ -180,9 +179,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
"settings.DATABASES is improperly configured. "
|
||||
"Please supply the NAME value.")
|
||||
kwargs = {
|
||||
# TODO: Remove str() when dropping support for PY36.
|
||||
# https://bugs.python.org/issue33496
|
||||
'database': str(settings_dict['NAME']),
|
||||
'database': settings_dict['NAME'],
|
||||
'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES,
|
||||
**settings_dict['OPTIONS'],
|
||||
}
|
||||
@ -206,13 +203,10 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
@async_unsafe
|
||||
def get_new_connection(self, conn_params):
|
||||
conn = Database.connect(**conn_params)
|
||||
if PY38:
|
||||
create_deterministic_function = functools.partial(
|
||||
conn.create_function,
|
||||
deterministic=True,
|
||||
)
|
||||
else:
|
||||
create_deterministic_function = conn.create_function
|
||||
create_deterministic_function = functools.partial(
|
||||
conn.create_function,
|
||||
deterministic=True,
|
||||
)
|
||||
create_deterministic_function('django_date_extract', 2, _sqlite_datetime_extract)
|
||||
create_deterministic_function('django_date_trunc', 4, _sqlite_date_trunc)
|
||||
create_deterministic_function('django_datetime_cast_date', 3, _sqlite_datetime_cast_date)
|
||||
|
@ -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
|
||||
|
@ -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__"):
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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+)?$")
|
||||
|
||||
|
@ -21,7 +21,6 @@ from django.test.utils import (
|
||||
teardown_test_environment,
|
||||
)
|
||||
from django.utils.datastructures import OrderedSet
|
||||
from django.utils.version import PY37
|
||||
|
||||
try:
|
||||
import ipdb as pdb
|
||||
@ -240,8 +239,8 @@ failure and get a correct traceback.
|
||||
self.stop_if_failfast()
|
||||
|
||||
def addSubTest(self, test, subtest, err):
|
||||
# Follow Python 3.5's implementation of unittest.TestResult.addSubTest()
|
||||
# by not doing anything when a subtest is successful.
|
||||
# Follow Python's implementation of unittest.TestResult.addSubTest() by
|
||||
# not doing anything when a subtest is successful.
|
||||
if err is not None:
|
||||
# Call check_picklable() before check_subtest_picklable() since
|
||||
# check_picklable() performs the tblib check.
|
||||
@ -540,15 +539,14 @@ class DiscoverRunner:
|
||||
'Output timings, including database set up and total run time.'
|
||||
),
|
||||
)
|
||||
if PY37:
|
||||
parser.add_argument(
|
||||
'-k', action='append', dest='test_name_patterns',
|
||||
help=(
|
||||
'Only run test methods and classes that match the pattern '
|
||||
'or substring. Can be used multiple times. Same as '
|
||||
'unittest -k option.'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-k', action='append', dest='test_name_patterns',
|
||||
help=(
|
||||
'Only run test methods and classes that match the pattern '
|
||||
'or substring. Can be used multiple times. Same as '
|
||||
'unittest -k option.'
|
||||
),
|
||||
)
|
||||
|
||||
def setup_test_environment(self, **kwargs):
|
||||
setup_test_environment(debug=self.debug_mode)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
2
setup.py
2
setup.py
@ -5,7 +5,7 @@ from distutils.sysconfig import get_python_lib
|
||||
from setuptools import setup
|
||||
|
||||
CURRENT_PYTHON = sys.version_info[:2]
|
||||
REQUIRED_PYTHON = (3, 6)
|
||||
REQUIRED_PYTHON = (3, 8)
|
||||
|
||||
# This check and everything above must remain compatible with Python 2.7.
|
||||
if CURRENT_PYTHON < REQUIRED_PYTHON:
|
||||
|
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -28,7 +28,6 @@ else:
|
||||
RemovedInDjango41Warning, RemovedInDjango50Warning,
|
||||
)
|
||||
from django.utils.log import DEFAULT_LOGGING
|
||||
from django.utils.version import PY37
|
||||
|
||||
try:
|
||||
import MySQLdb
|
||||
@ -521,14 +520,13 @@ if __name__ == "__main__":
|
||||
'--timing', action='store_true',
|
||||
help='Output timings, including database set up and total run time.',
|
||||
)
|
||||
if PY37:
|
||||
parser.add_argument(
|
||||
'-k', dest='test_name_patterns', action='append',
|
||||
help=(
|
||||
'Only run test methods and classes matching test name pattern. '
|
||||
'Same as unittest -k option. Can be used multiple times.'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-k', dest='test_name_patterns', action='append',
|
||||
help=(
|
||||
'Only run test methods and classes matching test name pattern. '
|
||||
'Same as unittest -k option. Can be used multiple times.'
|
||||
),
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
from contextlib import contextmanager
|
||||
from unittest import (
|
||||
TestSuite, TextTestRunner, defaultTestLoader, mock, skipUnless,
|
||||
)
|
||||
from unittest import TestSuite, TextTestRunner, defaultTestLoader, mock
|
||||
|
||||
from django.db import connections
|
||||
from django.test import SimpleTestCase
|
||||
@ -11,7 +9,6 @@ from django.test.runner import DiscoverRunner
|
||||
from django.test.utils import (
|
||||
NullTimeKeeper, TimeKeeper, captured_stderr, captured_stdout,
|
||||
)
|
||||
from django.utils.version import PY37
|
||||
|
||||
|
||||
@contextmanager
|
||||
@ -83,7 +80,6 @@ class DiscoverRunnerTests(SimpleTestCase):
|
||||
|
||||
self.assertEqual(count, 1)
|
||||
|
||||
@skipUnless(PY37, 'unittest -k option requires Python 3.7 and later')
|
||||
def test_name_patterns(self):
|
||||
all_test_1 = [
|
||||
'DjangoCase1.test_1', 'DjangoCase2.test_1',
|
||||
|
@ -2,7 +2,6 @@ import unittest
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.runner import RemoteTestResult
|
||||
from django.utils.version import PY37
|
||||
|
||||
try:
|
||||
import tblib
|
||||
@ -80,8 +79,7 @@ class RemoteTestResultTest(SimpleTestCase):
|
||||
event = events[1]
|
||||
self.assertEqual(event[0], 'addSubTest')
|
||||
self.assertEqual(str(event[2]), 'dummy_test (test_runner.test_parallel.SampleFailingSubtest) (index=0)')
|
||||
trailing_comma = '' if PY37 else ','
|
||||
self.assertEqual(repr(event[3][1]), "AssertionError('0 != 1'%s)" % trailing_comma)
|
||||
self.assertEqual(repr(event[3][1]), "AssertionError('0 != 1')")
|
||||
|
||||
event = events[2]
|
||||
self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1'%s)" % trailing_comma)
|
||||
self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1')")
|
||||
|
@ -1,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')
|
||||
|
||||
|
@ -17,7 +17,6 @@ from django.test import SimpleTestCase, override_settings
|
||||
from django.test.utils import captured_stderr, extend_sys_path, ignore_warnings
|
||||
from django.utils import translation
|
||||
from django.utils.deprecation import RemovedInDjango41Warning
|
||||
from django.utils.version import PY37
|
||||
|
||||
from .management.commands import dance
|
||||
|
||||
@ -337,20 +336,9 @@ class CommandTests(SimpleTestCase):
|
||||
msg = "Error: invalid choice: 'test' (choose from 'foo')"
|
||||
with self.assertRaisesMessage(CommandError, msg):
|
||||
management.call_command('subparser', 'test', 12)
|
||||
if PY37:
|
||||
# "required" option requires Python 3.7 and later.
|
||||
msg = 'Error: the following arguments are required: subcommand'
|
||||
with self.assertRaisesMessage(CommandError, msg):
|
||||
management.call_command('subparser_dest', subcommand='foo', bar=12)
|
||||
else:
|
||||
msg = (
|
||||
'Unknown option(s) for subparser_dest command: subcommand. '
|
||||
'Valid options are: bar, force_color, help, no_color, '
|
||||
'pythonpath, settings, skip_checks, stderr, stdout, '
|
||||
'traceback, verbosity, version.'
|
||||
)
|
||||
with self.assertRaisesMessage(TypeError, msg):
|
||||
management.call_command('subparser_dest', subcommand='foo', bar=12)
|
||||
msg = 'Error: the following arguments are required: subcommand'
|
||||
with self.assertRaisesMessage(CommandError, msg):
|
||||
management.call_command('subparser_dest', subcommand='foo', bar=12)
|
||||
|
||||
def test_create_parser_kwargs(self):
|
||||
"""BaseCommand.create_parser() passes kwargs to CommandParser."""
|
||||
|
@ -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'])
|
||||
|
@ -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)
|
||||
|
2
tox.ini
2
tox.ini
@ -23,7 +23,7 @@ passenv = DJANGO_SETTINGS_MODULE PYTHONPATH HOME DISPLAY OBJC_DISABLE_INITIALIZE
|
||||
setenv =
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
deps =
|
||||
py{3,36,37,38,39}: -rtests/requirements/py3.txt
|
||||
py{3,38,39}: -rtests/requirements/py3.txt
|
||||
postgres: -rtests/requirements/postgres.txt
|
||||
mysql: -rtests/requirements/mysql.txt
|
||||
oracle: -rtests/requirements/oracle.txt
|
||||
|
Loading…
Reference in New Issue
Block a user