Fixed #27857 -- Dropped support for Python 3.4.

This commit is contained in:
Tim Graham 2017-02-17 19:45:34 -05:00
parent a80903b711
commit cfff2af02b
20 changed files with 37 additions and 116 deletions

View File

@ -1,6 +1,6 @@
Thanks for downloading Django. Thanks for downloading Django.
To install it, make sure you have Python 3.4 or greater installed. Then run To install it, make sure you have Python 3.5 or greater installed. Then run
this command from the command prompt: this command from the command prompt:
python setup.py install python setup.py install

View File

@ -150,12 +150,6 @@ class BaseExpression:
if output_field is not None: if output_field is not None:
self.output_field = output_field self.output_field = output_field
def __getstate__(self):
# This method required only for Python 3.4.
state = self.__dict__.copy()
state.pop('convert_value', None)
return state
def get_db_converters(self, connection): def get_db_converters(self, connection):
return ( return (
[] []

View File

@ -1,20 +1,7 @@
import sys
from http import cookies from http import cookies
# Cookie pickling bug is fixed in Python 3.4.3+ # For backwards compatibility in Django 2.1.
# http://bugs.python.org/issue22775 SimpleCookie = cookies.SimpleCookie
if sys.version_info >= (3, 4, 3):
SimpleCookie = cookies.SimpleCookie
else:
Morsel = cookies.Morsel
class SimpleCookie(cookies.SimpleCookie):
def __setitem__(self, key, value):
if isinstance(value, Morsel):
# allow assignment of constructed Morsels (e.g. for pickling)
dict.__setitem__(self, key, value)
else:
super().__setitem__(key, value)
def parse_cookie(cookie): def parse_cookie(cookie):

View File

@ -1,8 +1,7 @@
"""Compare two HTML documents.""" """Compare two HTML documents."""
import re import re
from html.parser import HTMLParser
from django.utils.html_parser import HTMLParseError, HTMLParser
WHITESPACE = re.compile(r'\s+') WHITESPACE = re.compile(r'\s+')
@ -138,6 +137,10 @@ class RootElement(Element):
return ''.join(str(c) for c in self.children) return ''.join(str(c) for c in self.children)
class HTMLParseError(Exception):
pass
class Parser(HTMLParser): class Parser(HTMLParser):
SELF_CLOSING_TAGS = ( SELF_CLOSING_TAGS = (
'br', 'hr', 'input', 'img', 'meta', 'spacer', 'link', 'frame', 'base', 'br', 'hr', 'input', 'img', 'meta', 'spacer', 'link', 'frame', 'base',
@ -145,7 +148,7 @@ class Parser(HTMLParser):
) )
def __init__(self): def __init__(self):
HTMLParser.__init__(self) HTMLParser.__init__(self, convert_charrefs=False)
self.root = RootElement() self.root = RootElement()
self.open_tags = [] self.open_tags = []
self.element_positions = {} self.element_positions = {}

View File

@ -60,10 +60,7 @@ def register_converter(converter, type_name):
@lru_cache.lru_cache(maxsize=None) @lru_cache.lru_cache(maxsize=None)
def get_converters(): def get_converters():
converters = {} return {**DEFAULT_CONVERTERS, **REGISTERED_CONVERTERS}
converters.update(DEFAULT_CONVERTERS)
converters.update(REGISTERED_CONVERTERS)
return converters
def get_converter(raw_converter): def get_converter(raw_converter):

View File

@ -343,9 +343,7 @@ class URLPattern:
'path.to.ClassBasedView'). 'path.to.ClassBasedView').
""" """
callback = self.callback callback = self.callback
# Python 3.5 collapses nested partials, so can change "while" to "if" if isinstance(callback, functools.partial):
# when it's the minimum supported version.
while isinstance(callback, functools.partial):
callback = callback.func callback = callback.func
if not hasattr(callback, '__name__'): if not hasattr(callback, '__name__'):
return callback.__module__ + "." + callback.__class__.__name__ return callback.__module__ + "." + callback.__class__.__name__

View File

@ -1,6 +1,7 @@
"""HTML utilities suitable for global use.""" """HTML utilities suitable for global use."""
import re import re
from html.parser import HTMLParser
from urllib.parse import ( from urllib.parse import (
parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit, parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit,
) )
@ -11,8 +12,6 @@ from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS
from django.utils.safestring import SafeData, SafeText, mark_safe from django.utils.safestring import SafeData, SafeText, mark_safe
from django.utils.text import normalize_newlines from django.utils.text import normalize_newlines
from .html_parser import HTMLParseError, HTMLParser
# Configuration for urlize() function. # Configuration for urlize() function.
TRAILING_PUNCTUATION_RE = re.compile( TRAILING_PUNCTUATION_RE = re.compile(
'^' # Beginning of word '^' # Beginning of word
@ -132,7 +131,7 @@ def linebreaks(value, autoescape=False):
class MLStripper(HTMLParser): class MLStripper(HTMLParser):
def __init__(self): def __init__(self):
HTMLParser.__init__(self) HTMLParser.__init__(self, convert_charrefs=False)
self.reset() self.reset()
self.fed = [] self.fed = []
@ -154,16 +153,9 @@ def _strip_once(value):
Internal tag stripping utility used by strip_tags. Internal tag stripping utility used by strip_tags.
""" """
s = MLStripper() s = MLStripper()
try: s.feed(value)
s.feed(value) s.close()
except HTMLParseError: return s.get_data()
return value
try:
s.close()
except HTMLParseError:
return s.get_data() + s.rawdata
else:
return s.get_data()
@keep_lazy_text @keep_lazy_text

View File

@ -1,17 +0,0 @@
import html.parser
try:
HTMLParseError = html.parser.HTMLParseError
except AttributeError:
# create a dummy class for Python 3.5+ where it's been removed
class HTMLParseError(Exception):
pass
class HTMLParser(html.parser.HTMLParser):
"""Explicitly set convert_charrefs to be False.
This silences a deprecation warning on Python 3.4.
"""
def __init__(self, convert_charrefs=False, **kwargs):
html.parser.HTMLParser.__init__(self, convert_charrefs=convert_charrefs, **kwargs)

View File

@ -288,9 +288,9 @@ Once the tests complete, you should be greeted with a message informing you
whether the test suite passed or failed. Since you haven't yet made any changes whether the test suite passed or failed. Since you haven't yet made any changes
to Django's code, the entire test suite **should** pass. If you get failures or to Django's code, the entire test suite **should** pass. If you get failures or
errors make sure you've followed all of the previous steps properly. See errors make sure you've followed all of the previous steps properly. See
:ref:`running-unit-tests` for more information. If you're using Python 3.5+, :ref:`running-unit-tests` for more information. There will be a couple failures
there will be a couple failures related to deprecation warnings that you can related to deprecation warnings that you can ignore. These failures have since
ignore. These failures have since been fixed in Django. been fixed in Django.
Note that the latest Django trunk may not always be stable. When developing Note that the latest Django trunk may not always be stable. When developing
against trunk, you can check `Django's continuous integration builds`__ to against trunk, you can check `Django's continuous integration builds`__ to

View File

@ -29,7 +29,7 @@ your operating system's package manager.
You can verify that Python is installed by typing ``python`` from your shell; You can verify that Python is installed by typing ``python`` from your shell;
you should see something like:: you should see something like::
Python 3.4.x Python 3.x.y
[GCC 4.x] on linux [GCC 4.x] on linux
Type "help", "copyright", "credits" or "license" for more information. Type "help", "copyright", "credits" or "license" for more information.
>>> >>>

View File

@ -23,7 +23,7 @@ in a shell prompt (indicated by the $ prefix):
If Django is installed, you should see the version of your installation. If it 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| and Python 3.4 or later. If the This tutorial is written for Django |version| and Python 3.5 or later. If the
Django version doesn't match, you can refer to the tutorial for your version 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 of Django by using the version switcher at the bottom right corner of this
page, or update Django to the newest version. If you are still using Python page, or update Django to the newest version. If you are still using Python

View File

@ -192,7 +192,7 @@ Configurable attributes
.. attribute:: AppConfig.path .. attribute:: AppConfig.path
Filesystem path to the application directory, e.g. Filesystem path to the application directory, e.g.
``'/usr/lib/python3.4/dist-packages/django/contrib/admin'``. ``'/usr/lib/pythonX.Y/dist-packages/django/contrib/admin'``.
In most cases, Django can automatically detect and set this, but you can In most cases, Django can automatically detect and set this, but you can
also provide an explicit override as a class attribute on your also provide an explicit override as a class attribute on your

View File

@ -62,7 +62,6 @@ setup(
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP',

View File

@ -1,5 +1,3 @@
import unittest
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler, WSGIRequest, get_script_name from django.core.handlers.wsgi import WSGIHandler, WSGIRequest, get_script_name
from django.core.signals import request_finished, request_started from django.core.signals import request_finished, request_started
@ -8,11 +6,6 @@ from django.test import (
RequestFactory, SimpleTestCase, TransactionTestCase, override_settings, RequestFactory, SimpleTestCase, TransactionTestCase, override_settings,
) )
try:
from http import HTTPStatus
except ImportError: # Python < 3.5
HTTPStatus = None
class HandlerTests(SimpleTestCase): class HandlerTests(SimpleTestCase):
@ -182,7 +175,6 @@ class HandlerRequestTests(SimpleTestCase):
environ = RequestFactory().get('/%E2%A8%87%87%A5%E2%A8%A0').environ environ = RequestFactory().get('/%E2%A8%87%87%A5%E2%A8%A0').environ
self.assertIsInstance(environ['PATH_INFO'], str) self.assertIsInstance(environ['PATH_INFO'], str)
@unittest.skipIf(HTTPStatus is None, 'HTTPStatus only exists on Python 3.5+')
def test_handle_accepts_httpstatus_enum_value(self): def test_handle_accepts_httpstatus_enum_value(self):
def start_response(status, headers): def start_response(status, headers):
start_response.status = status start_response.status = status

View File

@ -1,13 +1,10 @@
from http import HTTPStatus
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.db import connection, transaction from django.db import connection, transaction
from django.http import HttpResponse, StreamingHttpResponse from django.http import HttpResponse, StreamingHttpResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
try:
from http import HTTPStatus
except ImportError: # Python < 3.5
pass
def regular(request): def regular(request):
return HttpResponse(b"regular content") return HttpResponse(b"regular content")

View File

@ -61,10 +61,7 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
def iter_attachments(): def iter_attachments():
for i in email_message.walk(): for i in email_message.walk():
# Once support for Python<3.5 has been dropped, we can use if i.get_content_disposition() == 'attachment':
# i.get_content_disposition() here instead.
content_disposition = i.get('content-disposition', '').split(';')[0].lower()
if content_disposition == 'attachment':
filename = i.get_filename() filename = i.get_filename()
content = i.get_payload(decode=True) content = i.get_payload(decode=True)
mimetype = i.get_content_type() mimetype = i.get_content_type()
@ -1161,8 +1158,8 @@ class FakeSMTPServer(smtpd.SMTPServer, threading.Thread):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
threading.Thread.__init__(self) threading.Thread.__init__(self)
# New kwarg added in Python 3.5; default switching to False in 3.6. # New kwarg added in Python 3.5; default switching to False in 3.6.
if sys.version_info >= (3, 5): # Setting a value only silences a deprecation warning in Python 3.5.
kwargs['decode_data'] = True kwargs['decode_data'] = True
smtpd.SMTPServer.__init__(self, *args, **kwargs) smtpd.SMTPServer.__init__(self, *args, **kwargs)
self._sink = [] self._sink = []
self.active = False self.active = False

View File

@ -5,7 +5,7 @@ import errno
import os import os
import socket import socket
import sys import sys
from http.client import HTTPConnection from http.client import HTTPConnection, RemoteDisconnected
from urllib.error import HTTPError from urllib.error import HTTPError
from urllib.parse import urlencode from urllib.parse import urlencode
from urllib.request import urlopen from urllib.request import urlopen
@ -14,11 +14,6 @@ from django.test import LiveServerTestCase, override_settings
from .models import Person from .models import Person
try:
from http.client import RemoteDisconnected
except ImportError: # Python 3.4
from http.client import BadStatusLine as RemoteDisconnected
TEST_ROOT = os.path.dirname(__file__) TEST_ROOT = os.path.dirname(__file__)
TEST_SETTINGS = { TEST_SETTINGS = {
'MEDIA_URL': '/media/', 'MEDIA_URL': '/media/',

View File

@ -2,7 +2,6 @@ import base64
import os import os
import shutil import shutil
import string import string
import sys
import tempfile import tempfile
import unittest import unittest
from datetime import timedelta from datetime import timedelta
@ -733,10 +732,9 @@ class SessionMiddlewareTests(TestCase):
# A deleted cookie header looks like: # A deleted cookie header looks like:
# Set-Cookie: sessionid=; expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/ # Set-Cookie: sessionid=; expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/
self.assertEqual( self.assertEqual(
'Set-Cookie: {}={}; expires=Thu, 01-Jan-1970 00:00:00 GMT; ' 'Set-Cookie: {}=""; expires=Thu, 01-Jan-1970 00:00:00 GMT; '
'Max-Age=0; Path=/'.format( 'Max-Age=0; Path=/'.format(
settings.SESSION_COOKIE_NAME, settings.SESSION_COOKIE_NAME,
'""' if sys.version_info >= (3, 5) else '',
), ),
str(response.cookies[settings.SESSION_COOKIE_NAME]) str(response.cookies[settings.SESSION_COOKIE_NAME])
) )
@ -763,10 +761,9 @@ class SessionMiddlewareTests(TestCase):
# expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; # expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0;
# Path=/example/ # Path=/example/
self.assertEqual( self.assertEqual(
'Set-Cookie: {}={}; Domain=.example.local; expires=Thu, ' 'Set-Cookie: {}=""; Domain=.example.local; expires=Thu, '
'01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/example/'.format( '01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/example/'.format(
settings.SESSION_COOKIE_NAME, settings.SESSION_COOKIE_NAME,
'""' if sys.version_info >= (3, 5) else '',
), ),
str(response.cookies[settings.SESSION_COOKIE_NAME]) str(response.cookies[settings.SESSION_COOKIE_NAME])
) )

View File

@ -1,4 +1,3 @@
import sys
import unittest import unittest
from io import StringIO from io import StringIO
@ -94,18 +93,13 @@ class TestDebugSQL(unittest.TestCase):
] ]
verbose_expected_outputs = [ verbose_expected_outputs = [
# Output format changed in Python 3.5+ 'runTest (test_runner.test_debug_sql.TestDebugSQL.FailingTest) ... FAIL',
x.format('' if sys.version_info < (3, 5) else 'TestDebugSQL.') for x in [ 'runTest (test_runner.test_debug_sql.TestDebugSQL.ErrorTest) ... ERROR',
'runTest (test_runner.test_debug_sql.{}FailingTest) ... FAIL', 'runTest (test_runner.test_debug_sql.TestDebugSQL.PassingTest) ... ok',
'runTest (test_runner.test_debug_sql.{}ErrorTest) ... ERROR', # If there are errors/failures in subtests but not in test itself,
'runTest (test_runner.test_debug_sql.{}PassingTest) ... ok', # the status is not written. That behavior comes from Python.
'runTest (test_runner.test_debug_sql.{}PassingSubTest) ... ok', 'runTest (test_runner.test_debug_sql.TestDebugSQL.FailingSubTest) ...',
# If there are errors/failures in subtests but not in test itself, 'runTest (test_runner.test_debug_sql.TestDebugSQL.ErrorSubTest) ...',
# the status is not written. That behavior comes from Python.
'runTest (test_runner.test_debug_sql.{}FailingSubTest) ...',
'runTest (test_runner.test_debug_sql.{}ErrorSubTest) ...',
]
] + [
('''SELECT COUNT(*) AS "__count" ''' ('''SELECT COUNT(*) AS "__count" '''
'''FROM "test_runner_person" WHERE ''' '''FROM "test_runner_person" WHERE '''
'''"test_runner_person"."first_name" = 'pass';'''), '''"test_runner_person"."first_name" = 'pass';'''),

View File

@ -1,5 +1,4 @@
import os import os
import sys
import unittest import unittest
from io import StringIO from io import StringIO
from unittest import mock from unittest import mock
@ -684,9 +683,6 @@ class HTMLEqualTests(SimpleTestCase):
error_msg = ( error_msg = (
"First argument is not valid HTML:\n" "First argument is not valid HTML:\n"
"('Unexpected end tag `div` (Line 1, Column 6)', (1, 6))" "('Unexpected end tag `div` (Line 1, Column 6)', (1, 6))"
) if sys.version_info >= (3, 5) else (
"First argument is not valid HTML:\n"
"Unexpected end tag `div` (Line 1, Column 6), at line 1, column 7"
) )
with self.assertRaisesMessage(AssertionError, error_msg): with self.assertRaisesMessage(AssertionError, error_msg):
self.assertHTMLEqual('< div></ div>', '<div></div>') self.assertHTMLEqual('< div></ div>', '<div></div>')