Browse Source

Fixed #27857 -- Dropped support for Python 3.4.

Tim Graham 8 years ago
parent
commit
cfff2af02b

+ 1 - 1
INSTALL

@@ -1,6 +1,6 @@
 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:
 
     python setup.py install

+ 0 - 6
django/db/models/expressions.py

@@ -150,12 +150,6 @@ class BaseExpression:
         if output_field is not None:
             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):
         return (
             []

+ 2 - 15
django/http/cookie.py

@@ -1,20 +1,7 @@
-import sys
 from http import cookies
 
-# Cookie pickling bug is fixed in Python 3.4.3+
-# http://bugs.python.org/issue22775
-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)
+# For backwards compatibility in Django 2.1.
+SimpleCookie = cookies.SimpleCookie
 
 
 def parse_cookie(cookie):

+ 6 - 3
django/test/html.py

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

+ 1 - 4
django/urls/converters.py

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

+ 1 - 3
django/urls/resolvers.py

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

+ 5 - 13
django/utils/html.py

@@ -1,6 +1,7 @@
 """HTML utilities suitable for global use."""
 
 import re
+from html.parser import HTMLParser
 from urllib.parse import (
     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.text import normalize_newlines
 
-from .html_parser import HTMLParseError, HTMLParser
-
 # Configuration for urlize() function.
 TRAILING_PUNCTUATION_RE = re.compile(
     '^'           # Beginning of word
@@ -132,7 +131,7 @@ def linebreaks(value, autoescape=False):
 
 class MLStripper(HTMLParser):
     def __init__(self):
-        HTMLParser.__init__(self)
+        HTMLParser.__init__(self, convert_charrefs=False)
         self.reset()
         self.fed = []
 
@@ -154,16 +153,9 @@ def _strip_once(value):
     Internal tag stripping utility used by strip_tags.
     """
     s = MLStripper()
-    try:
-        s.feed(value)
-    except HTMLParseError:
-        return value
-    try:
-        s.close()
-    except HTMLParseError:
-        return s.get_data() + s.rawdata
-    else:
-        return s.get_data()
+    s.feed(value)
+    s.close()
+    return s.get_data()
 
 
 @keep_lazy_text

+ 0 - 17
django/utils/html_parser.py

@@ -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)

+ 3 - 3
docs/intro/contributing.txt

@@ -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
 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
-:ref:`running-unit-tests` for more information. If you're using Python 3.5+,
-there will be a couple failures related to deprecation warnings that you can
-ignore. These failures have since been fixed in Django.
+:ref:`running-unit-tests` for more information. There will be a couple failures
+related to deprecation warnings that you can ignore. These failures have since
+been fixed in Django.
 
 Note that the latest Django trunk may not always be stable. When developing
 against trunk, you can check `Django's continuous integration builds`__ to

+ 1 - 1
docs/intro/install.txt

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

+ 1 - 1
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| 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
 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

+ 1 - 1
docs/ref/applications.txt

@@ -192,7 +192,7 @@ Configurable attributes
 .. attribute:: AppConfig.path
 
     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
     also provide an explicit override as a class attribute on your

+ 0 - 1
setup.py

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

+ 0 - 8
tests/handlers/tests.py

@@ -1,5 +1,3 @@
-import unittest
-
 from django.core.exceptions import ImproperlyConfigured
 from django.core.handlers.wsgi import WSGIHandler, WSGIRequest, get_script_name
 from django.core.signals import request_finished, request_started
@@ -8,11 +6,6 @@ from django.test import (
     RequestFactory, SimpleTestCase, TransactionTestCase, override_settings,
 )
 
-try:
-    from http import HTTPStatus
-except ImportError:  # Python < 3.5
-    HTTPStatus = None
-
 
 class HandlerTests(SimpleTestCase):
 
@@ -182,7 +175,6 @@ class HandlerRequestTests(SimpleTestCase):
         environ = RequestFactory().get('/%E2%A8%87%87%A5%E2%A8%A0').environ
         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 start_response(status, headers):
             start_response.status = status

+ 2 - 5
tests/handlers/views.py

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

+ 3 - 6
tests/mail/tests.py

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

+ 1 - 6
tests/servers/tests.py

@@ -5,7 +5,7 @@ import errno
 import os
 import socket
 import sys
-from http.client import HTTPConnection
+from http.client import HTTPConnection, RemoteDisconnected
 from urllib.error import HTTPError
 from urllib.parse import urlencode
 from urllib.request import urlopen
@@ -14,11 +14,6 @@ from django.test import LiveServerTestCase, override_settings
 
 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_SETTINGS = {
     'MEDIA_URL': '/media/',

+ 2 - 5
tests/sessions_tests/tests.py

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

+ 7 - 13
tests/test_runner/test_debug_sql.py

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

+ 0 - 4
tests/test_utils/tests.py

@@ -1,5 +1,4 @@
 import os
-import sys
 import unittest
 from io import StringIO
 from unittest import mock
@@ -684,9 +683,6 @@ class HTMLEqualTests(SimpleTestCase):
         error_msg = (
             "First argument is not valid HTML:\n"
             "('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):
             self.assertHTMLEqual('< div></ div>', '<div></div>')