From 6aae60fb530f7608bc379a57a477e904a816544d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 21 Mar 2013 23:38:40 +0100 Subject: Add Python3 support. --- .travis.yml | 2 + README | 3 ++ setup.py | 22 ++++++++--- src/semantic_version/base.py | 70 +++++++++++++++++++++++++++-------- src/semantic_version/compat.py | 18 +++++++++ src/semantic_version/django_fields.py | 19 +++++++--- tests/compat.py | 14 +++++++ tests/test_base.py | 27 +++++++++++--- tests/test_django.py | 4 +- tests/test_parsing.py | 4 +- 10 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 src/semantic_version/compat.py create mode 100644 tests/compat.py 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 -- cgit v1.2.1