summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphaël Barrois <raphael.barrois@polytechnique.org>2013-03-21 23:38:40 +0100
committerRaphaël Barrois <raphael.barrois@polytechnique.org>2013-03-21 23:38:49 +0100
commit6aae60fb530f7608bc379a57a477e904a816544d (patch)
tree44a68fb691b7d9aeb0a560b54438f6f7180ce02d
parenta77278819e5f9637e8ff1954ec33ecbf753ac89a (diff)
downloadsemantic-version-6aae60fb530f7608bc379a57a477e904a816544d.tar.gz
Add Python3 support.
-rw-r--r--.travis.yml2
-rw-r--r--README3
-rwxr-xr-xsetup.py22
-rw-r--r--src/semantic_version/base.py70
-rw-r--r--src/semantic_version/compat.py18
-rw-r--r--src/semantic_version/django_fields.py19
-rw-r--r--tests/compat.py14
-rwxr-xr-xtests/test_base.py27
-rw-r--r--tests/test_django.py4
-rwxr-xr-xtests/test_parsing.py4
10 files changed, 150 insertions, 33 deletions
diff --git a/.travis.yml b/.travis.yml
index eb98332..9edcb6e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,6 +2,8 @@ language: python
python:
- "2.6"
- "2.7"
+ - "3.2"
+ - "3.3"
script: "python setup.py test"
install: "if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi"
notifications:
diff --git a/README b/README
index 4d9fcaa..37692e0 100644
--- a/README
+++ b/README
@@ -12,6 +12,9 @@ Handles the full 2.0.0-rc1 version of the SemVer scheme, and provides tools to d
The full doc is available on http://python-semanticversion.readthedocs.org/; simple usage is described below.
+The semantic_version library supports Python 2.6, 2.7, 3.2, 3.3.
+
+
Usage
=====
diff --git a/setup.py b/setup.py
index 58ab214..d24cdd0 100755
--- a/setup.py
+++ b/setup.py
@@ -54,15 +54,20 @@ class test(cmd.Command):
ex_path = sys.path
sys.path.insert(0, os.path.join(root_dir, 'src'))
loader = unittest.defaultTestLoader
+ suite = unittest.TestSuite()
- if self.test_suite != self.DEFAULT_TEST_SUITE:
- suite = loader.loadTestsFromName(self.test_suite)
+ if self.test_suite == self.DEFAULT_TEST_SUITE:
+ for test_module in loader.discover('.'):
+ suite.addTest(test_module)
else:
- suite = loader.discover(self.test_suite)
+ suite.addTest(loader.loadTestsFromName(self.test_suite))
- unittest.TextTestRunner(verbosity=verbosity).run(suite)
+ result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
sys.path = ex_path
+ if not result.wasSuccessful():
+ sys.exit(1)
+
setup(
name="semantic_version",
@@ -77,12 +82,19 @@ setup(
package_dir={'': 'src'},
packages=['semantic_version'],
classifiers=[
- "Development Status :: 3 - Alpha",
+ "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Topic :: Software Development :: Libraries :: Python Modules",
'Operating System :: OS Independent',
'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Topic :: Software Development :: Libraries :: Python Modules'
],
cmdclass={'test': test},
)
diff --git a/src/semantic_version/base.py b/src/semantic_version/base.py
index 2319b5f..f5153b2 100644
--- a/src/semantic_version/base.py
+++ b/src/semantic_version/base.py
@@ -2,10 +2,14 @@
# Copyright (c) 2012-2013 Raphaël Barrois
# This code is distributed under the two-clause BSD License.
+from __future__ import unicode_literals
import functools
import re
+
+from .compat import base_cmp
+
def _to_int(value):
try:
return int(value), True
@@ -21,7 +25,7 @@ def identifier_cmp(a, b):
if a_is_int and b_is_int:
# Numeric identifiers are compared as integers
- return cmp(a_cmp, b_cmp)
+ return base_cmp(a_cmp, b_cmp)
elif a_is_int:
# Numeric identifiers have lower precedence
return -1
@@ -29,7 +33,7 @@ def identifier_cmp(a, b):
return 1
else:
# Non-numeric identifers are compared lexicographically
- return cmp(a_cmp, b_cmp)
+ return base_cmp(a_cmp, b_cmp)
def identifier_list_cmp(a, b):
@@ -53,7 +57,7 @@ def identifier_list_cmp(a, b):
if cmp_res != 0:
return cmp_res
# alpha1.3 < alpha1.3.1
- return cmp(len(a), len(b))
+ return base_cmp(len(a), len(b))
class Version(object):
@@ -221,9 +225,6 @@ class Version(object):
', partial=True' if self.partial else '',
)
- def __hash__(self):
- return hash((self.major, self.minor, self.patch, self.prerelease, self.build))
-
@classmethod
def _comparison_functions(cls, partial=False):
"""Retrieve comparison methods to apply on version components.
@@ -281,17 +282,17 @@ class Version(object):
if partial:
return [
- cmp, # Major is still mandatory
- make_optional(cmp),
- make_optional(cmp),
+ base_cmp, # Major is still mandatory
+ make_optional(base_cmp),
+ make_optional(base_cmp),
make_optional(prerelease_cmp),
make_optional(build_cmp),
]
else:
return [
- cmp,
- cmp,
- cmp,
+ base_cmp,
+ base_cmp,
+ base_cmp,
prerelease_cmp,
build_cmp,
]
@@ -302,15 +303,54 @@ class Version(object):
field_pairs = zip(self, other)
comparison_functions = self._comparison_functions(partial=self.partial or other.partial)
+ comparisons = zip(comparison_functions, self, other)
- for cmp_fun, field_pair in zip(comparison_functions, field_pairs):
- self_field, other_field = field_pair
+ for cmp_fun, self_field, other_field in comparisons:
cmp_res = cmp_fun(self_field, other_field)
if cmp_res != 0:
return cmp_res
return 0
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) == 0
+
+ def __hash__(self):
+ return hash((self.major, self.minor, self.patch, self.prerelease, self.build))
+
+ def __ne__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) != 0
+
+ def __lt__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) < 0
+
+ def __le__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) <= 0
+
+ def __gt__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) > 0
+
+ def __ge__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.__cmp__(other) >= 0
+
class SpecItem(object):
"""A requirement specification."""
@@ -434,7 +474,7 @@ class Spec(object):
def compare(v1, v2):
- return cmp(Version(v1), Version(v2))
+ return base_cmp(Version(v1), Version(v2))
def match(spec, version):
diff --git a/src/semantic_version/compat.py b/src/semantic_version/compat.py
new file mode 100644
index 0000000..51102fc
--- /dev/null
+++ b/src/semantic_version/compat.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012-2013 Raphaël Barrois
+# This code is distributed under the two-clause BSD License.
+
+import sys
+
+is_python2 = (sys.version_info[0] == 2)
+
+if is_python2: # pragma: no cover
+ base_cmp = cmp
+else: # pragma: no cover
+ def base_cmp(x, y):
+ if x < y:
+ return -1
+ elif x > y:
+ return 1
+ else:
+ return 0
diff --git a/src/semantic_version/django_fields.py b/src/semantic_version/django_fields.py
index 3259331..6a70129 100644
--- a/src/semantic_version/django_fields.py
+++ b/src/semantic_version/django_fields.py
@@ -2,6 +2,8 @@
# Copyright (c) 2012-2013 Raphaël Barrois
# This code is distributed under the two-clause BSD License.
+from __future__ import unicode_literals
+
from django.db import models
from django.utils.translation import ugettext_lazy as _
@@ -31,11 +33,16 @@ class BaseSemVerField(models.CharField):
return super(BaseSemVerField, self).run_validators(str(value))
-class VersionField(BaseSemVerField):
+# Py2 and Py3-compatible metaclass
+SemVerField = models.SubfieldBase(
+ str('SemVerField'), (BaseSemVerField, models.CharField), {})
+
+
+class VersionField(SemVerField):
default_error_messages = {
- 'invalid': _(u"Enter a valid version number in X.Y.Z format."),
+ 'invalid': _("Enter a valid version number in X.Y.Z format."),
}
- description = _(u"Version")
+ description = _("Version")
def __init__(self, *args, **kwargs):
self.partial = kwargs.pop('partial', False)
@@ -54,11 +61,11 @@ class VersionField(BaseSemVerField):
return base.Version(value, partial=self.partial)
-class SpecField(BaseSemVerField):
+class SpecField(SemVerField):
default_error_messages = {
- 'invalid': _(u"Enter a valid version number spec list in ==X.Y.Z,>=A.B.C format."),
+ 'invalid': _("Enter a valid version number spec list in ==X.Y.Z,>=A.B.C format."),
}
- description = _(u"Version specification list")
+ description = _("Version specification list")
def to_python(self, value):
"""Converts any value to a base.Spec field."""
diff --git a/tests/compat.py b/tests/compat.py
new file mode 100644
index 0000000..90f1baa
--- /dev/null
+++ b/tests/compat.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012-2013 Raphaël Barrois
+# This code is distributed under the two-clause BSD License.
+
+import sys
+
+is_python2 = (sys.version_info[0] == 2)
+
+
+try: # pragma: no cover
+ import unittest2 as unittest
+except ImportError: # pragma: no cover
+ import unittest
+
diff --git a/tests/test_base.py b/tests/test_base.py
index 84119f1..3006ba0 100755
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -5,11 +5,7 @@
"""Test the various functions from 'base'."""
-try: # pragma: no cover
- import unittest2 as unittest
-except ImportError: # pragma: no cover
- import unittest
-
+from .compat import unittest, is_python2
from semantic_version import base
@@ -171,6 +167,8 @@ class VersionTestCase(unittest.TestCase):
self.assertNotEqual(text, base.Version(text))
partial_versions = {
+ '1.1': (1, 1, None, None, None),
+ '2': (2, None, None, None, None),
'1.0.0-alpha': (1, 0, 0, ('alpha',), None),
'1.0.0-alpha.1': (1, 0, 0, ('alpha', '1'), None),
'1.0.0-beta.2': (1, 0, 0, ('beta', '2'), None),
@@ -229,6 +227,21 @@ class VersionTestCase(unittest.TestCase):
]))
)
+ @unittest.skipIf(is_python2, "Comparisons to other objects are broken in Py2.")
+ def test_invalid_comparisons(self):
+ v = base.Version('0.1.0')
+ with self.assertRaises(TypeError):
+ v < '0.1.0'
+ with self.assertRaises(TypeError):
+ v <= '0.1.0'
+ with self.assertRaises(TypeError):
+ v > '0.1.0'
+ with self.assertRaises(TypeError):
+ v >= '0.1.0'
+
+ self.assertTrue(v != '0.1.0')
+ self.assertFalse(v == '0.1.0')
+
class SpecItemTestCase(unittest.TestCase):
components = {
@@ -345,6 +358,7 @@ class SpecItemTestCase(unittest.TestCase):
class CoerceTestCase(unittest.TestCase):
examples = {
# Dict of target: [list of equivalents]
+ '0.0.0': ('0', '0.0', '0.0.0', '0.0.0+', '0-', '00000000.00'),
'0.1.0': ('0.1', '0.1+', '0.1-', '0.1.0', '0.000001.000000000000'),
'0.1.0+2': ('0.1.0+2', '0.1.0.2'),
'0.1.0+2.3.4': ('0.1.0+2.3.4', '0.1.0+2+3+4', '0.1.0.2+3+4'),
@@ -360,6 +374,9 @@ class CoerceTestCase(unittest.TestCase):
v_sample = base.Version.coerce(sample)
self.assertEqual(target, v_sample)
+ def test_invalid(self):
+ self.assertRaises(ValueError, base.Version.coerce, 'v1')
+
class SpecTestCase(unittest.TestCase):
examples = {
diff --git a/tests/test_django.py b/tests/test_django.py
index fd4e044..ffdfe58 100644
--- a/tests/test_django.py
+++ b/tests/test_django.py
@@ -2,6 +2,8 @@
# Copyright (c) 2012-2013 Raphaël Barrois
# This code is distributed under the two-clause BSD License.
+from __future__ import unicode_literals
+
try: # pragma: no cover
import unittest2 as unittest
except ImportError: # pragma: no cover
@@ -28,7 +30,7 @@ if django_loaded: # pragma: no cover
'tests.django_test_app',
]
)
- from django_test_app import models
+ from .django_test_app import models
from django.core import serializers
try: # pragma: no cover
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 01c8ae8..585011d 100755
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -63,7 +63,9 @@ class ComparisonTestCase(unittest.TestCase):
self.assertTrue(first_ver == second_ver, '%r != %r' % (first_ver, second_ver))
else:
self.assertTrue(first_ver > second_ver, '%r !> %r' % (first_ver, second_ver))
- self.assertEqual(cmp(i, j), semantic_version.compare(first, second))
+
+ cmp_res = -1 if i < j else (1 if i > j else 0)
+ self.assertEqual(cmp_res, semantic_version.compare(first, second))
if __name__ == '__main__': # pragma: no cover