Repository: ckan/ckan

Issue:
Drop Python 2.6 support
It was announced to the mailing list by @amercader in January that support for python 2.6 was going to be dropped with ckan 2.6 (what a wonderful coincidence).

https://lists.okfn.org/pipermail/ckan-dev/2016-January/009608.html

> Starting from the next CKAN 2.6 version, the minimum Python version
> required for CKAN will be Python 2.7 (we recommend using the latest
> Python patch release).

There still are some backports and workaraounds for 2.6 in the code and the documentation needs upgrading. 
## Grep for `2.6`
### Python files

```
ckan/lib/util.py
13:# subprocess.check_output() isn't in Python 2.6.
15:# Python 2.6.

ckan/model/extension.py
17:        "Replaces stdlib operator.methodcaller in python <2.6"

ckan/tests/helpers.py
36:    # Python 2.6 doesn't have these, so define them here

ckanext/datapusher/tests/test.py
87:        # Httpretty crashes with Solr on Python 2.6,

ckanext/resourceproxy/tests/test_proxy.py
63:        # Httpretty crashes with Solr on Python 2.6,
```
### Documentation:

```
doc/contributing/python.rst
77:We use `the Python standard library's logging module <http://docs.python.org/2.6/library/logging.html>`_

doc/contributing/string-i18n.rst
339:  Worse (doesn't work in Python 2.6):

doc/maintaining/installing/install-from-source.rst
36:Python                 `The Python programming language, v2.6 or 2.7 <http://www.python.org/getit/>`_
```

I'll prepare a PR to remove all of those things.



Discussion:

Resolving patch:
diff --git a/ckan/lib/util.py b/ckan/lib/util.py
deleted file mode 100644
--- a/ckan/lib/util.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# encoding: utf-8
-
-'''Shared utility functions for any Python code to use.
-
-Unlike :py:mod:`ckan.lib.helpers`, the functions in this module are not
-available to templates.
-
-'''
-import subprocess
-
-
-# We implement our own check_output() function because
-# subprocess.check_output() isn't in Python 2.6.
-# This code is copy-pasted from Python 2.7 and adapted to make it work with
-# Python 2.6.
-# http://hg.python.org/cpython/file/d37f963394aa/Lib/subprocess.py#l544
-def check_output(*popenargs, **kwargs):
-    r"""Run command with arguments and return its output as a byte string.
-
-    If the exit code was non-zero it raises a CalledProcessError.  The
-    CalledProcessError object will have the return code in the returncode
-    attribute and output in the output attribute.
-
-    The arguments are the same as for the Popen constructor.  Example:
-
-    >>> check_output(["ls", "-l", "/dev/null"])
-    'crw-rw-rw- 1 root root 1, 3 Oct 18  2007 /dev/null\n'
-
-    The stdout argument is not allowed as it is used internally.
-    To capture standard error in the result, use stderr=STDOUT.
-
-    >>> check_output(["/bin/sh", "-c",
-    ...               "ls -l non_existent_file ; exit 0"],
-    ...              stderr=STDOUT)
-    'ls: non_existent_file: No such file or directory\n'
-    """
-    if 'stdout' in kwargs:
-        raise ValueError('stdout argument not allowed, it will be overridden.')
-    process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
-    output, unused_err = process.communicate()
-    retcode = process.poll()
-    if retcode:
-        cmd = kwargs.get("args")
-        if cmd is None:
-            cmd = popenargs[0]
-        raise subprocess.CalledProcessError(retcode, cmd)
-    return output
diff --git a/ckan/model/extension.py b/ckan/model/extension.py
--- a/ckan/model/extension.py
+++ b/ckan/model/extension.py
@@ -4,23 +4,17 @@
 Provides bridges between the model and plugin PluginImplementationss
 """
 import logging
+from operator import methodcaller
 
 from sqlalchemy.orm.interfaces import MapperExtension
 from sqlalchemy.orm.session import SessionExtension
 
 import ckan.plugins as plugins
 
-try:
-    from operator import methodcaller
-except ImportError:
-    def methodcaller(name, *args, **kwargs):
-        "Replaces stdlib operator.methodcaller in python <2.6"
-        def caller(obj):
-            return getattr(obj, name)(*args, **kwargs)
-        return caller
 
 log = logging.getLogger(__name__)
 
+
 class ObserverNotifier(object):
     """
     Mixin for hooking into SQLAlchemy
@@ -93,7 +87,6 @@ def notify_observers(self, func):
         for observer in plugins.PluginImplementations(plugins.ISession):
             func(observer)
 
-
     def after_begin(self, session, transaction, connection):
         return self.notify_observers(
             methodcaller('after_begin', session, transaction, connection)
@@ -123,4 +116,3 @@ def after_rollback(self, session):
         return self.notify_observers(
             methodcaller('after_rollback', session)
         )
-
diff --git a/doc/conf.py b/doc/conf.py
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -18,8 +18,6 @@
 import os
 import subprocess
 
-import ckan.lib.util as util
-
 # If your extensions (or modules documented by autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -126,7 +124,7 @@ def latest_release_tag():
     This requires git to be installed.
 
     '''
-    git_tags = util.check_output(
+    git_tags = subprocess.check_output(
         ['git', 'tag', '-l'], stderr=subprocess.STDOUT).split()
 
     # FIXME: We could do more careful pattern matching against ckan-X.Y.Z here.


Test patch:
diff --git a/ckan/tests/controllers/test_api.py b/ckan/tests/controllers/test_api.py
--- a/ckan/tests/controllers/test_api.py
+++ b/ckan/tests/controllers/test_api.py
@@ -7,10 +7,9 @@
 import json
 
 from routes import url_for
-from nose.tools import assert_equal
+from nose.tools import assert_equal, assert_in
 
 import ckan.tests.helpers as helpers
-from ckan.tests.helpers import assert_in
 from ckan.tests import factories
 from ckan import model
 
diff --git a/ckan/tests/controllers/test_group.py b/ckan/tests/controllers/test_group.py
--- a/ckan/tests/controllers/test_group.py
+++ b/ckan/tests/controllers/test_group.py
@@ -1,7 +1,7 @@
 # encoding: utf-8
 
 from bs4 import BeautifulSoup
-from nose.tools import assert_equal, assert_true
+from nose.tools import assert_equal, assert_true, assert_in
 
 from routes import url_for
 
@@ -9,7 +9,6 @@
 import ckan.model as model
 from ckan.tests import factories
 
-assert_in = helpers.assert_in
 webtest_submit = helpers.webtest_submit
 submit_and_follow = helpers.submit_and_follow
 
diff --git a/ckan/tests/controllers/test_organization.py b/ckan/tests/controllers/test_organization.py
--- a/ckan/tests/controllers/test_organization.py
+++ b/ckan/tests/controllers/test_organization.py
@@ -1,12 +1,12 @@
 # encoding: utf-8
 
 from bs4 import BeautifulSoup
-from nose.tools import assert_equal, assert_true
+from nose.tools import assert_equal, assert_true, assert_in
 from routes import url_for
 from mock import patch
 
 from ckan.tests import factories, helpers
-from ckan.tests.helpers import webtest_submit, submit_and_follow, assert_in
+from ckan.tests.helpers import webtest_submit, submit_and_follow
 
 
 class TestOrganizationNew(helpers.FunctionalTestBase):
diff --git a/ckan/tests/controllers/test_package.py b/ckan/tests/controllers/test_package.py
--- a/ckan/tests/controllers/test_package.py
+++ b/ckan/tests/controllers/test_package.py
@@ -6,6 +6,7 @@
     assert_not_equal,
     assert_raises,
     assert_true,
+    assert_in
 )
 
 from mock import patch, MagicMock
@@ -17,10 +18,8 @@
 
 import ckan.tests.helpers as helpers
 import ckan.tests.factories as factories
-from ckan.tests.helpers import assert_in
 
 
-assert_in = helpers.assert_in
 webtest_submit = helpers.webtest_submit
 submit_and_follow = helpers.submit_and_follow
 
diff --git a/ckan/tests/controllers/test_tags.py b/ckan/tests/controllers/test_tags.py
--- a/ckan/tests/controllers/test_tags.py
+++ b/ckan/tests/controllers/test_tags.py
@@ -3,7 +3,7 @@
 import math
 import string
 
-from nose.tools import assert_equal, assert_true, assert_false
+from nose.tools import assert_equal, assert_true, assert_false, assert_in
 from bs4 import BeautifulSoup
 
 from routes import url_for
@@ -11,7 +11,6 @@
 import ckan.tests.helpers as helpers
 from ckan.tests import factories
 
-assert_in = helpers.assert_in
 webtest_submit = helpers.webtest_submit
 submit_and_follow = helpers.submit_and_follow
 
diff --git a/ckan/tests/helpers.py b/ckan/tests/helpers.py
--- a/ckan/tests/helpers.py
+++ b/ckan/tests/helpers.py
@@ -22,6 +22,7 @@
 import webtest
 from pylons import config
 import nose.tools
+from nose.tools import assert_in, assert_not_in
 import mock
 
 import ckan.lib.search as search
@@ -30,17 +31,6 @@
 import ckan.logic as logic
 
 
-try:
-    from nose.tools import assert_in, assert_not_in
-except ImportError:
-    # Python 2.6 doesn't have these, so define them here
-    def assert_in(a, b, msg=None):
-        assert a in b, msg or '%r was not in %r' % (a, b)
-
-    def assert_not_in(a, b, msg=None):
-        assert a not in b, msg or '%r was in %r' % (a, b)
-
-
 def reset_db():
     '''Reset CKAN's database.
 
diff --git a/ckan/tests/legacy/__init__.py b/ckan/tests/legacy/__init__.py
--- a/ckan/tests/legacy/__init__.py
+++ b/ckan/tests/legacy/__init__.py
@@ -355,15 +355,6 @@ def skip_test(*args):
 def clear_flash(res=None):
     messages = h._flash.pop_messages()
 
-try:
-    from nose.tools import assert_in, assert_not_in
-except ImportError:
-    def assert_in(a, b, msg=None):
-        assert a in b, msg or '%r was not in %r' % (a, b)
-    def assert_not_in(a, b, msg=None):
-        assert a not in b, msg or '%r was in %r' % (a, b)
-
-
 class StatusCodes:
     STATUS_200_OK = 200
     STATUS_201_CREATED = 201
diff --git a/ckan/tests/legacy/functional/api/test_api.py b/ckan/tests/legacy/functional/api/test_api.py
--- a/ckan/tests/legacy/functional/api/test_api.py
+++ b/ckan/tests/legacy/functional/api/test_api.py
@@ -2,10 +2,10 @@
 
 import json
 
+from nose.tools import assert_in
+
 from ckan.tests.legacy.functional.api.base import *
 
-import ckan.tests.legacy
-assert_in = ckan.tests.legacy.assert_in
 
 class ApiTestCase(ApiTestCase, ControllerTestCase):
 
diff --git a/ckan/tests/legacy/lib/test_dictization.py b/ckan/tests/legacy/lib/test_dictization.py
--- a/ckan/tests/legacy/lib/test_dictization.py
+++ b/ckan/tests/legacy/lib/test_dictization.py
@@ -1,10 +1,10 @@
 # encoding: utf-8
 
-from ckan.tests.legacy import assert_equal, assert_not_in, assert_in
+from nose.tools import assert_equal, assert_not_in, assert_in
 from pprint import pprint, pformat
 from difflib import unified_diff
-import ckan.lib.search as search
 
+import ckan.lib.search as search
 from ckan.lib.create_test_data import CreateTestData
 from ckan import model
 from ckan.lib.dictization import (table_dictize,
diff --git a/ckan/tests/legacy/models/test_group.py b/ckan/tests/legacy/models/test_group.py
--- a/ckan/tests/legacy/models/test_group.py
+++ b/ckan/tests/legacy/models/test_group.py
@@ -1,9 +1,12 @@
 # encoding: utf-8
 
-from ckan.tests.legacy import assert_equal, assert_in, assert_not_in, CreateTestData
+from nose.tools import assert_in, assert_not_in, assert_equal
+
+from ckan.tests.legacy import CreateTestData
 
 import ckan.model as model
 
+
 class TestGroup(object):
 
     @classmethod
diff --git a/ckan/tests/legacy/test_coding_standards.py b/ckan/tests/legacy/test_coding_standards.py
--- a/ckan/tests/legacy/test_coding_standards.py
+++ b/ckan/tests/legacy/test_coding_standards.py
@@ -488,7 +488,6 @@ class TestPep8(object):
         'ckan/model/authz.py',
         'ckan/model/dashboard.py',
         'ckan/model/domain_object.py',
-        'ckan/model/extension.py',
         'ckan/model/follower.py',
         'ckan/model/group.py',
         'ckan/model/group_extra.py',
diff --git a/ckan/tests/lib/search/test_index.py b/ckan/tests/lib/search/test_index.py
--- a/ckan/tests/lib/search/test_index.py
+++ b/ckan/tests/lib/search/test_index.py
@@ -3,17 +3,13 @@
 import datetime
 import hashlib
 import json
-import nose.tools
 import nose
+from nose.tools import assert_equal, assert_in, assert_not_in
 
 from pylons import config
 import ckan.lib.search as search
 import ckan.tests.helpers as helpers
 
-assert_equal = nose.tools.assert_equal
-assert_in = helpers.assert_in
-assert_not_in = helpers.assert_not_in
-
 
 class TestSearchIndex(object):
 
diff --git a/ckan/tests/lib/test_mailer.py b/ckan/tests/lib/test_mailer.py
--- a/ckan/tests/lib/test_mailer.py
+++ b/ckan/tests/lib/test_mailer.py
@@ -1,6 +1,6 @@
 # encoding: utf-8
 
-from nose.tools import assert_equal, assert_raises
+from nose.tools import assert_equal, assert_raises, assert_in
 from pylons import config
 from email.mime.text import MIMEText
 from email.parser import Parser
@@ -16,8 +16,6 @@
 import ckan.tests.helpers as helpers
 import ckan.tests.factories as factories
 
-assert_in = helpers.assert_in
-
 
 class MailerBase(SmtpServerHarness):
 
diff --git a/ckan/tests/test_coding_standards.py b/ckan/tests/test_coding_standards.py
--- a/ckan/tests/test_coding_standards.py
+++ b/ckan/tests/test_coding_standards.py
@@ -15,8 +15,6 @@
 import re
 import subprocess
 
-import ckan.lib.util as util
-
 
 def test_building_the_docs():
     '''There should be no warnings or errors when building the Sphinx docs.
@@ -28,8 +26,12 @@ def test_building_the_docs():
 
     '''
     try:
-        output = util.check_output(
-            ['python', 'setup.py', 'build_sphinx', '--all-files', '--fresh-env'],
+        output = subprocess.check_output(
+            ['python',
+             'setup.py',
+             'build_sphinx',
+             '--all-files',
+             '--fresh-env'],
             stderr=subprocess.STDOUT)
     except subprocess.CalledProcessError as err:
         assert False, (
diff --git a/ckanext/datapusher/tests/test.py b/ckanext/datapusher/tests/test.py
--- a/ckanext/datapusher/tests/test.py
+++ b/ckanext/datapusher/tests/test.py
@@ -4,7 +4,6 @@
 import httpretty
 import httpretty.core
 import nose
-import sys
 import datetime
 
 import pylons
@@ -56,19 +55,12 @@ def __getattr__(self, attr):
 httpretty.core.fakesock.socket = HTTPrettyFix
 
 
-# avoid hanging tests https://github.com/gabrielfalcao/HTTPretty/issues/34
-if sys.version_info < (2, 7, 0):
-    import socket
-    socket.setdefaulttimeout(1)
-
-
 class TestDatastoreCreate(tests.WsgiAppCase):
     sysadmin_user = None
     normal_user = None
 
     @classmethod
     def setup_class(cls):
-
         wsgiapp = middleware.make_app(config['global_conf'], **config)
         cls.app = paste.fixture.TestApp(wsgiapp)
         if not tests.is_datastore_supported():
@@ -84,20 +76,11 @@ def setup_class(cls):
         set_url_type(
             model.Package.get('annakarenina').resources, cls.sysadmin_user)
 
-        # Httpretty crashes with Solr on Python 2.6,
-        # skip the tests
-        if (sys.version_info[0] == 2 and sys.version_info[1] == 6):
-            raise nose.SkipTest()
-
     @classmethod
     def teardown_class(cls):
         rebuild_all_dbs(cls.Session)
         p.unload('datastore')
         p.unload('datapusher')
-        # Reenable Solr indexing
-        if (sys.version_info[0] == 2 and sys.version_info[1] == 6
-                and not p.plugin_loaded('synchronous_search')):
-            p.load('synchronous_search')
 
     def test_create_ckan_resource_in_package(self):
         package = model.Package.get('annakarenina')
diff --git a/ckanext/datastore/tests/test_create.py b/ckanext/datastore/tests/test_create.py
--- a/ckanext/datastore/tests/test_create.py
+++ b/ckanext/datastore/tests/test_create.py
@@ -22,12 +22,6 @@
 from ckanext.datastore.tests.helpers import rebuild_all_dbs, set_url_type
 
 
-# avoid hanging tests https://github.com/gabrielfalcao/HTTPretty/issues/34
-if sys.version_info < (2, 7, 0):
-    import socket
-    socket.setdefaulttimeout(1)
-
-
 class TestDatastoreCreateNewTests(object):
     @classmethod
     def setup_class(cls):
diff --git a/ckanext/example_igroupform/tests/test_controllers.py b/ckanext/example_igroupform/tests/test_controllers.py
--- a/ckanext/example_igroupform/tests/test_controllers.py
+++ b/ckanext/example_igroupform/tests/test_controllers.py
@@ -1,6 +1,6 @@
 # encoding: utf-8
 
-from nose.tools import assert_equal
+from nose.tools import assert_equal, assert_in
 from routes import url_for
 
 import ckan.plugins as plugins
@@ -8,7 +8,6 @@
 import ckan.model as model
 from ckan.tests import factories
 
-assert_in = helpers.assert_in
 webtest_submit = helpers.webtest_submit
 submit_and_follow = helpers.submit_and_follow
 
diff --git a/ckanext/example_iuploader/test/test_plugin.py b/ckanext/example_iuploader/test/test_plugin.py
--- a/ckanext/example_iuploader/test/test_plugin.py
+++ b/ckanext/example_iuploader/test/test_plugin.py
@@ -6,6 +6,7 @@
 from mock import patch
 from nose.tools import (
     assert_equal,
+    assert_in,
     assert_is_instance
 )
 from pyfakefs import fake_filesystem
@@ -19,7 +20,6 @@
 import ckan.tests.helpers as helpers
 import ckanext.example_iuploader.plugin as plugin
 
-assert_in = helpers.assert_in
 webtest_submit = helpers.webtest_submit
 submit_and_follow = helpers.submit_and_follow
 
diff --git a/ckanext/example_theme/custom_emails/tests.py b/ckanext/example_theme/custom_emails/tests.py
--- a/ckanext/example_theme/custom_emails/tests.py
+++ b/ckanext/example_theme/custom_emails/tests.py
@@ -11,10 +11,7 @@
 from ckan.tests.lib.test_mailer import MailerBase
 import ckan.tests.helpers as helpers
 
-from nose.tools import assert_equal
-
-
-assert_in = helpers.assert_in
+from nose.tools import assert_equal, assert_in
 
 
 class TestExampleCustomEmailsPlugin(MailerBase):
diff --git a/ckanext/resourceproxy/tests/test_proxy.py b/ckanext/resourceproxy/tests/test_proxy.py
--- a/ckanext/resourceproxy/tests/test_proxy.py
+++ b/ckanext/resourceproxy/tests/test_proxy.py
@@ -1,6 +1,5 @@
 # encoding: utf-8
 
-import sys
 import requests
 import unittest
 import json
@@ -60,20 +59,12 @@ def setup_class(cls):
         wsgiapp = middleware.make_app(config['global_conf'], **config)
         cls.app = paste.fixture.TestApp(wsgiapp)
         create_test_data.CreateTestData.create()
-        # Httpretty crashes with Solr on Python 2.6,
-        # skip the tests
-        if (sys.version_info[0] == 2 and sys.version_info[1] == 6):
-            raise nose.SkipTest()
 
     @classmethod
     def teardown_class(cls):
         config.clear()
         config.update(cls._original_config)
         model.repo.rebuild_db()
-        # Reenable Solr indexing
-        if (sys.version_info[0] == 2 and sys.version_info[1] == 6
-                and not p.plugin_loaded('synchronous_search')):
-            p.load('synchronous_search')
 
     def setUp(self):
         self.url = 'http://www.ckan.org/static/example.json'