From a5cc0fb509b2c515ae73c85f9a4d426a3f9100e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 Aug 2019 17:41:33 +0200 Subject: Allow Version(major=1, ...). Eases the creation of version objects from existing versions. We still validate the type and structure of each component. --- ChangeLog | 8 ++++++++ README.rst | 16 ++++++++++++++++ docs/reference.rst | 10 ++++++++++ semantic_version/base.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- tests/test_parsing.py | 21 +++++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8804796..ca4f3af 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,14 @@ ChangeLog ========= +2.7.0 (unreleased) +------------------ + +*New:* + + * Allow creation of a ``Version`` directly from parsed components, as keyword arguments + (``Version(major=1, minor=2, patch=3)``) + 2.6.0 (2016-09-25) ------------------ diff --git a/README.rst b/README.rst index 682a8c7..ac42f72 100644 --- a/README.rst +++ b/README.rst @@ -149,6 +149,22 @@ It is also possible to check whether a given string is a proper semantic version False +Finally, one may create a :class:`Version` with named components instead: + +.. code-block:: pycon + + >>> semantic_version.Version(major=0, minor=1, patch=2) + Version('0.1.2') + +In that case, ``major``, ``minor`` and ``patch`` are mandatory, and must be integers. +``prerelease`` and ``patch``, if provided, must be tuples of strings: + +.. code-block:: pycon + + >>> semantic_version.Version(major=0, minor=1, patch=2, prerelease=('alpha', '2')) + Version('0.1.2-alpha.2') + + Requirement specification ------------------------- diff --git a/docs/reference.rst b/docs/reference.rst index 7c91200..58aa320 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -76,6 +76,16 @@ Representing a version (the Version class) >>> str(Version('1.1.1')) '1.1.1' +.. class:: Version(major: int, minor: int, patch: int, prereleases: tuple, build: tuple[, partial=False]) + + Constructed from named components: + + .. code-block:: pycon + + >>> Version(major=1, minor=2, patch=3) + Version('1.2.3') + + .. rubric:: Attributes diff --git a/semantic_version/base.py b/semantic_version/base.py index 4ddaf05..49c3fb5 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -73,8 +73,30 @@ class Version(object): version_re = re.compile(r'^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$') partial_version_re = re.compile(r'^(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:-([0-9a-zA-Z.-]*))?(?:\+([0-9a-zA-Z.-]*))?$') - def __init__(self, version_string, partial=False): - major, minor, patch, prerelease, build = self.parse(version_string, partial) + def __init__( + self, + version_string=None, + *, + major=None, + minor=None, + patch=None, + prerelease=None, + build=None, + partial=False, + ): + has_text = version_string is not None + has_parts = not (major is minor is patch is prerelease is build is None) + if not has_text ^ has_parts: + raise ValueError("Call either Version('1.2.3') or Version(major=1, ...).") + + if has_text: + major, minor, patch, prerelease, build = self.parse(version_string, partial) + else: + # Convenience: allow to omit prerelease/build. + if not partial: + prerelease = prerelease or () + build = build or () + self._validate_kwargs(major, minor, patch, prerelease, build, partial) self.major = major self.minor = minor @@ -254,6 +276,25 @@ class Version(object): if item[0] == '0' and item.isdigit() and item != '0' and not allow_leading_zeroes: raise ValueError("Invalid leading zero in identifier %r" % item) + @classmethod + def _validate_kwargs(cls, major, minor, patch, prerelease, build, partial): + if ( + major != int(major) + or minor != cls._coerce(minor, partial) + or patch != cls._coerce(patch, partial) + or prerelease is None and not partial + or build is None and not partial + ): + raise ValueError( + "Invalid kwargs to Version(major=%r, minor=%r, patch=%r, " + "prerelease=%r, build=%r, partial=%r" % ( + major, minor, patch, prerelease, build, partial + )) + if prerelease is not None: + cls._validate_identifiers(prerelease, allow_leading_zeroes=False) + if build is not None: + cls._validate_identifiers(build, allow_leading_zeroes=True) + def __iter__(self): return iter((self.major, self.minor, self.patch, self.prerelease, self.build)) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 8fd22da..af99d05 100755 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -28,6 +28,15 @@ class ParsingTestCase(unittest.TestCase): '0.1.2-rc1.3-14.15+build.2012-01-01.11h34', ] + valid_fields = [ + ('0.1.1', [0, 1, 1, (), ()]), + ('0.1.1', [0, 1, 1, None, None]), + ('0.1.2-rc1', [0, 1, 2, ('rc1',), ()]), + ('0.1.2-rc1.3.4', [0, 1, 2, ('rc1', '3', '4'), ()]), + ('0.1.2+build42-12.2012-01-01.12h23', [0, 1, 2, (), ('build42-12', '2012-01-01', '12h23')]), + ('0.1.2-rc1.3-14.15+build.2012-01-01.11h34', [0, 1, 2, ('rc1', '3-14', '15'), ('build', '2012-01-01', '11h34')]), + ] + def test_invalid(self): for invalid in self.invalids: self.assertRaises(ValueError, semantic_version.Version, invalid) @@ -37,6 +46,18 @@ class ParsingTestCase(unittest.TestCase): version = semantic_version.Version(valid) self.assertEqual(valid, str(version)) + def test_kwargs(self): + for text, fields in self.valid_fields: + major, minor, patch, prerelease, build = fields + version = semantic_version.Version( + major=major, + minor=minor, + patch=patch, + prerelease=prerelease, + build=build, + ) + self.assertEqual(text, str(version)) + class ComparisonTestCase(unittest.TestCase): order = [ -- cgit v1.2.1