Skip to content

Commit ec0ff40

Browse files
felixxmcarltongibson
authored andcommitted
Fixed #32355 -- Dropped support for Python 3.6 and 3.7
1 parent 9c6ba87 commit ec0ff40

File tree

33 files changed

+59
-274
lines changed

33 files changed

+59
-274
lines changed

INSTALL

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Thanks for downloading Django.
22

3-
To install it, make sure you have Python 3.6 or greater installed. Then run
3+
To install it, make sure you have Python 3.8 or greater installed. Then run
44
this command from the command prompt:
55

66
python -m pip install .

django/core/management/commands/compilemessages.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,7 @@ def compile_messages(self, locations):
154154
self.has_errors = True
155155
return
156156

157-
# PY37: Remove str() when dropping support for PY37.
158-
# https://bugs.python.org/issue31961
159-
args = [self.program, *self.program_options, '-o', str(mo_path), str(po_path)]
157+
args = [self.program, *self.program_options, '-o', mo_path, po_path]
160158
futures.append(executor.submit(popen_wrapper, args))
161159

162160
for future in concurrent.futures.as_completed(futures):

django/db/backends/postgresql/base.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,7 @@ def chunked_cursor(self):
261261
# For now, it's here so that every use of "threading" is
262262
# also async-compatible.
263263
try:
264-
if hasattr(asyncio, 'current_task'):
265-
# Python 3.7 and up
266-
current_task = asyncio.current_task()
267-
else:
268-
# Python 3.6
269-
current_task = asyncio.Task.current_task()
264+
current_task = asyncio.current_task()
270265
except RuntimeError:
271266
current_task = None
272267
# Current task can be none even if the current_task call didn't error

django/db/backends/sqlite3/base.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from django.utils.dateparse import parse_datetime, parse_time
2626
from django.utils.duration import duration_microseconds
2727
from django.utils.regex_helper import _lazy_re_compile
28-
from django.utils.version import PY38
2928

3029
from .client import DatabaseClient
3130
from .creation import DatabaseCreation
@@ -180,9 +179,7 @@ def get_connection_params(self):
180179
"settings.DATABASES is improperly configured. "
181180
"Please supply the NAME value.")
182181
kwargs = {
183-
# TODO: Remove str() when dropping support for PY36.
184-
# https://bugs.python.org/issue33496
185-
'database': str(settings_dict['NAME']),
182+
'database': settings_dict['NAME'],
186183
'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES,
187184
**settings_dict['OPTIONS'],
188185
}
@@ -206,13 +203,10 @@ def get_connection_params(self):
206203
@async_unsafe
207204
def get_new_connection(self, conn_params):
208205
conn = Database.connect(**conn_params)
209-
if PY38:
210-
create_deterministic_function = functools.partial(
211-
conn.create_function,
212-
deterministic=True,
213-
)
214-
else:
215-
create_deterministic_function = conn.create_function
206+
create_deterministic_function = functools.partial(
207+
conn.create_function,
208+
deterministic=True,
209+
)
216210
create_deterministic_function('django_date_extract', 2, _sqlite_datetime_extract)
217211
create_deterministic_function('django_date_trunc', 4, _sqlite_date_trunc)
218212
create_deterministic_function('django_datetime_cast_date', 3, _sqlite_datetime_cast_date)

django/db/backends/sqlite3/client.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,5 @@ class DatabaseClient(BaseDatabaseClient):
66

77
@classmethod
88
def settings_to_cmd_args_env(cls, settings_dict, parameters):
9-
args = [
10-
cls.executable_name,
11-
# TODO: Remove str() when dropping support for PY37. args
12-
# parameter accepts path-like objects on Windows since Python 3.8.
13-
str(settings_dict['NAME']),
14-
*parameters,
15-
]
9+
args = [cls.executable_name, settings_dict['NAME'], *parameters]
1610
return args, None

django/db/migrations/questioner.py

-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ def ask_initial(self, app_label):
4444
except ImportError:
4545
return self.defaults.get("ask_initial", False)
4646
else:
47-
# getattr() needed on PY36 and older (replace with attribute access).
4847
if getattr(migrations_module, "__file__", None):
4948
filenames = os.listdir(os.path.dirname(migrations_module.__file__))
5049
elif hasattr(migrations_module, "__path__"):

django/http/cookie.py

-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
# For backwards compatibility in Django 2.1.
44
SimpleCookie = cookies.SimpleCookie
55

6-
# Add support for the SameSite attribute (obsolete when PY37 is unsupported).
7-
cookies.Morsel._reserved.setdefault('samesite', 'SameSite')
8-
96

107
def parse_cookie(cookie):
118
"""

django/http/request.py

-9
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,10 @@
1818
from django.utils.encoding import escape_uri_path, iri_to_uri
1919
from django.utils.functional import cached_property
2020
from django.utils.http import is_same_domain
21-
from django.utils.inspect import func_supports_parameter
2221
from django.utils.regex_helper import _lazy_re_compile
2322

2423
from .multipartparser import parse_header
2524

26-
# TODO: Remove when dropping support for PY37. inspect.signature() is used to
27-
# detect whether the max_num_fields argument is available as this security fix
28-
# was backported to Python 3.6.8 and 3.7.2, and may also have been applied by
29-
# downstream package maintainers to other versions in their repositories.
30-
if not func_supports_parameter(parse_qsl, 'max_num_fields'):
31-
from django.utils.http import parse_qsl
32-
33-
3425
RAISE_ERROR = object()
3526
host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(:\d+)?$")
3627

django/test/runner.py

+10-12
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
teardown_test_environment,
2222
)
2323
from django.utils.datastructures import OrderedSet
24-
from django.utils.version import PY37
2524

2625
try:
2726
import ipdb as pdb
@@ -240,8 +239,8 @@ def addFailure(self, test, err):
240239
self.stop_if_failfast()
241240

242241
def addSubTest(self, test, subtest, err):
243-
# Follow Python 3.5's implementation of unittest.TestResult.addSubTest()
244-
# by not doing anything when a subtest is successful.
242+
# Follow Python's implementation of unittest.TestResult.addSubTest() by
243+
# not doing anything when a subtest is successful.
245244
if err is not None:
246245
# Call check_picklable() before check_subtest_picklable() since
247246
# check_picklable() performs the tblib check.
@@ -540,15 +539,14 @@ def add_arguments(cls, parser):
540539
'Output timings, including database set up and total run time.'
541540
),
542541
)
543-
if PY37:
544-
parser.add_argument(
545-
'-k', action='append', dest='test_name_patterns',
546-
help=(
547-
'Only run test methods and classes that match the pattern '
548-
'or substring. Can be used multiple times. Same as '
549-
'unittest -k option.'
550-
),
551-
)
542+
parser.add_argument(
543+
'-k', action='append', dest='test_name_patterns',
544+
help=(
545+
'Only run test methods and classes that match the pattern '
546+
'or substring. Can be used multiple times. Same as '
547+
'unittest -k option.'
548+
),
549+
)
552550

553551
def setup_test_environment(self, **kwargs):
554552
setup_test_environment(debug=self.debug_mode)

django/utils/autoreload.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -231,15 +231,11 @@ def get_child_arguments():
231231
exe_entrypoint = py_script.with_suffix('.exe')
232232
if exe_entrypoint.exists():
233233
# Should be executed directly, ignoring sys.executable.
234-
# TODO: Remove str() when dropping support for PY37.
235-
# args parameter accepts path-like on Windows from Python 3.8.
236-
return [str(exe_entrypoint), *sys.argv[1:]]
234+
return [exe_entrypoint, *sys.argv[1:]]
237235
script_entrypoint = py_script.with_name('%s-script.py' % py_script.name)
238236
if script_entrypoint.exists():
239237
# Should be executed as usual.
240-
# TODO: Remove str() when dropping support for PY37.
241-
# args parameter accepts path-like on Windows from Python 3.8.
242-
return [*args, str(script_entrypoint), *sys.argv[1:]]
238+
return [*args, script_entrypoint, *sys.argv[1:]]
243239
raise RuntimeError('Script %s does not exist.' % py_script)
244240
else:
245241
args += sys.argv

django/utils/http.py

+1-73
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from email.utils import formatdate
88
from urllib.parse import (
99
ParseResult, SplitResult, _coerce_args, _splitnetloc, _splitparams,
10-
scheme_chars, unquote, urlencode as original_urlencode, uses_params,
10+
scheme_chars, urlencode as original_urlencode, uses_params,
1111
)
1212

1313
from django.utils.datastructures import MultiValueDict
@@ -343,78 +343,6 @@ def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
343343
(not scheme or scheme in valid_schemes))
344344

345345

346-
# TODO: Remove when dropping support for PY37.
347-
def parse_qsl(
348-
qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8',
349-
errors='replace', max_num_fields=None,
350-
):
351-
"""
352-
Return a list of key/value tuples parsed from query string.
353-
354-
Backport of urllib.parse.parse_qsl() from Python 3.8.
355-
Copyright (C) 2020 Python Software Foundation (see LICENSE.python).
356-
357-
----
358-
359-
Parse a query given as a string argument.
360-
361-
Arguments:
362-
363-
qs: percent-encoded query string to be parsed
364-
365-
keep_blank_values: flag indicating whether blank values in
366-
percent-encoded queries should be treated as blank strings. A
367-
true value indicates that blanks should be retained as blank
368-
strings. The default false value indicates that blank values
369-
are to be ignored and treated as if they were not included.
370-
371-
strict_parsing: flag indicating what to do with parsing errors. If false
372-
(the default), errors are silently ignored. If true, errors raise a
373-
ValueError exception.
374-
375-
encoding and errors: specify how to decode percent-encoded sequences
376-
into Unicode characters, as accepted by the bytes.decode() method.
377-
378-
max_num_fields: int. If set, then throws a ValueError if there are more
379-
than n fields read by parse_qsl().
380-
381-
Returns a list, as G-d intended.
382-
"""
383-
qs, _coerce_result = _coerce_args(qs)
384-
385-
# If max_num_fields is defined then check that the number of fields is less
386-
# than max_num_fields. This prevents a memory exhaustion DOS attack via
387-
# post bodies with many fields.
388-
if max_num_fields is not None:
389-
num_fields = 1 + qs.count('&') + qs.count(';')
390-
if max_num_fields < num_fields:
391-
raise ValueError('Max number of fields exceeded')
392-
393-
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
394-
r = []
395-
for name_value in pairs:
396-
if not name_value and not strict_parsing:
397-
continue
398-
nv = name_value.split('=', 1)
399-
if len(nv) != 2:
400-
if strict_parsing:
401-
raise ValueError("bad query field: %r" % (name_value,))
402-
# Handle case of a control-name with no equal sign.
403-
if keep_blank_values:
404-
nv.append('')
405-
else:
406-
continue
407-
if len(nv[1]) or keep_blank_values:
408-
name = nv[0].replace('+', ' ')
409-
name = unquote(name, encoding=encoding, errors=errors)
410-
name = _coerce_result(name)
411-
value = nv[1].replace('+', ' ')
412-
value = unquote(value, encoding=encoding, errors=errors)
413-
value = _coerce_result(value)
414-
r.append((name, value))
415-
return r
416-
417-
418346
def escape_leading_slashes(url):
419347
"""
420348
If redirecting to an absolute path (two leading slashes), a slash must be

django/utils/module_loading.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,9 @@ def module_has_submodule(package, module_name):
7272
full_module_name = package_name + '.' + module_name
7373
try:
7474
return importlib_find(full_module_name, package_path) is not None
75-
except (ModuleNotFoundError, AttributeError):
75+
except ModuleNotFoundError:
7676
# When module_name is an invalid dotted path, Python raises
77-
# ModuleNotFoundError. AttributeError is raised on PY36 (fixed in PY37)
78-
# if the penultimate part of the path is not a package.
77+
# ModuleNotFoundError.
7978
return False
8079

8180

django/utils/version.py

-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
# or later". So that third-party apps can use these values, each constant
1010
# should remain as long as the oldest supported Django version supports that
1111
# Python version.
12-
PY36 = sys.version_info >= (3, 6)
13-
PY37 = sys.version_info >= (3, 7)
1412
PY38 = sys.version_info >= (3, 8)
1513
PY39 = sys.version_info >= (3, 9)
1614

docs/internals/contributing/writing-code/unit-tests.txt

+5-5
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,14 @@ In addition to the default environments, ``tox`` supports running unit tests
8989
for other versions of Python and other database backends. Since Django's test
9090
suite doesn't bundle a settings file for database backends other than SQLite,
9191
however, you must :ref:`create and provide your own test settings
92-
<running-unit-tests-settings>`. For example, to run the tests on Python 3.7
92+
<running-unit-tests-settings>`. For example, to run the tests on Python 3.9
9393
using PostgreSQL:
9494

9595
.. console::
9696

97-
$ tox -e py37-postgres -- --settings=my_postgres_settings
97+
$ tox -e py39-postgres -- --settings=my_postgres_settings
9898

99-
This command sets up a Python 3.7 virtual environment, installs Django's
99+
This command sets up a Python 3.9 virtual environment, installs Django's
100100
test suite dependencies (including those for PostgreSQL), and calls
101101
``runtests.py`` with the supplied arguments (in this case,
102102
``--settings=my_postgres_settings``).
@@ -110,14 +110,14 @@ set. For example, the following is equivalent to the command above:
110110

111111
.. code-block:: console
112112

113-
$ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py35-postgres
113+
$ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py39-postgres
114114

115115
Windows users should use:
116116

117117
.. code-block:: doscon
118118

119119
...\> set DJANGO_SETTINGS_MODULE=my_postgres_settings
120-
...\> tox -e py35-postgres
120+
...\> tox -e py39-postgres
121121

122122
Running the JavaScript tests
123123
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

docs/intro/reusable-apps.txt

+2-3
Original file line numberDiff line numberDiff line change
@@ -212,16 +212,15 @@ this. For a small app like polls, this process isn't too difficult.
212212
Programming Language :: Python
213213
Programming Language :: Python :: 3
214214
Programming Language :: Python :: 3 :: Only
215-
Programming Language :: Python :: 3.6
216-
Programming Language :: Python :: 3.7
217215
Programming Language :: Python :: 3.8
216+
Programming Language :: Python :: 3.9
218217
Topic :: Internet :: WWW/HTTP
219218
Topic :: Internet :: WWW/HTTP :: Dynamic Content
220219

221220
[options]
222221
include_package_data = true
223222
packages = find:
224-
python_requires = >=3.6
223+
python_requires = >=3.8
225224
install_requires =
226225
Django >= X.Y # Replace "X.Y" as appropriate
227226

docs/intro/tutorial01.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ in a shell prompt (indicated by the $ prefix):
2323
If Django is installed, you should see the version of your installation. If it
2424
isn't, you'll get an error telling "No module named django".
2525

26-
This tutorial is written for Django |version|, which supports Python 3.6 and
26+
This tutorial is written for Django |version|, which supports Python 3.8 and
2727
later. If the Django version doesn't match, you can refer to the tutorial for
2828
your version of Django by using the version switcher at the bottom right corner
2929
of this page, or update Django to the newest version. If you're using an older

docs/ref/django-admin.txt

-4
Original file line numberDiff line numberDiff line change
@@ -1507,10 +1507,6 @@ May be specified multiple times and combined with :option:`test --tag`.
15071507
Runs test methods and classes matching test name patterns, in the same way as
15081508
:option:`unittest's -k option<unittest.-k>`. Can be specified multiple times.
15091509

1510-
.. admonition:: Python 3.7 and later
1511-
1512-
This feature is only available for Python 3.7 and later.
1513-
15141510
.. django-admin-option:: --pdb
15151511

15161512
Spawns a ``pdb`` debugger at each test error or failure. If you have it

docs/ref/utils.txt

+1-3
Original file line numberDiff line numberDiff line change
@@ -493,9 +493,7 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004
493493
expensive ``get_friends()`` method and wanted to allow calling it without
494494
retrieving the cached value, you could write::
495495

496-
friends = cached_property(get_friends, name='friends')
497-
498-
You only need the ``name`` argument for Python < 3.6 support.
496+
friends = cached_property(get_friends)
499497

500498
While ``person.get_friends()`` will recompute the friends on each call, the
501499
value of the cached property will persist until you delete it as described

docs/releases/4.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Python compatibility
2121
Django 4.0 supports Python 3.8, 3.9, and 3.10. We **highly recommend** and only
2222
officially support the latest release of each series.
2323

24+
The Django 3.2.x series is the last to support Python 3.6 and 3.7.
25+
2426
.. _whats-new-4.0:
2527

2628
What's new in Django 4.0

0 commit comments

Comments
 (0)