From ec0ff406311de88f4e2a135d784363424fe602aa Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 19 Jan 2021 08:35:16 +0100 Subject: [PATCH] Fixed #32355 -- Dropped support for Python 3.6 and 3.7 --- INSTALL | 2 +- .../management/commands/compilemessages.py | 4 +- django/db/backends/postgresql/base.py | 7 +- django/db/backends/sqlite3/base.py | 16 ++-- django/db/backends/sqlite3/client.py | 8 +- django/db/migrations/questioner.py | 1 - django/http/cookie.py | 3 - django/http/request.py | 9 --- django/test/runner.py | 22 +++--- django/utils/autoreload.py | 8 +- django/utils/http.py | 74 +------------------ django/utils/module_loading.py | 5 +- django/utils/version.py | 2 - .../contributing/writing-code/unit-tests.txt | 10 +-- docs/intro/reusable-apps.txt | 5 +- docs/intro/tutorial01.txt | 2 +- docs/ref/django-admin.txt | 4 - docs/ref/utils.txt | 4 +- docs/releases/4.0.txt | 2 + setup.cfg | 4 +- setup.py | 2 +- tests/dbshell/test_sqlite.py | 2 +- tests/handlers/tests.py | 3 +- tests/managers_regress/tests.py | 4 - tests/model_inheritance/tests.py | 3 - tests/runtests.py | 16 ++-- tests/test_runner/test_discover_runner.py | 6 +- tests/test_runner/test_parallel.py | 6 +- .../management/commands/subparser_dest.py | 4 +- tests/user_commands/tests.py | 18 +---- tests/utils_tests/test_autoreload.py | 8 +- tests/utils_tests/test_http.py | 67 +---------------- tox.ini | 2 +- 33 files changed, 59 insertions(+), 274 deletions(-) diff --git a/INSTALL b/INSTALL index 7c10946c87..cd9dd33274 100644 --- a/INSTALL +++ b/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 . diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py index cad24f8140..308fa8831b 100644 --- a/django/core/management/commands/compilemessages.py +++ b/django/core/management/commands/compilemessages.py @@ -154,9 +154,7 @@ class Command(BaseCommand): self.has_errors = True return - # PY37: Remove str() when dropping support for PY37. - # https://bugs.python.org/issue31961 - args = [self.program, *self.program_options, '-o', str(mo_path), str(po_path)] + args = [self.program, *self.program_options, '-o', mo_path, po_path] futures.append(executor.submit(popen_wrapper, args)) for future in concurrent.futures.as_completed(futures): diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 126f30b462..ad8d85da29 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -261,12 +261,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): # For now, it's here so that every use of "threading" is # also async-compatible. try: - if hasattr(asyncio, 'current_task'): - # Python 3.7 and up - current_task = asyncio.current_task() - else: - # Python 3.6 - current_task = asyncio.Task.current_task() + current_task = asyncio.current_task() except RuntimeError: current_task = None # Current task can be none even if the current_task call didn't error diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index f8e1def982..32d767878e 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -25,7 +25,6 @@ from django.utils.asyncio import async_unsafe from django.utils.dateparse import parse_datetime, parse_time from django.utils.duration import duration_microseconds from django.utils.regex_helper import _lazy_re_compile -from django.utils.version import PY38 from .client import DatabaseClient from .creation import DatabaseCreation @@ -180,9 +179,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): "settings.DATABASES is improperly configured. " "Please supply the NAME value.") kwargs = { - # TODO: Remove str() when dropping support for PY36. - # https://bugs.python.org/issue33496 - 'database': str(settings_dict['NAME']), + 'database': settings_dict['NAME'], 'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES, **settings_dict['OPTIONS'], } @@ -206,13 +203,10 @@ class DatabaseWrapper(BaseDatabaseWrapper): @async_unsafe def get_new_connection(self, conn_params): conn = Database.connect(**conn_params) - if PY38: - create_deterministic_function = functools.partial( - conn.create_function, - deterministic=True, - ) - else: - create_deterministic_function = conn.create_function + create_deterministic_function = functools.partial( + conn.create_function, + deterministic=True, + ) create_deterministic_function('django_date_extract', 2, _sqlite_datetime_extract) create_deterministic_function('django_date_trunc', 4, _sqlite_date_trunc) create_deterministic_function('django_datetime_cast_date', 3, _sqlite_datetime_cast_date) diff --git a/django/db/backends/sqlite3/client.py b/django/db/backends/sqlite3/client.py index 59a2fe7f50..69b9568db3 100644 --- a/django/db/backends/sqlite3/client.py +++ b/django/db/backends/sqlite3/client.py @@ -6,11 +6,5 @@ class DatabaseClient(BaseDatabaseClient): @classmethod def settings_to_cmd_args_env(cls, settings_dict, parameters): - args = [ - cls.executable_name, - # TODO: Remove str() when dropping support for PY37. args - # parameter accepts path-like objects on Windows since Python 3.8. - str(settings_dict['NAME']), - *parameters, - ] + args = [cls.executable_name, settings_dict['NAME'], *parameters] return args, None diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py index 9edb8180ef..216f2af6a1 100644 --- a/django/db/migrations/questioner.py +++ b/django/db/migrations/questioner.py @@ -44,7 +44,6 @@ class MigrationQuestioner: except ImportError: return self.defaults.get("ask_initial", False) else: - # getattr() needed on PY36 and older (replace with attribute access). if getattr(migrations_module, "__file__", None): filenames = os.listdir(os.path.dirname(migrations_module.__file__)) elif hasattr(migrations_module, "__path__"): diff --git a/django/http/cookie.py b/django/http/cookie.py index 5c418d7e35..b94d2b0386 100644 --- a/django/http/cookie.py +++ b/django/http/cookie.py @@ -3,9 +3,6 @@ from http import cookies # For backwards compatibility in Django 2.1. SimpleCookie = cookies.SimpleCookie -# Add support for the SameSite attribute (obsolete when PY37 is unsupported). -cookies.Morsel._reserved.setdefault('samesite', 'SameSite') - def parse_cookie(cookie): """ diff --git a/django/http/request.py b/django/http/request.py index bdf9b508b1..79fc8350fd 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -18,19 +18,10 @@ from django.utils.datastructures import ( from django.utils.encoding import escape_uri_path, iri_to_uri from django.utils.functional import cached_property from django.utils.http import is_same_domain -from django.utils.inspect import func_supports_parameter from django.utils.regex_helper import _lazy_re_compile from .multipartparser import parse_header -# TODO: Remove when dropping support for PY37. inspect.signature() is used to -# detect whether the max_num_fields argument is available as this security fix -# was backported to Python 3.6.8 and 3.7.2, and may also have been applied by -# downstream package maintainers to other versions in their repositories. -if not func_supports_parameter(parse_qsl, 'max_num_fields'): - from django.utils.http import parse_qsl - - RAISE_ERROR = object() host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(:\d+)?$") diff --git a/django/test/runner.py b/django/test/runner.py index 8e6c29b0b4..e14c6381eb 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -21,7 +21,6 @@ from django.test.utils import ( teardown_test_environment, ) from django.utils.datastructures import OrderedSet -from django.utils.version import PY37 try: import ipdb as pdb @@ -240,8 +239,8 @@ failure and get a correct traceback. self.stop_if_failfast() def addSubTest(self, test, subtest, err): - # Follow Python 3.5's implementation of unittest.TestResult.addSubTest() - # by not doing anything when a subtest is successful. + # Follow Python's implementation of unittest.TestResult.addSubTest() by + # not doing anything when a subtest is successful. if err is not None: # Call check_picklable() before check_subtest_picklable() since # check_picklable() performs the tblib check. @@ -540,15 +539,14 @@ class DiscoverRunner: 'Output timings, including database set up and total run time.' ), ) - if PY37: - parser.add_argument( - '-k', action='append', dest='test_name_patterns', - help=( - 'Only run test methods and classes that match the pattern ' - 'or substring. Can be used multiple times. Same as ' - 'unittest -k option.' - ), - ) + parser.add_argument( + '-k', action='append', dest='test_name_patterns', + help=( + 'Only run test methods and classes that match the pattern ' + 'or substring. Can be used multiple times. Same as ' + 'unittest -k option.' + ), + ) def setup_test_environment(self, **kwargs): setup_test_environment(debug=self.debug_mode) diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index faa3252c71..3847252632 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -231,15 +231,11 @@ def get_child_arguments(): exe_entrypoint = py_script.with_suffix('.exe') if exe_entrypoint.exists(): # Should be executed directly, ignoring sys.executable. - # TODO: Remove str() when dropping support for PY37. - # args parameter accepts path-like on Windows from Python 3.8. - return [str(exe_entrypoint), *sys.argv[1:]] + return [exe_entrypoint, *sys.argv[1:]] script_entrypoint = py_script.with_name('%s-script.py' % py_script.name) if script_entrypoint.exists(): # Should be executed as usual. - # TODO: Remove str() when dropping support for PY37. - # args parameter accepts path-like on Windows from Python 3.8. - return [*args, str(script_entrypoint), *sys.argv[1:]] + return [*args, script_entrypoint, *sys.argv[1:]] raise RuntimeError('Script %s does not exist.' % py_script) else: args += sys.argv diff --git a/django/utils/http.py b/django/utils/http.py index 962716eb00..5397bb8190 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -7,7 +7,7 @@ from binascii import Error as BinasciiError from email.utils import formatdate from urllib.parse import ( ParseResult, SplitResult, _coerce_args, _splitnetloc, _splitparams, - scheme_chars, unquote, urlencode as original_urlencode, uses_params, + scheme_chars, urlencode as original_urlencode, uses_params, ) from django.utils.datastructures import MultiValueDict @@ -343,78 +343,6 @@ def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False): (not scheme or scheme in valid_schemes)) -# TODO: Remove when dropping support for PY37. -def parse_qsl( - qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8', - errors='replace', max_num_fields=None, -): - """ - Return a list of key/value tuples parsed from query string. - - Backport of urllib.parse.parse_qsl() from Python 3.8. - Copyright (C) 2020 Python Software Foundation (see LICENSE.python). - - ---- - - Parse a query given as a string argument. - - Arguments: - - qs: percent-encoded query string to be parsed - - keep_blank_values: flag indicating whether blank values in - percent-encoded queries should be treated as blank strings. A - true value indicates that blanks should be retained as blank - strings. The default false value indicates that blank values - are to be ignored and treated as if they were not included. - - strict_parsing: flag indicating what to do with parsing errors. If false - (the default), errors are silently ignored. If true, errors raise a - ValueError exception. - - encoding and errors: specify how to decode percent-encoded sequences - into Unicode characters, as accepted by the bytes.decode() method. - - max_num_fields: int. If set, then throws a ValueError if there are more - than n fields read by parse_qsl(). - - Returns a list, as G-d intended. - """ - qs, _coerce_result = _coerce_args(qs) - - # If max_num_fields is defined then check that the number of fields is less - # than max_num_fields. This prevents a memory exhaustion DOS attack via - # post bodies with many fields. - if max_num_fields is not None: - num_fields = 1 + qs.count('&') + qs.count(';') - if max_num_fields < num_fields: - raise ValueError('Max number of fields exceeded') - - pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] - r = [] - for name_value in pairs: - if not name_value and not strict_parsing: - continue - nv = name_value.split('=', 1) - if len(nv) != 2: - if strict_parsing: - raise ValueError("bad query field: %r" % (name_value,)) - # Handle case of a control-name with no equal sign. - if keep_blank_values: - nv.append('') - else: - continue - if len(nv[1]) or keep_blank_values: - name = nv[0].replace('+', ' ') - name = unquote(name, encoding=encoding, errors=errors) - name = _coerce_result(name) - value = nv[1].replace('+', ' ') - value = unquote(value, encoding=encoding, errors=errors) - value = _coerce_result(value) - r.append((name, value)) - return r - - def escape_leading_slashes(url): """ If redirecting to an absolute path (two leading slashes), a slash must be diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index df8e65098d..9f58c06856 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -72,10 +72,9 @@ def module_has_submodule(package, module_name): full_module_name = package_name + '.' + module_name try: return importlib_find(full_module_name, package_path) is not None - except (ModuleNotFoundError, AttributeError): + except ModuleNotFoundError: # When module_name is an invalid dotted path, Python raises - # ModuleNotFoundError. AttributeError is raised on PY36 (fixed in PY37) - # if the penultimate part of the path is not a package. + # ModuleNotFoundError. return False diff --git a/django/utils/version.py b/django/utils/version.py index 4b26586b36..68708df7aa 100644 --- a/django/utils/version.py +++ b/django/utils/version.py @@ -9,8 +9,6 @@ from distutils.version import LooseVersion # or later". So that third-party apps can use these values, each constant # should remain as long as the oldest supported Django version supports that # Python version. -PY36 = sys.version_info >= (3, 6) -PY37 = sys.version_info >= (3, 7) PY38 = sys.version_info >= (3, 8) PY39 = sys.version_info >= (3, 9) diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index fef10b2db4..373df5ea42 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -89,14 +89,14 @@ In addition to the default environments, ``tox`` supports running unit tests for other versions of Python and other database backends. Since Django's test suite doesn't bundle a settings file for database backends other than SQLite, however, you must :ref:`create and provide your own test settings -`. For example, to run the tests on Python 3.7 +`. 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt index 3cf0cfdc60..ea3bd64584 100644 --- a/docs/intro/reusable-apps.txt +++ b/docs/intro/reusable-apps.txt @@ -212,16 +212,15 @@ this. For a small app like polls, this process isn't too difficult. Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Topic :: Internet :: WWW/HTTP Topic :: Internet :: WWW/HTTP :: Dynamic Content [options] include_package_data = true packages = find: - python_requires = >=3.6 + python_requires = >=3.8 install_requires = Django >= X.Y # Replace "X.Y" as appropriate diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index 05f99b6f76..c2bfcd80c4 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -23,7 +23,7 @@ in a shell prompt (indicated by the $ prefix): If Django is installed, you should see the version of your installation. If it isn't, you'll get an error telling "No module named django". -This tutorial is written for Django |version|, which supports Python 3.6 and +This tutorial is written for Django |version|, which supports Python 3.8 and later. If the Django version doesn't match, you can refer to the tutorial for your version of Django by using the version switcher at the bottom right corner of this page, or update Django to the newest version. If you're using an older diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 2dcd860400..7252fca57f 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1507,10 +1507,6 @@ May be specified multiple times and combined with :option:`test --tag`. Runs test methods and classes matching test name patterns, in the same way as :option:`unittest's -k option`. 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 diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 843d6ab4de..485009bde6 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -493,9 +493,7 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004 expensive ``get_friends()`` method and wanted to allow calling it without retrieving the cached value, you could write:: - friends = cached_property(get_friends, name='friends') - - You only need the ``name`` argument for Python < 3.6 support. + friends = cached_property(get_friends) While ``person.get_friends()`` will recompute the friends on each call, the value of the cached property will persist until you delete it as described diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index 296967a92b..88a73369ef 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -21,6 +21,8 @@ Python compatibility Django 4.0 supports Python 3.8, 3.9, and 3.10. We **highly recommend** and only officially support the latest release of each series. +The Django 3.2.x series is the last to support Python 3.6 and 3.7. + .. _whats-new-4.0: What's new in Django 4.0 diff --git a/setup.cfg b/setup.cfg index cb4c73e7cb..33a5d9947a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,8 +17,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Topic :: Internet :: WWW/HTTP @@ -34,7 +32,7 @@ project_urls = Tracker = https://code.djangoproject.com/ [options] -python_requires = >=3.6 +python_requires = >=3.8 packages = find: include_package_data = true zip_safe = false diff --git a/setup.py b/setup.py index 4afdf8a79a..ff42140d5a 100644 --- a/setup.py +++ b/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: diff --git a/tests/dbshell/test_sqlite.py b/tests/dbshell/test_sqlite.py index 570230f62d..ea4bdd62e1 100644 --- a/tests/dbshell/test_sqlite.py +++ b/tests/dbshell/test_sqlite.py @@ -13,7 +13,7 @@ class SqliteDbshellCommandTestCase(SimpleTestCase): def test_path_name(self): self.assertEqual( self.settings_to_cmd_args_env({'NAME': Path('test.db.sqlite3')}), - (['sqlite3', 'test.db.sqlite3'], None), + (['sqlite3', Path('test.db.sqlite3')], None), ) def test_parameters(self): diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index 1d445cd38c..dd727cde32 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -5,7 +5,6 @@ from django.db import close_old_connections, connection from django.test import ( RequestFactory, SimpleTestCase, TransactionTestCase, override_settings, ) -from django.utils.version import PY37 class HandlerTests(SimpleTestCase): @@ -183,7 +182,7 @@ class HandlerRequestTests(SimpleTestCase): def test_invalid_urls(self): response = self.client.get('~%A9helloworld') self.assertEqual(response.status_code, 404) - self.assertEqual(response.context['request_path'], '/~%25A9helloworld' if PY37 else '/%7E%25A9helloworld') + self.assertEqual(response.context['request_path'], '/~%25A9helloworld') response = self.client.get('d%aao%aaw%aan%aal%aao%aaa%aad%aa/') self.assertEqual(response.context['request_path'], '/d%25AAo%25AAw%25AAn%25AAl%25AAo%25AAa%25AAd%25AA') diff --git a/tests/managers_regress/tests.py b/tests/managers_regress/tests.py index 421e30ca45..c18cd8adda 100644 --- a/tests/managers_regress/tests.py +++ b/tests/managers_regress/tests.py @@ -1,10 +1,7 @@ -from unittest import skipUnless - from django.db import models from django.template import Context, Template from django.test import SimpleTestCase, TestCase, override_settings from django.test.utils import isolate_apps -from django.utils.version import PY37 from .models import ( AbstractBase1, AbstractBase2, AbstractBase3, Child1, Child2, Child3, @@ -287,6 +284,5 @@ class TestManagerInheritance(SimpleTestCase): self.assertEqual(TestModel._meta.managers, (TestModel.custom_manager,)) self.assertEqual(TestModel._meta.managers_map, {'custom_manager': TestModel.custom_manager}) - @skipUnless(PY37, '__class_getitem__() was added in Python 3.7') def test_manager_class_getitem(self): self.assertIs(models.Manager[Child1], models.Manager) diff --git a/tests/model_inheritance/tests.py b/tests/model_inheritance/tests.py index 60edaa3d99..1ab0e15eee 100644 --- a/tests/model_inheritance/tests.py +++ b/tests/model_inheritance/tests.py @@ -1,11 +1,9 @@ from operator import attrgetter -from unittest import skipUnless from django.core.exceptions import FieldError, ValidationError from django.db import connection, models from django.test import SimpleTestCase, TestCase from django.test.utils import CaptureQueriesContext, isolate_apps -from django.utils.version import PY37 from .models import ( Base, Chef, CommonInfo, GrandChild, GrandParent, ItalianRestaurant, @@ -219,7 +217,6 @@ class ModelInheritanceTests(TestCase): self.assertSequenceEqual(qs, [p2, p1]) self.assertIn(expected_order_by_sql, str(qs.query)) - @skipUnless(PY37, '__class_getitem__() was added in Python 3.7') def test_queryset_class_getitem(self): self.assertIs(models.QuerySet[Post], models.QuerySet) self.assertIs(models.QuerySet[Post, Post], models.QuerySet) diff --git a/tests/runtests.py b/tests/runtests.py index 4cd16809c4..afdf68c21b 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -28,7 +28,6 @@ else: RemovedInDjango41Warning, RemovedInDjango50Warning, ) from django.utils.log import DEFAULT_LOGGING - from django.utils.version import PY37 try: import MySQLdb @@ -521,14 +520,13 @@ if __name__ == "__main__": '--timing', action='store_true', help='Output timings, including database set up and total run time.', ) - if PY37: - parser.add_argument( - '-k', dest='test_name_patterns', action='append', - help=( - 'Only run test methods and classes matching test name pattern. ' - 'Same as unittest -k option. Can be used multiple times.' - ), - ) + parser.add_argument( + '-k', dest='test_name_patterns', action='append', + help=( + 'Only run test methods and classes matching test name pattern. ' + 'Same as unittest -k option. Can be used multiple times.' + ), + ) options = parser.parse_args() diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index 183e283d08..ee95f9da72 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/tests/test_runner/test_discover_runner.py @@ -1,9 +1,7 @@ import os from argparse import ArgumentParser from contextlib import contextmanager -from unittest import ( - TestSuite, TextTestRunner, defaultTestLoader, mock, skipUnless, -) +from unittest import TestSuite, TextTestRunner, defaultTestLoader, mock from django.db import connections from django.test import SimpleTestCase @@ -11,7 +9,6 @@ from django.test.runner import DiscoverRunner from django.test.utils import ( NullTimeKeeper, TimeKeeper, captured_stderr, captured_stdout, ) -from django.utils.version import PY37 @contextmanager @@ -83,7 +80,6 @@ class DiscoverRunnerTests(SimpleTestCase): self.assertEqual(count, 1) - @skipUnless(PY37, 'unittest -k option requires Python 3.7 and later') def test_name_patterns(self): all_test_1 = [ 'DjangoCase1.test_1', 'DjangoCase2.test_1', diff --git a/tests/test_runner/test_parallel.py b/tests/test_runner/test_parallel.py index c1a89bd0f0..70959012b5 100644 --- a/tests/test_runner/test_parallel.py +++ b/tests/test_runner/test_parallel.py @@ -2,7 +2,6 @@ import unittest from django.test import SimpleTestCase from django.test.runner import RemoteTestResult -from django.utils.version import PY37 try: import tblib @@ -80,8 +79,7 @@ class RemoteTestResultTest(SimpleTestCase): event = events[1] self.assertEqual(event[0], 'addSubTest') self.assertEqual(str(event[2]), 'dummy_test (test_runner.test_parallel.SampleFailingSubtest) (index=0)') - trailing_comma = '' if PY37 else ',' - self.assertEqual(repr(event[3][1]), "AssertionError('0 != 1'%s)" % trailing_comma) + self.assertEqual(repr(event[3][1]), "AssertionError('0 != 1')") event = events[2] - self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1'%s)" % trailing_comma) + self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1')") diff --git a/tests/user_commands/management/commands/subparser_dest.py b/tests/user_commands/management/commands/subparser_dest.py index ffea7efac7..000078911d 100644 --- a/tests/user_commands/management/commands/subparser_dest.py +++ b/tests/user_commands/management/commands/subparser_dest.py @@ -1,11 +1,9 @@ from django.core.management.base import BaseCommand -from django.utils.version import PY37 class Command(BaseCommand): def add_arguments(self, parser): - kwargs = {'required': True} if PY37 else {} - subparsers = parser.add_subparsers(dest='subcommand', **kwargs) + subparsers = parser.add_subparsers(dest='subcommand', required=True) parser_foo = subparsers.add_parser('foo') parser_foo.add_argument('--bar') diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index 9262e2717a..05415717c6 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -17,7 +17,6 @@ from django.test import SimpleTestCase, override_settings from django.test.utils import captured_stderr, extend_sys_path, ignore_warnings from django.utils import translation from django.utils.deprecation import RemovedInDjango41Warning -from django.utils.version import PY37 from .management.commands import dance @@ -337,20 +336,9 @@ class CommandTests(SimpleTestCase): msg = "Error: invalid choice: 'test' (choose from 'foo')" with self.assertRaisesMessage(CommandError, msg): management.call_command('subparser', 'test', 12) - if PY37: - # "required" option requires Python 3.7 and later. - msg = 'Error: the following arguments are required: subcommand' - with self.assertRaisesMessage(CommandError, msg): - management.call_command('subparser_dest', subcommand='foo', bar=12) - else: - msg = ( - 'Unknown option(s) for subparser_dest command: subcommand. ' - 'Valid options are: bar, force_color, help, no_color, ' - 'pythonpath, settings, skip_checks, stderr, stdout, ' - 'traceback, verbosity, version.' - ) - with self.assertRaisesMessage(TypeError, msg): - management.call_command('subparser_dest', subcommand='foo', bar=12) + msg = 'Error: the following arguments are required: subcommand' + with self.assertRaisesMessage(CommandError, msg): + management.call_command('subparser_dest', subcommand='foo', bar=12) def test_create_parser_kwargs(self): """BaseCommand.create_parser() passes kwargs to CommandParser.""" diff --git a/tests/utils_tests/test_autoreload.py b/tests/utils_tests/test_autoreload.py index 86c510eace..3cb901af7d 100644 --- a/tests/utils_tests/test_autoreload.py +++ b/tests/utils_tests/test_autoreload.py @@ -195,10 +195,10 @@ class TestChildArguments(SimpleTestCase): with tempfile.TemporaryDirectory() as tmpdir: exe_path = Path(tmpdir) / 'django-admin.exe' exe_path.touch() - with mock.patch('sys.argv', [str(exe_path.with_suffix('')), 'runserver']): + with mock.patch('sys.argv', [exe_path.with_suffix(''), 'runserver']): self.assertEqual( autoreload.get_child_arguments(), - [str(exe_path), 'runserver'] + [exe_path, 'runserver'] ) @mock.patch('sys.warnoptions', []) @@ -206,10 +206,10 @@ class TestChildArguments(SimpleTestCase): with tempfile.TemporaryDirectory() as tmpdir: script_path = Path(tmpdir) / 'django-admin-script.py' script_path.touch() - with mock.patch('sys.argv', [str(script_path.with_name('django-admin')), 'runserver']): + with mock.patch('sys.argv', [script_path.with_name('django-admin'), 'runserver']): self.assertEqual( autoreload.get_child_arguments(), - [sys.executable, str(script_path), 'runserver'] + [sys.executable, script_path, 'runserver'] ) @mock.patch('sys.argv', ['does-not-exist', 'runserver']) diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py index 4c11f91116..675a6e186e 100644 --- a/tests/utils_tests/test_http.py +++ b/tests/utils_tests/test_http.py @@ -7,7 +7,7 @@ from django.test import SimpleTestCase from django.utils.datastructures import MultiValueDict from django.utils.http import ( base36_to_int, escape_leading_slashes, http_date, int_to_base36, - is_same_domain, parse_etags, parse_http_date, parse_qsl, quote_etag, + is_same_domain, parse_etags, parse_http_date, quote_etag, url_has_allowed_host_and_scheme, urlencode, urlsafe_base64_decode, urlsafe_base64_encode, ) @@ -331,68 +331,3 @@ class EscapeLeadingSlashesTests(unittest.TestCase): for url, expected in tests: with self.subTest(url=url): self.assertEqual(escape_leading_slashes(url), expected) - - -# TODO: Remove when dropping support for PY37. Backport of unit tests for -# urllib.parse.parse_qsl() from Python 3.8. Copyright (C) 2020 Python Software -# Foundation (see LICENSE.python). -class ParseQSLBackportTests(unittest.TestCase): - def test_parse_qsl(self): - tests = [ - ('', []), - ('&', []), - ('&&', []), - ('=', [('', '')]), - ('=a', [('', 'a')]), - ('a', [('a', '')]), - ('a=', [('a', '')]), - ('&a=b', [('a', 'b')]), - ('a=a+b&b=b+c', [('a', 'a b'), ('b', 'b c')]), - ('a=1&a=2', [('a', '1'), ('a', '2')]), - (b'', []), - (b'&', []), - (b'&&', []), - (b'=', [(b'', b'')]), - (b'=a', [(b'', b'a')]), - (b'a', [(b'a', b'')]), - (b'a=', [(b'a', b'')]), - (b'&a=b', [(b'a', b'b')]), - (b'a=a+b&b=b+c', [(b'a', b'a b'), (b'b', b'b c')]), - (b'a=1&a=2', [(b'a', b'1'), (b'a', b'2')]), - (';', []), - (';;', []), - (';a=b', [('a', 'b')]), - ('a=a+b;b=b+c', [('a', 'a b'), ('b', 'b c')]), - ('a=1;a=2', [('a', '1'), ('a', '2')]), - (b';', []), - (b';;', []), - (b';a=b', [(b'a', b'b')]), - (b'a=a+b;b=b+c', [(b'a', b'a b'), (b'b', b'b c')]), - (b'a=1;a=2', [(b'a', b'1'), (b'a', b'2')]), - ] - for original, expected in tests: - with self.subTest(original): - result = parse_qsl(original, keep_blank_values=True) - self.assertEqual(result, expected, 'Error parsing %r' % original) - expect_without_blanks = [v for v in expected if len(v[1])] - result = parse_qsl(original, keep_blank_values=False) - self.assertEqual(result, expect_without_blanks, 'Error parsing %r' % original) - - def test_parse_qsl_encoding(self): - result = parse_qsl('key=\u0141%E9', encoding='latin-1') - self.assertEqual(result, [('key', '\u0141\xE9')]) - result = parse_qsl('key=\u0141%C3%A9', encoding='utf-8') - self.assertEqual(result, [('key', '\u0141\xE9')]) - result = parse_qsl('key=\u0141%C3%A9', encoding='ascii') - self.assertEqual(result, [('key', '\u0141\ufffd\ufffd')]) - result = parse_qsl('key=\u0141%E9-', encoding='ascii') - self.assertEqual(result, [('key', '\u0141\ufffd-')]) - result = parse_qsl('key=\u0141%E9-', encoding='ascii', errors='ignore') - self.assertEqual(result, [('key', '\u0141-')]) - - def test_parse_qsl_max_num_fields(self): - with self.assertRaises(ValueError): - parse_qsl('&'.join(['a=a'] * 11), max_num_fields=10) - with self.assertRaises(ValueError): - parse_qsl(';'.join(['a=a'] * 11), max_num_fields=10) - parse_qsl('&'.join(['a=a'] * 10), max_num_fields=10) diff --git a/tox.ini b/tox.ini index 0332fb9f76..fa5e0051d8 100644 --- a/tox.ini +++ b/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