From 7e6b214ed34f5562dbd83cf54924a5b589a29715 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 18 Jan 2019 10:04:29 -0500 Subject: [PATCH] Fixed #30116 -- Dropped support for Python 3.5. --- INSTALL | 2 +- django/core/checks/model_checks.py | 6 ++-- django/db/backends/base/features.py | 4 --- django/db/backends/sqlite3/features.py | 3 -- django/db/transaction.py | 8 ----- django/utils/functional.py | 29 ++---------------- django/utils/module_loading.py | 9 +++--- docs/howto/windows.txt | 6 ++-- .../contributing/writing-code/unit-tests.txt | 6 ++-- docs/intro/reusable-apps.txt | 2 +- docs/intro/tutorial01.txt | 2 +- docs/ref/utils.txt | 12 -------- docs/topics/testing/tools.txt | 17 ----------- setup.py | 3 +- tests/admin_scripts/tests.py | 3 +- tests/mail/tests.py | 5 +--- tests/migrations/test_writer.py | 6 +--- tests/model_inheritance/tests.py | 4 --- tests/runtests.py | 2 +- tests/transactions/tests.py | 2 -- tests/utils_tests/test_functional.py | 30 ------------------- tests/view_tests/tests/test_debug.py | 3 +- tox.ini | 2 +- 23 files changed, 25 insertions(+), 141 deletions(-) diff --git a/INSTALL b/INSTALL index dda9b4c4e59..eb9cf6eaf2a 100644 --- a/INSTALL +++ b/INSTALL @@ -1,6 +1,6 @@ Thanks for downloading Django. -To install it, make sure you have Python 3.5 or greater installed. Then run +To install it, make sure you have Python 3.6 or greater installed. Then run this command from the command prompt: python setup.py install diff --git a/django/core/checks/model_checks.py b/django/core/checks/model_checks.py index 8514fb4d12c..6c6ac2c7f4d 100644 --- a/django/core/checks/model_checks.py +++ b/django/core/checks/model_checks.py @@ -77,10 +77,8 @@ def _check_lazy_references(apps, ignore=None): """ operation, args, keywords = obj, [], {} while hasattr(operation, 'func'): - # The or clauses are redundant but work around a bug (#25945) in - # functools.partial in Python <= 3.5.1. - args.extend(getattr(operation, 'args', []) or []) - keywords.update(getattr(operation, 'keywords', {}) or {}) + args.extend(getattr(operation, 'args', [])) + keywords.update(getattr(operation, 'keywords', {})) operation = operation.func return operation, args, keywords diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index e0b7372eb1a..db85c883e09 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -160,10 +160,6 @@ class BaseDatabaseFeatures: # Support for the DISTINCT ON clause can_distinct_on_fields = False - # Does the backend decide to commit before SAVEPOINT statements - # when autocommit is disabled? https://bugs.python.org/issue8145#msg109965 - autocommits_when_autocommit_is_off = False - # Does the backend prevent running SQL queries in broken transactions? atomic_transactions = True diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index 40b99a07b97..363329edfd3 100644 --- a/django/db/backends/sqlite3/features.py +++ b/django/db/backends/sqlite3/features.py @@ -1,5 +1,3 @@ -import sys - from django.db.backends.base.features import BaseDatabaseFeatures from .base import Database @@ -15,7 +13,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_timezones = False max_query_params = 999 supports_mixed_date_datetime_comparisons = False - autocommits_when_autocommit_is_off = sys.version_info < (3, 6) can_introspect_autofield = True can_introspect_decimal_field = False can_introspect_duration_field = False diff --git a/django/db/transaction.py b/django/db/transaction.py index 901d8b62e7a..508a10c9246 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -173,14 +173,6 @@ class Atomic(ContextDecorator): connection.commit_on_exit = True connection.needs_rollback = False if not connection.get_autocommit(): - # sqlite3 in Python < 3.6 doesn't handle transactions and - # savepoints properly when autocommit is off. - # Turning autocommit back on isn't an option; it would trigger - # a premature commit. Give up if that happens. - if connection.features.autocommits_when_autocommit_is_off: - raise TransactionManagementError( - "Your database backend doesn't behave properly when " - "autocommit is off. Turn it on before using 'atomic'.") # Pretend we're already in an atomic block to bypass the code # that disables autocommit to enter a transaction, and make a # note to deal with this case in __exit__. diff --git a/django/utils/functional.py b/django/utils/functional.py index 64c79d3361d..6118c108ef0 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -3,8 +3,6 @@ import itertools import operator from functools import total_ordering, wraps -from django.utils.version import PY36, get_docs_version - # You can't trivially replace this with `functools.partial` because this binds # to classes and returns bound instances, whereas functools.partial (on @@ -22,8 +20,8 @@ class cached_property: A cached property can be made out of an existing method: (e.g. ``url = cached_property(get_absolute_url)``). - On Python < 3.6, the optional ``name`` argument must be provided, e.g. - ``url = cached_property(get_absolute_url, name='url')``. + The optional ``name`` argument is obsolete as of Python 3.6 and will be + deprecated in Django 4.0 (#30127). """ name = None @@ -34,29 +32,8 @@ class cached_property: '__set_name__() on it.' ) - @staticmethod - def _is_mangled(name): - return name.startswith('__') and not name.endswith('__') - def __init__(self, func, name=None): - if PY36: - self.real_func = func - else: - func_name = func.__name__ - name = name or func_name - if not (isinstance(name, str) and name.isidentifier()): - raise ValueError( - "%r can't be used as the name of a cached_property." % name, - ) - if self._is_mangled(name): - raise ValueError( - 'cached_property does not work with mangled methods on ' - 'Python < 3.6 without the appropriate `name` argument. See ' - 'https://docs.djangoproject.com/en/%s/ref/utils/' - '#cached-property-mangled-name' % get_docs_version(), - ) - self.name = name - self.func = func + self.real_func = func self.__doc__ = getattr(func, '__doc__') def __set_name__(self, owner, name): diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index 38119431fa8..df8e65098d4 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -72,11 +72,10 @@ 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 (ImportError, AttributeError): - # When module_name is an invalid dotted path, Python raises ImportError - # (or ModuleNotFoundError in Python 3.6+). AttributeError may be raised + except (ModuleNotFoundError, AttributeError): + # 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. - # (https://bugs.python.org/issue30436) return False @@ -87,7 +86,7 @@ def module_dir(module): Raise ValueError otherwise, e.g. for namespace packages that are split over several directories. """ - # Convert to list because _NamespacePath does not support indexing on 3.3. + # Convert to list because _NamespacePath does not support indexing. paths = list(getattr(module, '__path__', [])) if len(paths) == 1: return paths[0] diff --git a/docs/howto/windows.txt b/docs/howto/windows.txt index c37c23eb493..0939ddd7c4b 100644 --- a/docs/howto/windows.txt +++ b/docs/howto/windows.txt @@ -2,7 +2,7 @@ How to install Django on Windows ================================ -This document will guide you through installing Python 3.5 and Django on +This document will guide you through installing Python 3.7 and Django on Windows. It also provides instructions for installing `virtualenv`_ and `virtualenvwrapper`_, which make it easier to work on Python projects. This is meant as a beginner's guide for users working on Django projects and does not @@ -17,12 +17,12 @@ Install Python ============== Django is a Python web framework, thus requiring Python to be installed on your -machine. At the time of writing, Python 3.5 is the latest version. +machine. At the time of writing, Python 3.7 is the latest version. To install Python on your machine go to https://python.org/downloads/. The website should offer you a download button for the latest Python version. Download the executable installer and run it. Check the box next to ``Add -Python 3.5 to PATH`` and then click ``Install Now``. +Python 3.7 to PATH`` and then click ``Install Now``. After installation, open the command prompt and check that the Python version matches the version you installed by executing:: diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 3480e3f2cac..51d1e613135 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -90,12 +90,12 @@ 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.5 +`. For example, to run the tests on Python 3.7 using PostgreSQL:: - $ tox -e py35-postgres -- --settings=my_postgres_settings + $ tox -e py37-postgres -- --settings=my_postgres_settings -This command sets up a Python 3.5 virtual environment, installs Django's +This command sets up a Python 3.7 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``). diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt index 61c413de00d..f156c2a63b5 100644 --- a/docs/intro/reusable-apps.txt +++ b/docs/intro/reusable-apps.txt @@ -219,8 +219,8 @@ this. For a small app like polls, this process isn't too difficult. 'License :: OSI Approved :: BSD License', # example license 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index 208a4f3a3b0..c25e363d658 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.5 and +This tutorial is written for Django |version|, which supports Python 3.6 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/utils.txt b/docs/ref/utils.txt index 84cffddde06..300d4c1bea6 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -514,18 +514,6 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004 z = person.friends # does not call x is z # is True - .. warning:: - - .. _cached-property-mangled-name: - - On Python < 3.6, ``cached_property`` doesn't work properly with a - mangled__ name unless it's passed a ``name`` of the form - ``_Class__attribute``:: - - __friends = cached_property(get_friends, name='_Person__friends') - - __ https://docs.python.org/faq/programming.html#i-try-to-use-spam-and-i-get-an-error-about-someclassname-spam - .. function:: keep_lazy(func, *resultclasses) Django offers many utility functions (particularly in ``django.utils``) diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 233edde2ce2..4ffe84dd046 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -1329,23 +1329,6 @@ The decorator can also be applied to test case classes:: decorator. For a given class, :func:`~django.test.modify_settings` is always applied after :func:`~django.test.override_settings`. -.. admonition:: Considerations with Python 3.5 - - If using Python 3.5 (or older, if using an older version of Django), avoid - mixing ``remove`` with ``append`` and ``prepend`` in - :func:`~django.test.modify_settings`. In some cases it matters whether a - value is first added and then removed or vice versa, and dictionary key - order isn't preserved until Python 3.6. Instead, apply the decorator twice - to guarantee the order of operations. For example, to ensure that - ``SessionMiddleware`` appears first in ``MIDDLEWARE``:: - - @modify_settings(MIDDLEWARE={ - 'remove': ['django.contrib.sessions.middleware.SessionMiddleware'], - ) - @modify_settings(MIDDLEWARE={ - 'prepend': ['django.contrib.sessions.middleware.SessionMiddleware'], - }) - .. warning:: The settings file contains some settings that are only consulted during diff --git a/setup.py b/setup.py index 227af529c7d..41617c5bd14 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from distutils.sysconfig import get_python_lib from setuptools import find_packages, setup CURRENT_PYTHON = sys.version_info[:2] -REQUIRED_PYTHON = (3, 5) +REQUIRED_PYTHON = (3, 6) # This check and everything above must remain compatible with Python 2.7. if CURRENT_PYTHON < REQUIRED_PYTHON: @@ -98,7 +98,6 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3 :: Only', diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index aeec3724b69..8e00a86071d 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -32,7 +32,6 @@ from django.db.migrations.recorder import MigrationRecorder from django.test import ( LiveServerTestCase, SimpleTestCase, TestCase, override_settings, ) -from django.utils.version import PY36 custom_templates_dir = os.path.join(os.path.dirname(__file__), 'custom_templates') @@ -1145,7 +1144,7 @@ class ManageCheck(AdminScriptTestCase): args = ['check'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, 'ModuleNotFoundError' if PY36 else 'ImportError') + self.assertOutput(err, 'ModuleNotFoundError') self.assertOutput(err, 'No module named') self.assertOutput(err, 'admin_scriptz') diff --git a/tests/mail/tests.py b/tests/mail/tests.py index e283101b79f..029d332c427 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -1211,10 +1211,7 @@ 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. - # Setting a value only silences a deprecation warning in Python 3.5. - kwargs['decode_data'] = True - smtpd.SMTPServer.__init__(self, *args, **kwargs) + smtpd.SMTPServer.__init__(self, *args, decode_data=True, **kwargs) self._sink = [] self.active = False self.active_lock = threading.Lock() diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index abeeaf51829..dd7cff2abb0 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -22,7 +22,6 @@ from django.utils.deconstruct import deconstructible from django.utils.functional import SimpleLazyObject from django.utils.timezone import get_default_timezone, get_fixed_timezone, utc from django.utils.translation import gettext_lazy as _ -from django.utils.version import PY36 from .models import FoodManager, FoodQuerySet @@ -413,10 +412,7 @@ class WriterTests(SimpleTestCase): # Test a string regex with flag validator = RegexValidator(r'^[0-9]+$', flags=re.S) string = MigrationWriter.serialize(validator)[0] - if PY36: - self.assertEqual(string, "django.core.validators.RegexValidator('^[0-9]+$', flags=re.RegexFlag(16))") - else: - self.assertEqual(string, "django.core.validators.RegexValidator('^[0-9]+$', flags=16)") + self.assertEqual(string, "django.core.validators.RegexValidator('^[0-9]+$', flags=re.RegexFlag(16))") self.serialize_round_trip(validator) # Test message and code diff --git a/tests/model_inheritance/tests.py b/tests/model_inheritance/tests.py index 5f89b4aa83a..5eef0f5bfa1 100644 --- a/tests/model_inheritance/tests.py +++ b/tests/model_inheritance/tests.py @@ -1,11 +1,9 @@ -import unittest from operator import attrgetter 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 PY36 from .models import ( Base, Chef, CommonInfo, GrandChild, GrandParent, ItalianRestaurant, @@ -176,7 +174,6 @@ class ModelInheritanceTests(TestCase): self.assertIs(C._meta.parents[A], C._meta.get_field('a')) - @unittest.skipUnless(PY36, 'init_subclass is new in Python 3.6') @isolate_apps('model_inheritance') def test_init_subclass(self): saved_kwargs = {} @@ -193,7 +190,6 @@ class ModelInheritanceTests(TestCase): self.assertEqual(saved_kwargs, kwargs) - @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') @isolate_apps('model_inheritance') def test_set_name(self): class ClassAttr: diff --git a/tests/runtests.py b/tests/runtests.py index 871b6c202dd..0861ec87984 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -234,7 +234,7 @@ def teardown(state): # Discard the multiprocessing.util finalizer that tries to remove a # temporary directory that's already removed by this script's # atexit.register(shutil.rmtree, TMPDIR) handler. Prevents - # FileNotFoundError at the end of a test run on Python 3.6+ (#27890). + # FileNotFoundError at the end of a test run (#27890). from multiprocessing.util import _finalizer_registry _finalizer_registry.pop((-100, 0), None) diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index af3416aeaa1..2ac2f8cc844 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -228,7 +228,6 @@ class AtomicInsideTransactionTests(AtomicTests): self.atomic.__exit__(*sys.exc_info()) -@skipIfDBFeature('autocommits_when_autocommit_is_off') class AtomicWithoutAutocommitTests(AtomicTests): """All basic tests for atomic should also pass when autocommit is turned off.""" @@ -480,7 +479,6 @@ class AtomicMiscTests(TransactionTestCase): Reporter.objects.create() -@skipIfDBFeature('autocommits_when_autocommit_is_off') class NonAutocommitTests(TransactionTestCase): available_apps = [] diff --git a/tests/utils_tests/test_functional.py b/tests/utils_tests/test_functional.py index 8d26a906b9d..ab649b79837 100644 --- a/tests/utils_tests/test_functional.py +++ b/tests/utils_tests/test_functional.py @@ -1,8 +1,5 @@ -import unittest - from django.test import SimpleTestCase from django.utils.functional import cached_property, lazy -from django.utils.version import PY36 class FunctionalTests(SimpleTestCase): @@ -104,7 +101,6 @@ class FunctionalTests(SimpleTestCase): for attr in attrs: self.assertCachedPropertyWorks(attr, Class) - @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') def test_cached_property_auto_name(self): """ cached_property caches its value and behaves like a property @@ -132,7 +128,6 @@ class FunctionalTests(SimpleTestCase): obj.other2 self.assertFalse(hasattr(obj, 'different_name')) - @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') def test_cached_property_reuse_different_names(self): """Disallow this case because the decorated function wouldn't be cached.""" with self.assertRaises(RuntimeError) as ctx: @@ -151,7 +146,6 @@ class FunctionalTests(SimpleTestCase): )) ) - @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') def test_cached_property_reuse_same_name(self): """ Reusing a cached_property on different classes under the same name is @@ -177,7 +171,6 @@ class FunctionalTests(SimpleTestCase): self.assertEqual(b.cp, 2) self.assertEqual(a.cp, 1) - @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') def test_cached_property_set_name_not_called(self): cp = cached_property(lambda s: None) @@ -189,29 +182,6 @@ class FunctionalTests(SimpleTestCase): with self.assertRaisesMessage(TypeError, msg): Foo().cp - @unittest.skipIf(PY36, '__set_name__ is new in Python 3.6') - def test_cached_property_mangled_error(self): - msg = ( - 'cached_property does not work with mangled methods on ' - 'Python < 3.6 without the appropriate `name` argument.' - ) - with self.assertRaisesMessage(ValueError, msg): - @cached_property - def __value(self): - pass - with self.assertRaisesMessage(ValueError, msg): - def func(self): - pass - cached_property(func, name='__value') - - @unittest.skipIf(PY36, '__set_name__ is new in Python 3.6') - def test_cached_property_name_validation(self): - msg = "%s can't be used as the name of a cached_property." - with self.assertRaisesMessage(ValueError, msg % "''"): - cached_property(lambda x: None) - with self.assertRaisesMessage(ValueError, msg % 42): - cached_property(str, name=42) - def test_lazy_equality(self): """ == and != work correctly for Promises. diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index db23d10d321..1ff1220803c 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -17,7 +17,6 @@ from django.test.utils import LoggingCaptureMixin from django.urls import path, reverse from django.utils.functional import SimpleLazyObject from django.utils.safestring import mark_safe -from django.utils.version import PY36 from django.views.debug import ( CLEANSED_SUBSTITUTE, CallableSettingWrapper, ExceptionReporter, cleanse_setting, technical_500_response, @@ -515,7 +514,7 @@ class ExceptionReporterTests(SimpleTestCase): exc_type, exc_value, tb = sys.exc_info() reporter = ExceptionReporter(request, exc_type, exc_value, tb) html = reporter.get_traceback_html() - self.assertInHTML('

%sError at /test_view/

' % ('ModuleNotFound' if PY36 else 'Import'), html) + self.assertInHTML('

ModuleNotFoundError at /test_view/

', html) def test_ignore_traceback_evaluation_exceptions(self): """ diff --git a/tox.ini b/tox.ini index 2f03d810c87..09a7ac82705 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ passenv = DJANGO_SETTINGS_MODULE PYTHONPATH HOME DISPLAY setenv = PYTHONDONTWRITEBYTECODE=1 deps = - py{3,35,36,37}: -rtests/requirements/py3.txt + py{3,36,37}: -rtests/requirements/py3.txt postgres: -rtests/requirements/postgres.txt mysql: -rtests/requirements/mysql.txt oracle: -rtests/requirements/oracle.txt