Repository: yola/yoconfigurator

Issue:
Make it Python 3.x compatible
We'll need it for our new project (which will be run under Python 3.x)

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 5a893c1..07aa865 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,8 @@
 language: python
 python:
-    - "2.6"
     - "2.7"
-install:
-    - pip install -r requirements.txt --use-mirrors
+    - "3.2"
+    - "3.3"
+    - "3.4"
+    - "3.5"
 script: python setup.py test
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 2f7e009..a3529c9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,12 @@
 Change Log
 ==========
 
+Dev
+-----
+
+* Update to work with Python 2.7, 3.2, 3.3, 3.4, 3.5
+* Drop support for Python <= 2.6
+
 0.4.6
 -----
 
diff --git a/requirements.txt b/requirements.txt
index 05bf2df..e69de29 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +0,0 @@
-# Only for tests on Python < 2.7:
-argparse
-unittest2
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2a9acf1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
index a7d8dbe..51b1d62 100755
--- a/setup.py
+++ b/setup.py
@@ -1,12 +1,7 @@
 #!/usr/bin/env python
 
-import sys
-
 from setuptools import setup, find_packages
 
-install_requires = []
-if sys.version_info < (2, 7):
-    install_requires.append('argparse')
 
 setup(
     name='yoconfigurator',
@@ -18,6 +13,21 @@ setup(
     version='0.4.6',
     packages=find_packages(),
     scripts=['bin/configurator.py'],
-    install_requires=install_requires,
     test_suite='yoconfigurator.tests',
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Environment :: Console',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Natural Language :: English',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.2',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Topic :: Utilities',
+    ]
 )
diff --git a/yoconfigurator/base.py b/yoconfigurator/base.py
index 546ae41..f339b95 100644
--- a/yoconfigurator/base.py
+++ b/yoconfigurator/base.py
@@ -1,15 +1,26 @@
-import imp
 import json
 import os
+import types
 import sys
 
 from yoconfigurator.dicts import DotDict, MissingValue
 
+try:
+    # SourceFileLoader added in Python 3.3
+    from importlib.machinery import SourceFileLoader
+except ImportError:
+    # imp.load_source deprecated in Python 3.3
+    from imp import load_source
+    _load_module = load_source
+else:
+    def _load_module(module_name, file_name):
+         return SourceFileLoader(module_name, file_name).load_module()
+
 
 class DetectMissingEncoder(json.JSONEncoder):
     def default(self, obj):
         if isinstance(obj, MissingValue):
-            raise ValueError("Missing Value found in config: %s" % obj.name)
+            raise ValueError('Missing Value found in config: %s' % obj.name)
         return super(DetectMissingEncoder, self).default(obj)
 
 
@@ -32,7 +43,7 @@ def get_config_module(config_pathname):
     """Imports the config file to yoconfigurator.configs.<config_basename>."""
     configs_mod = 'yoconfigurator.configs'
     if configs_mod not in sys.modules:
-        sys.modules[configs_mod] = imp.new_module(configs_mod)
-    module_name = os.path.basename(config_pathname).rsplit('.', 1)[-1]
+        sys.modules[configs_mod] = types.ModuleType(configs_mod)
+    module_name = os.path.basename(config_pathname).rsplit('.', 1)[0]
     module_name = configs_mod + '.' + module_name
-    return imp.load_source(module_name, config_pathname)
+    return _load_module(module_name, config_pathname)
diff --git a/yoconfigurator/credentials.py b/yoconfigurator/credentials.py
index 8ef29f9..e703222 100644
--- a/yoconfigurator/credentials.py
+++ b/yoconfigurator/credentials.py
@@ -4,5 +4,5 @@ import hashlib
 def seeded_auth_token(client, service, seed):
     """Return an auth token based on the client+service+seed tuple."""
     hash_func = hashlib.md5()
-    hash_func.update(','.join((client, service, seed)))
+    hash_func.update(b','.join((client, service, seed)))
     return hash_func.hexdigest()
diff --git a/yoconfigurator/dicts.py b/yoconfigurator/dicts.py
index 9a3e8c2..d3cfbaf 100644
--- a/yoconfigurator/dicts.py
+++ b/yoconfigurator/dicts.py
@@ -15,7 +15,7 @@ class DotDict(dict):
 
     def __init__(self, *args, **kwargs):
         super(DotDict, self).__init__(*args, **kwargs)
-        for key, value in self.iteritems():
+        for key, value in self.items():
             self[key] = self._convert_item(value)
 
     def __setitem__(self, dottedkey, value):
@@ -90,10 +90,10 @@ class MissingValue(object):
         self.name = name
 
     def __getattr__(self, k):
-        raise AttributeError("No value provided for %s" % self.name)
+        raise AttributeError('No value provided for %s' % self.name)
 
     def get(self, k, default=None):
-        raise KeyError("No value provided for %s" % self.name)
+        raise KeyError('No value provided for %s' % self.name)
 
     __getitem__ = get
 
@@ -109,7 +109,7 @@ def merge_dicts(d1, d2, _path=None):
     if _path is None:
         _path = ()
     if isinstance(d1, dict) and isinstance(d2, dict):
-        for k, v in d2.iteritems():
+        for k, v in d2.items():
             if isinstance(v, MissingValue) and v.name is None:
                 v.name = '.'.join(_path + (k,))
 


Test patch:
diff --git a/yoconfigurator/tests/__init__.py b/yoconfigurator/tests/__init__.py
index 68116d5..e69de29 100644
--- a/yoconfigurator/tests/__init__.py
+++ b/yoconfigurator/tests/__init__.py
@@ -1,6 +0,0 @@
-import sys
-
-if sys.version_info >= (2, 7):
-    import unittest
-else:
-    import unittest2 as unittest
diff --git a/yoconfigurator/tests/samples/public-config/deploy/configuration/public-data.py b/yoconfigurator/tests/samples/public-config/deploy/configuration/public-data.py
index ac49920..7f14363 100644
--- a/yoconfigurator/tests/samples/public-config/deploy/configuration/public-data.py
+++ b/yoconfigurator/tests/samples/public-config/deploy/configuration/public-data.py
@@ -6,8 +6,8 @@ from yoconfigurator.dicts import filter_dict
 def filter(config):
     """The subset of configuration keys to be made public."""
     keys = [
-        "myapp.hello",
-        "myapp.some.deeply.nested.value",
-        "myapp.oz"
+        'myapp.hello',
+        'myapp.some.deeply.nested.value',
+        'myapp.oz'
     ]
     return filter_dict(config, keys)
diff --git a/yoconfigurator/tests/test_base.py b/yoconfigurator/tests/test_base.py
index 6a33f03..4aff504 100644
--- a/yoconfigurator/tests/test_base.py
+++ b/yoconfigurator/tests/test_base.py
@@ -1,13 +1,12 @@
 import json
 import os
 import shutil
+import unittest
 from tempfile import mkdtemp
 
 from ..base import DetectMissingEncoder, read_config, write_config
 from ..dicts import merge_dicts, MissingValue
 
-from . import unittest
-
 
 class TestDetectMissingEncoder(unittest.TestCase):
     """Not part of the public API"""
diff --git a/yoconfigurator/tests/test_configurator.py b/yoconfigurator/tests/test_configurator.py
index b99b63b..92f3d61 100644
--- a/yoconfigurator/tests/test_configurator.py
+++ b/yoconfigurator/tests/test_configurator.py
@@ -2,8 +2,7 @@ import json
 import os
 import subprocess
 import sys
-
-from yoconfigurator.tests import unittest
+import unittest
 
 
 class TestConfigurator(unittest.TestCase):
@@ -29,7 +28,7 @@ class TestConfigurator(unittest.TestCase):
             stderr=subprocess.PIPE, env=env)
         out, err = p.communicate()
         self.assertEqual(p.wait(), 0)
-        self.assertEqual(err, '')
+        self.assertEqual(err, b'')
 
     def tearDown(self):
         os.remove(self.pub_conf)
@@ -43,24 +42,24 @@ class TestConfigurator(unittest.TestCase):
 
     def test_creates_a_config_that_looks_as_expected(self):
         expected = {
-            "yoconfigurator": {
-                "app": "myapp"
+            'yoconfigurator': {
+                'app': 'myapp'
             },
-            "myapp": {
-                "secret": "sauce",
-                "some": {
-                    "deeply": {
-                        "nested": {
-                            "value": "Stefano likes beer"
+            'myapp': {
+                'secret': 'sauce',
+                'some': {
+                    'deeply': {
+                        'nested': {
+                            'value': 'Stefano likes beer'
                         }
                     }
                 },
-                "hello": "world",
-                "oz": {
-                    "bears": True,
-                    "tigers": True,
-                    "lions": True,
-                    "zebras": False
+                'hello': 'world',
+                'oz': {
+                    'bears': True,
+                    'tigers': True,
+                    'lions': True,
+                    'zebras': False
                 }
             }
         }
diff --git a/yoconfigurator/tests/test_credentials.py b/yoconfigurator/tests/test_credentials.py
index 01c8438..4daa915 100644
--- a/yoconfigurator/tests/test_credentials.py
+++ b/yoconfigurator/tests/test_credentials.py
@@ -1,9 +1,9 @@
-from . import unittest
+import unittest
 
 from ..credentials import seeded_auth_token
 
 
 class TestSeededAuthToken(unittest.TestCase):
     def test_seeded_auth(self):
-        self.assertEqual(seeded_auth_token('foo', 'bar', 'baz'),
+        self.assertEqual(seeded_auth_token(b'foo', b'bar', b'baz'),
                          '5a9350198f854de4b2ab56f187f87707')
diff --git a/yoconfigurator/tests/test_dicts.py b/yoconfigurator/tests/test_dicts.py
index 626fa77..9682991 100644
--- a/yoconfigurator/tests/test_dicts.py
+++ b/yoconfigurator/tests/test_dicts.py
@@ -1,20 +1,14 @@
 import copy
+import unittest
 
 from yoconfigurator.dicts import (DeletedValue, DotDict, MissingValue,
                                   filter_dict, merge_dicts)
-from yoconfigurator.tests import unittest
 
 
 class DotDictTestCase(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        # Python < 2.7 compatibility:
-        if not hasattr(cls, 'assertIsInstance'):
-            cls.assertIsInstance = lambda self, a, b: self.assertTrue(
-                isinstance(a, b))
 
     def test_create(self):
-        'ensure that we support all dict creation methods'
+        """Ensure that we support all dict creation methods"""
         by_attr = DotDict(one=1, two=2)
         by_dict = DotDict({'one': 1, 'two': 2})
         by_list = DotDict([['one', 1], ['two', 2]])
@@ -22,30 +16,30 @@ class DotDictTestCase(unittest.TestCase):
         self.assertEqual(by_attr, by_list)
 
     def test_create_tree(self):
-        'ensure that nested dicts are converted to DotDicts'
+        """Ensure that nested dicts are converted to DotDicts"""
         tree = DotDict({'foo': {'bar': True}})
         self.assertIsInstance(tree['foo'], DotDict)
 
     def test_list_of_dicts(self):
-        'ensure that nested dicts insied lists are converted to DotDicts'
+        """Ensure that nested dicts insied lists are converted to DotDicts"""
         tree = DotDict({'foo': [{'bar': True}]})
         self.assertIsInstance(tree['foo'][0], DotDict)
 
     def test_mutablity(self):
-        'ensure that the whole tree is mutable'
+        """Ensure that the whole tree is mutable"""
         tree = DotDict({'foo': {'bar': True}})
         self.assertTrue(tree.foo.bar)
         tree.foo.bar = False
         self.assertFalse(tree.foo.bar)
 
     def test_setdefault(self):
-        'ensure that the setdefault works'
+        """Ensure that the setdefault works"""
         tree = DotDict({'foo': 'bar'})
         tree.setdefault('baz', {})
         self.assertIsInstance(tree.baz, DotDict)
 
     def test_update(self):
-        'ensure that update works'
+        """Ensure that update works"""
         tree = DotDict({'foo': 'bar'})
         tree.update({'foo': {}})
         self.assertIsInstance(tree.foo, DotDict)
@@ -55,17 +49,17 @@ class DotDictTestCase(unittest.TestCase):
         self.assertIsInstance(tree.baz, DotDict)
 
     def test_deepcopy(self):
-        'ensure that DotDict can be deepcopied'
+        """Ensure that DotDict can be deepcopied"""
         tree = DotDict({'foo': 'bar'})
         self.assertEqual(tree, copy.deepcopy(tree))
 
     def test_get_dotted(self):
-        'ensure that DotDict can get values using a dotted key'
+        """Ensure that DotDict can get values using a dotted key"""
         tree = DotDict({'foo': {'bar': {'baz': 'huzzah'}}})
         self.assertEqual(tree['foo.bar.baz'], 'huzzah')
 
     def test_set_dotted(self):
-        'ensure that DotDict can set values using a dotted key'
+        """Ensure that DotDict can set values using a dotted key"""
         tree = DotDict()
         tree['foo.bar.baz'] = 'huzzah'
         self.assertEqual(tree['foo.bar.baz'], 'huzzah')
@@ -84,7 +78,7 @@ class TestMissingValue(unittest.TestCase):
 
 class TestMergeDicts(unittest.TestCase):
     def test_merge(self):
-        'ensure that the new entries in B are merged into A'
+        """Ensure that the new entries in B are merged into A"""
         a = DotDict(a=1, b=1)
         b = DotDict(c=1)
         c = merge_dicts(a, b)
@@ -93,14 +87,14 @@ class TestMergeDicts(unittest.TestCase):
         self.assertEqual(c.c, 1)
 
     def test_filter(self):
-        'ensure that the subset of A is filtered out using keys'
+        """Ensure that the subset of A is filtered out using keys"""
         a = DotDict(a=1, b=1)
         keys = ['a']
         b = filter_dict(a, keys)
         self.assertEqual(b, {'a': 1})
 
     def test_replacement(self):
-        'ensure that the new entries in B replace equivalents in A'
+        """Ensure that the new entries in B replace equivalents in A"""
         a = DotDict(a=1, b=1)
         b = DotDict(b=2)
         c = merge_dicts(a, b)
@@ -108,7 +102,7 @@ class TestMergeDicts(unittest.TestCase):
         self.assertEqual(c.b, 2)
 
     def test_sub_merge(self):
-        'ensure that a subtree from B is merged with the same subtree in A'
+        """Ensure that a subtree from B is merged with the same subtree in A"""
         a = DotDict(a=1, sub={'c': 1})
         b = DotDict(b=2, sub={'d': 2})
         c = merge_dicts(a, b)
@@ -118,7 +112,7 @@ class TestMergeDicts(unittest.TestCase):
         self.assertEqual(c.sub.d, 2)
 
     def test_sub_replacement(self):
-        'ensure that a subtree from B is merged with the same subtree in A'
+        """Ensure that a subtree from B is merged with the same subtree in A"""
         a = DotDict(a=1, sub={'c': 1})
         b = DotDict(b=2, sub={'c': 2})
         c = merge_dicts(a, b)
@@ -127,7 +121,7 @@ class TestMergeDicts(unittest.TestCase):
         self.assertEqual(c.sub.c, 2)
 
     def test_replace_missing_with_dict(self):
-        'ensure that a subtree from B replaces a MissingValue in A'
+        """Ensure that a subtree from B replaces a MissingValue in A"""
         a = DotDict(a=1, sub=MissingValue('sub'))
         b = DotDict(b=2, sub={'c': 2})
         c = merge_dicts(a, b)
@@ -136,21 +130,21 @@ class TestMergeDicts(unittest.TestCase):
         self.assertEqual(c.sub.c, 2)
 
     def test_unnamed_missing_value(self):
-        'ensure that missing values get a name assigned'
+        """Ensure that missing values get a name assigned"""
         a = DotDict()
         b = DotDict(foo=MissingValue())
         c = merge_dicts(a, b)
         self.assertEqual(c.foo.name, 'foo')
 
     def test_unnamed_missing_value_in_new_tree(self):
-        'ensure that missing values in new sub-trees get a name assigned'
+        """Ensure that missing values in new sub-trees get a name assigned"""
         a = DotDict()
         b = DotDict(foo={'bar': MissingValue()})
         c = merge_dicts(a, b)
         self.assertEqual(c.foo.bar.name, 'foo.bar')
 
     def test_merge_lists(self):
-        'ensure that leaf lists are merged'
+        """Ensure that leaf lists are merged"""
         a = DotDict(a=1, sub=[1, 2])
         b = DotDict(b=2, sub=[3, 4])
         c = merge_dicts(a, b)
@@ -159,7 +153,7 @@ class TestMergeDicts(unittest.TestCase):
         self.assertEqual(c.sub, [1, 2, 3, 4])
 
     def test_merge_incompatible(self):
-        'ensure that the merged items are of the same types'
+        """Ensure that the merged items are of the same types"""
         a = DotDict(foo=42)
         b = DotDict(foo='42')
         self.assertRaises(TypeError, merge_dicts, a, b)
@@ -167,14 +161,14 @@ class TestMergeDicts(unittest.TestCase):
         self.assertRaises(TypeError, merge_dicts, a, b)
 
     def test_replace_none(self):
-        'ensure that None can be replaced with another type'
+        """Ensure that None can be replaced with another type"""
         a = DotDict(foo=None)
         b = DotDict(foo='foo')
         c = merge_dicts(a, b)
         self.assertEqual(c, {'foo': 'foo'})
 
     def test_deltedvalue(self):
-        'ensure that deletedvalue deletes values'
+        """Ensure that deletedvalue deletes values"""
         a = DotDict(foo=42)
         b = DotDict(foo=DeletedValue())
         c = merge_dicts(a, b)
diff --git a/yoconfigurator/tests/test_script.py b/yoconfigurator/tests/test_script.py
index 7207384..d7f61c3 100644
--- a/yoconfigurator/tests/test_script.py
+++ b/yoconfigurator/tests/test_script.py
@@ -1,8 +1,7 @@
 import os
 import subprocess
 import sys
-
-from . import unittest
+import unittest
 
 
 class TestScript(unittest.TestCase):
@@ -18,4 +17,4 @@ class TestScript(unittest.TestCase):
                              stderr=subprocess.PIPE, env=env)
         out, err = p.communicate()
         self.assertEqual(p.wait(), 0)
-        self.assertEqual(err, '')
+        self.assertEqual(err, b'')
diff --git a/yoconfigurator/tests/test_smush.py b/yoconfigurator/tests/test_smush.py
index 42af2d1..2bb9028 100644
--- a/yoconfigurator/tests/test_smush.py
+++ b/yoconfigurator/tests/test_smush.py
@@ -1,14 +1,13 @@
 import json
 import os
 import shutil
+import unittest
 from tempfile import mkdtemp
 
 from ..dicts import MissingValue
 from ..smush import (config_sources, available_sources, smush_config,
                      LenientJSONEncoder)
 
-from . import unittest
-
 
 class TestLenientJSONEncoder(unittest.TestCase):