From 353c3a910423e6d24f88d0d864ca79bc615e998f Mon Sep 17 00:00:00 2001 From: Carlo Date: Sun, 24 Mar 2019 12:45:01 +0100 Subject: [PATCH 001/219] add LICENSE file to distribution (#116) --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 9561fb10..926cc7b1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include README.rst +include LICENSE.txt From 2a7e0207352b1942f298f1194c14f7bc03cb39f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20B=C3=B6hn?= Date: Sun, 24 Mar 2019 07:45:26 -0400 Subject: [PATCH 002/219] Use a method decorator to deduplicate binary operator logic (#118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The binary-operator comparison methods of the `VersionInfo` class all contain the same boilerplate type-checking code. This patch adds a decorator function that wraps these methods in the type-check logic, which allows the operator method implementations to be stripped down to single lines of code – whose legibility allows them to be readily scanned by newcomers to the project and others unfamiliar with the codebase. --- semver.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/semver.py b/semver.py index 5f5be2c2..76045b5e 100644 --- a/semver.py +++ b/semver.py @@ -5,6 +5,7 @@ import collections import re +from functools import wraps __version__ = '2.8.1' __author__ = 'Kostiantyn Rybnikov' @@ -73,6 +74,16 @@ def parse(version): return version_parts +def comparator(operator): + """ Wrap a VersionInfo binary op method in a type-check """ + @wraps(operator) + def wrapper(self, other): + if not isinstance(other, (VersionInfo, dict)): + return NotImplemented + return operator(self, other) + return wrapper + + class VersionInfo(object): """ :param int major: version when you make incompatible API changes. @@ -124,34 +135,28 @@ def _asdict(self): ("build", self.build) )) + @comparator def __eq__(self, other): - if not isinstance(other, (VersionInfo, dict)): - return NotImplemented return _compare_by_keys(self._asdict(), _to_dict(other)) == 0 + @comparator def __ne__(self, other): - if not isinstance(other, (VersionInfo, dict)): - return NotImplemented return _compare_by_keys(self._asdict(), _to_dict(other)) != 0 + @comparator def __lt__(self, other): - if not isinstance(other, (VersionInfo, dict)): - return NotImplemented return _compare_by_keys(self._asdict(), _to_dict(other)) < 0 + @comparator def __le__(self, other): - if not isinstance(other, (VersionInfo, dict)): - return NotImplemented return _compare_by_keys(self._asdict(), _to_dict(other)) <= 0 + @comparator def __gt__(self, other): - if not isinstance(other, (VersionInfo, dict)): - return NotImplemented return _compare_by_keys(self._asdict(), _to_dict(other)) > 0 + @comparator def __ge__(self, other): - if not isinstance(other, (VersionInfo, dict)): - return NotImplemented return _compare_by_keys(self._asdict(), _to_dict(other)) >= 0 def __repr__(self): From d2671fd54246224f83c4ffac2fad40409475eae3 Mon Sep 17 00:00:00 2001 From: Thomas Schraitle Date: Sun, 24 Mar 2019 12:46:00 +0100 Subject: [PATCH 003/219] Improve test_immutable function (#120) * Add property setter and raise AttributeError Raise AttributeError with useful message for major, minor, patch, prerelease, and build. * Improve test_immutable with pytest.raises * Use pytest.raises which replace and shorten the try..except..else blocks * Use match keyword in pytest.raises which matches the AttributeError from the property setter * Split test_immutable into several different test functions, e.g. test_immutable_major * Fix flake8 issues --- semver.py | 20 +++++++++++++ test_semver.py | 80 +++++++++++++++++++++++--------------------------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/semver.py b/semver.py index 76045b5e..e76bf5a5 100644 --- a/semver.py +++ b/semver.py @@ -106,22 +106,42 @@ def __init__(self, major, minor, patch, prerelease=None, build=None): def major(self): return self._major + @major.setter + def major(self, value): + raise AttributeError("attribute 'major' is readonly") + @property def minor(self): return self._minor + @minor.setter + def minor(self, value): + raise AttributeError("attribute 'minor' is readonly") + @property def patch(self): return self._patch + @patch.setter + def patch(self, value): + raise AttributeError("attribute 'patch' is readonly") + @property def prerelease(self): return self._prerelease + @prerelease.setter + def prerelease(self, value): + raise AttributeError("attribute 'prerelease' is readonly") + @property def build(self): return self._build + @build.setter + def build(self, value): + raise AttributeError("attribute 'build' is readonly") + def _astuple(self): return (self.major, self.minor, self.patch, self.prerelease, self.build) diff --git a/test_semver.py b/test_semver.py index c8532202..13a0d716 100644 --- a/test_semver.py +++ b/test_semver.py @@ -23,6 +23,12 @@ ] +@pytest.fixture +def version(): + return VersionInfo(major=1, minor=2, patch=3, + prerelease='alpha.1.2', build='build.11.e0f985a') + + @pytest.mark.parametrize("func", SEMVERFUNCS, ids=[func.__name__ for func in SEMVERFUNCS]) def test_fordocstrings(func): @@ -416,47 +422,33 @@ def test_parse_method_for_version_info(): assert str(v) == s_version -def test_immutable(): - v = VersionInfo(major=1, minor=2, patch=3, - prerelease='alpha.1.2', build='build.11.e0f985a') - try: - v.major = 9 - except AttributeError: - pass - else: - assert 0, "attribute 'major' must be readonly" - - try: - v.minor = 9 - except AttributeError: - pass - else: - assert 0, "attribute 'minor' must be readonly" - - try: - v.patch = 9 - except AttributeError: - pass - else: - assert 0, "attribute 'patch' must be readonly" - - try: - v.prerelease = 'alpha.9.9' - except AttributeError: - pass - else: - assert 0, "attribute 'prerelease' must be readonly" - - try: - v.build = 'build.99.e0f985a' - except AttributeError: - pass - else: - assert 0, "attribute 'build' must be readonly" - - try: - v.new_attribute = 'forbidden' - except AttributeError: - pass - else: - assert 0, "no new attribute can be set" +def test_immutable_major(version): + with pytest.raises(AttributeError, match="attribute 'major' is readonly"): + version.major = 9 + + +def test_immutable_minor(version): + with pytest.raises(AttributeError, match="attribute 'minor' is readonly"): + version.minor = 9 + + +def test_immutable_patch(version): + with pytest.raises(AttributeError, match="attribute 'patch' is readonly"): + version.patch = 9 + + +def test_immutable_prerelease(version): + with pytest.raises(AttributeError, + match="attribute 'prerelease' is readonly"): + version.prerelease = 'alpha.9.9' + + +def test_immutable_build(version): + with pytest.raises(AttributeError, match="attribute 'build' is readonly"): + version.build = 'build.99.e0f985a' + + +def test_immutable_unknown_attribute(version): + # "no new attribute can be set" + with pytest.raises(AttributeError): + version.new_attribute = 'forbidden' From 0566d968d93f6f33c3b99c2b9d367622939cb261 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 24 Mar 2019 14:09:46 +0100 Subject: [PATCH 004/219] Fix #121: use python3 instead of python3.4 (#122) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 15773afe..20024a57 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,6 @@ setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 [testenv:flake8] -basepython = python3.4 +basepython = python3 deps = flake8 commands = flake8 From 122dcaeae1c46e4dc31abc8d052494dd54c6c1a7 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 24 Mar 2019 15:02:25 +0100 Subject: [PATCH 005/219] Fix __repr__ and derive class name from type (#123) Avoids hard coded class name --- semver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/semver.py b/semver.py index e76bf5a5..4475a767 100644 --- a/semver.py +++ b/semver.py @@ -182,7 +182,7 @@ def __ge__(self, other): def __repr__(self): s = ", ".join("%s=%r" % (key, val) for key, val in self._asdict().items()) - return "VersionInfo(%s)" % s + return "%s(%s)" % (type(self).__name__, s) def __str__(self): return format_version(*(self._astuple())) From 373af82cebee874709662257df6831977d2b9cae Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 24 Mar 2019 15:43:53 +0100 Subject: [PATCH 006/219] Fixes #109 and add iterator to VersionInfo (#124) * Add Karl to CONTRIBUTORS * Use version fixture in test function test_version_info_should_be_iterable based on the work of Karol Werner in PR #109 --- CONTRIBUTORS | 2 +- semver.py | 6 ++++++ test_semver.py | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index ce0b166c..205de33a 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -29,7 +29,7 @@ Significant contributors * Craig Blaszczyk * Jan Pieter Waagmeester * Jelo Agnasin -* Karol Werner +* Karol Werner * Peter Bittner * robi-wan * T. Jameson Little diff --git a/semver.py b/semver.py index 4475a767..5f9a938c 100644 --- a/semver.py +++ b/semver.py @@ -155,6 +155,12 @@ def _asdict(self): ("build", self.build) )) + def __iter__(self): + """Implement iter(self).""" + # As long as we support Py2.7, we can't use the "yield from" syntax + for v in self._astuple(): + yield v + @comparator def __eq__(self, other): return _compare_by_keys(self._asdict(), _to_dict(other)) == 0 diff --git a/test_semver.py b/test_semver.py index 13a0d716..5e8aae5b 100644 --- a/test_semver.py +++ b/test_semver.py @@ -452,3 +452,8 @@ def test_immutable_unknown_attribute(version): # "no new attribute can be set" with pytest.raises(AttributeError): version.new_attribute = 'forbidden' + + +def test_version_info_should_be_iterable(version): + assert tuple(version) == (version.major, version.minor, version.patch, + version.prerelease, version.build) From 344ea8902a52b198534e15759925884601ac7509 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 24 Mar 2019 17:07:49 +0100 Subject: [PATCH 007/219] Create setup.cfg for pytest & tox config and coverage (#125) * Create setup.cfg for pytest config * Use "pytest" command instead of "py.test" (the latter may be obsolete in the future) * Move some options into setup.cfg * Introduce pytest-cov in "tox.ini" for coverage and config file ".coveragerc" * Add additional options to activate coverage for semver script and output coverage report * Use py.test again instead of pytest For some reasons, this works not on Travis with 14.04 LTS (Trusty Tahr) * Ignore .egg/ directory * Typo fix: .egg -> .eggs * Use --ignore option --- .coveragerc | 9 +++++++++ setup.cfg | 9 +++++++++ tox.ini | 6 ++++-- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 .coveragerc create mode 100644 setup.cfg diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..0b2e8f7a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,9 @@ +# https://pytest-cov.readthedocs.io/ + +[run] +source = semver +branch = True + +[report] +exclude_lines = + pragma: no cover diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..fcaea3c3 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,9 @@ +[tool:pytest] +norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ +addopts = + -q + --ignore=.eggs/ + --no-cov-on-fail + --cov=semver + --cov-report=term-missing + --doctest-modules diff --git a/tox.ini b/tox.ini index 20024a57..82695362 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,10 @@ envlist = pypy [testenv] -commands = py.test -q --doctest-modules -deps = pytest +commands = py.test +deps = + pytest + pytest-cov setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 From c399fe7a2075dd699a79d27d7a16d4bee953d2ed Mon Sep 17 00:00:00 2001 From: Jarek Zgoda Date: Mon, 15 Apr 2019 21:02:39 +0200 Subject: [PATCH 008/219] Update doc for format_version to match expected param types (#129) --- semver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/semver.py b/semver.py index 5f9a938c..0709a77f 100644 --- a/semver.py +++ b/semver.py @@ -403,9 +403,9 @@ def min_ver(ver1, ver2): def format_version(major, minor, patch, prerelease=None, build=None): """Format a version according to the Semantic Versioning specification - :param str major: the required major part of a version - :param str minor: the required minor part of a version - :param str patch: the required patch part of a version + :param int major: the required major part of a version + :param int minor: the required minor part of a version + :param int patch: the required patch part of a version :param str prerelease: the optional prerelease part of a version :param str build: the optional build part of a version :return: the formatted string From 17cfd1b824e8afbb334dace3f09bb9022a5a4af1 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 29 May 2019 16:09:54 +0200 Subject: [PATCH 009/219] Fix CI (#133) * Update setup.py * Update tox.ini * Update .travis.yml * Allow recent virtualenv * Use newer pip and setuptools * Fix for 3.7 on travis See https://docs.travis-ci.com/user/languages/python/#python-37-and-higher Closes #131 Closes #112 Replaces #113 --- .travis.yml | 8 +++++++- setup.py | 3 ++- tox.ini | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 68925a5d..633becbe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: python -install: pip install "virtualenv<14.0.0" +install: + - pip install --upgrade pip setuptools + - pip install virtualenv script: python setup.py test matrix: include: @@ -18,5 +20,9 @@ matrix: - python: "3.6" env: TOXENV=py36 + - python: "3.7" + dist: xenial + env: TOXENV=py37 + - python: "pypy" env: TOXENV=pypy diff --git a/setup.py b/setup.py index acdb6024..9deec889 100755 --- a/setup.py +++ b/setup.py @@ -94,9 +94,10 @@ def read_file(filename): 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Software Development :: Libraries :: Python Modules', ], - tests_require=['tox', 'virtualenv<14.0.0'], + tests_require=['tox', 'virtualenv'], cmdclass={ 'clean': Clean, 'test': Tox, diff --git a/tox.ini b/tox.ini index 82695362..a9d4ad57 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = flake8 - py{27,34,35,36} + py{27,34,35,36,37} pypy [testenv] From 21f87f6403b10bc262ece0f540be043ceef51c23 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 29 May 2019 17:27:55 +0200 Subject: [PATCH 010/219] Fix comparison (#132) Closes #102 Closes #103 --- CHANGELOG | 8 ++++++++ semver.py | 13 +++++++++---- test_semver.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 721047b5..f794629a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,14 @@ Python SemVer library All notable changes to this code base will be documented in this file, in every released version. +Version 2.8.x +============= +:Released: 20yy-mm-dd +:Maintainer: Sébastien Celles + +* Issue #102 (PR #...). Fix comparison between VersionInfo and tuple +* Issue #103 (PR #...). Disallow comparison between VersionInfo and string (and int) + Version 2.8.1 ============= :Released: 2018-07-09 diff --git a/semver.py b/semver.py index 0709a77f..d234f26a 100644 --- a/semver.py +++ b/semver.py @@ -7,7 +7,8 @@ from functools import wraps -__version__ = '2.8.1' + +__version__ = '2.8.2' __author__ = 'Kostiantyn Rybnikov' __author_email__ = 'k-bx@k-bx.com' __maintainer__ = 'Sebastien Celles' @@ -78,8 +79,10 @@ def comparator(operator): """ Wrap a VersionInfo binary op method in a type-check """ @wraps(operator) def wrapper(self, other): - if not isinstance(other, (VersionInfo, dict)): - return NotImplemented + comparable_types = (VersionInfo, dict, tuple) + if not isinstance(other, comparable_types): + raise TypeError("other type %r must be in %r" + % (type(other), comparable_types)) return operator(self, other) return wrapper @@ -95,7 +98,7 @@ class VersionInfo(object): """ __slots__ = ('_major', '_minor', '_patch', '_prerelease', '_build') - def __init__(self, major, minor, patch, prerelease=None, build=None): + def __init__(self, major, minor=0, patch=0, prerelease=None, build=None): self._major = major self._minor = minor self._patch = patch @@ -215,6 +218,8 @@ def parse(version): def _to_dict(obj): if isinstance(obj, VersionInfo): return obj._asdict() + elif isinstance(obj, tuple): + return VersionInfo(*obj)._asdict() return obj diff --git a/test_semver.py b/test_semver.py index 5e8aae5b..96b78967 100644 --- a/test_semver.py +++ b/test_semver.py @@ -401,6 +401,46 @@ def test_should_compare_version_dictionaries(): assert not(v1 == v4) +def test_should_compare_version_tuples(): + v0 = VersionInfo(major=0, minor=4, patch=5, + prerelease='pre.2', build='build.4') + v1 = VersionInfo(major=3, minor=4, patch=5, + prerelease='pre.2', build='build.4') + for t in ((1, 0, 0), (1, 0), (1,), (1, 0, 0, 'pre.2'), + (1, 0, 0, 'pre.2', 'build.4')): + assert v0 < t + assert v0 <= t + assert v0 != t + assert not v0 == t + assert v1 > t + assert v1 >= t + # Symmetric + assert t > v0 + assert t >= v0 + assert t < v1 + assert t <= v1 + assert t != v0 + assert not t == v0 + + +def test_should_not_allow_to_compare_version_with_string(): + v1 = VersionInfo(major=3, minor=4, patch=5, + prerelease='pre.2', build='build.4') + with pytest.raises(TypeError): + v1 > "1.0.0" + with pytest.raises(TypeError): + "1.0.0" > v1 + + +def test_should_not_allow_to_compare_version_with_int(): + v1 = VersionInfo(major=3, minor=4, patch=5, + prerelease='pre.2', build='build.4') + with pytest.raises(TypeError): + v1 > 1 + with pytest.raises(TypeError): + 1 > v1 + + def test_should_compare_prerelease_with_numbers_and_letters(): v1 = VersionInfo(major=1, minor=9, patch=1, prerelease='1unms', build=None) v2 = VersionInfo(major=1, minor=9, patch=1, prerelease=None, build='1asd') From c1ce5a12b1ca07708e3e472ec7b485986a5d4f6f Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 29 Sep 2019 19:41:57 +0200 Subject: [PATCH 011/219] Fix #126: Provide tox.ini for documentation (#127) * Add "docs" target to run sphinx and output HTML * Provide docs/requirements.txt file * Fix typo in docs/conf.py --- docs/conf.py | 2 +- docs/requirements.txt | 3 +++ tox.ini | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 docs/requirements.txt diff --git a/docs/conf.py b/docs/conf.py index 843b7466..1afd8e7f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -172,6 +172,6 @@ ] -interpshinx_mapping = { +intersphinx_mapping = { "matplotlib": ('http://matplotlib.org', None), } diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..383acaaa --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +# requirements file for documentation +sphinx +sphinx_rtd_theme diff --git a/tox.ini b/tox.ini index a9d4ad57..e172f8a1 100644 --- a/tox.ini +++ b/tox.ini @@ -16,3 +16,9 @@ setenv = basepython = python3 deps = flake8 commands = flake8 + +[testenv:docs] +basepython = python3 +deps = -r{toxinidir}/docs/requirements.txt +skip_install = true +commands = sphinx-build {posargs:-E} -b html docs dist/docs From a598059a3efababba0bd9d626cfef6e0bc7361d1 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 30 Sep 2019 20:27:19 +0200 Subject: [PATCH 012/219] Implement #139: Bump version inside VersionInfo (#141) * Implement #139: Bump version inside VersionInfo * Add bump_major, bump_minor, bump_patch, bump_prerelease and bump_build as methods in class VersionInfo. With this methods, it is now possible to write something like this: ```python ver = semver.parse_version_info("3.4.5") new_ver = ver.bump_major().bump_minor() ``` * Add test cases * Fix Travis CI build * Fix docstring to indicate about new object Co-authored-by: Karol "ppkt" --- semver.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ test_semver.py | 45 ++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/semver.py b/semver.py index d234f26a..ea0ac81b 100644 --- a/semver.py +++ b/semver.py @@ -164,6 +164,80 @@ def __iter__(self): for v in self._astuple(): yield v + def bump_major(self): + """Raise the major part of the version, return a new object + but leave self untouched + + :return: new object with the raised major part + :rtype: VersionInfo + + >>> import semver + >>> ver = semver.parse_version_info("3.4.5") + >>> ver.bump_major() + VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) + """ + return parse_version_info(bump_major(str(self))) + + def bump_minor(self): + """Raise the minor part of the version, return a new object + but leave self untouched + + :return: new object with the raised minor part + :rtype: VersionInfo + + >>> import semver + >>> ver = semver.parse_version_info("3.4.5") + >>> ver.bump_minor() + VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) + """ + return parse_version_info(bump_minor(str(self))) + + def bump_patch(self): + """Raise the patch part of the version, return a new object + but leave self untouched + + :return: new object with the raised patch part + :rtype: VersionInfo + + >>> import semver + >>> ver = semver.parse_version_info("3.4.5") + >>> ver.bump_patch() + VersionInfo(major=3, minor=4, patch=6, prerelease=None, build=None) + """ + return parse_version_info(bump_patch(str(self))) + + def bump_prerelease(self, token='rc'): + """Raise the prerelease part of the version, return a new object + but leave self untouched + + :param token: defaults to 'rc' + :return: new object with the raised prerelease part + :rtype: str + + >>> import semver + >>> ver = semver.parse_version_info("3.4.5-rc.1") + >>> ver.bump_prerelease() + VersionInfo(major=3, minor=4, patch=5, prerelease='rc.2', \ +build=None) + """ + return parse_version_info(bump_prerelease(str(self), token)) + + def bump_build(self, token='build'): + """Raise the build part of the version, return a new object + but leave self untouched + + :param token: defaults to 'build' + :return: new object with the raised build part + :rtype: str + + >>> import semver + >>> ver = semver.parse_version_info("3.4.5-rc.1+build.9") + >>> ver.bump_build() + VersionInfo(major=3, minor=4, patch=5, prerelease='rc.1', \ +build='build.10') + """ + return parse_version_info(bump_build(str(self), token)) + @comparator def __eq__(self, other): return _compare_by_keys(self._asdict(), _to_dict(other)) == 0 diff --git a/test_semver.py b/test_semver.py index 96b78967..45ec3405 100644 --- a/test_semver.py +++ b/test_semver.py @@ -263,6 +263,51 @@ def test_should_bump_patch(): assert bump_patch('3.4.5') == '3.4.6' +def test_should_versioninfo_bump_major_and_minor(): + v = parse_version_info("3.4.5") + expected = parse_version_info("4.1.0") + assert v.bump_major().bump_minor() == expected + + +def test_should_versioninfo_bump_minor_and_patch(): + v = parse_version_info("3.4.5") + expected = parse_version_info("3.5.1") + assert v.bump_minor().bump_patch() == expected + + +def test_should_versioninfo_bump_patch_and_prerelease(): + v = parse_version_info("3.4.5-rc.1") + expected = parse_version_info("3.4.6-rc.1") + assert v.bump_patch().bump_prerelease() == expected + + +def test_should_versioninfo_bump_patch_and_prerelease_with_token(): + v = parse_version_info("3.4.5-dev.1") + expected = parse_version_info("3.4.6-dev.1") + assert v.bump_patch().bump_prerelease("dev") == expected + + +def test_should_versioninfo_bump_prerelease_and_build(): + v = parse_version_info("3.4.5-rc.1+build.1") + expected = parse_version_info("3.4.5-rc.2+build.2") + assert v.bump_prerelease().bump_build() == expected + + +def test_should_versioninfo_bump_prerelease_and_build_with_token(): + v = parse_version_info("3.4.5-rc.1+b.1") + expected = parse_version_info("3.4.5-rc.2+b.2") + assert v.bump_prerelease().bump_build("b") == expected + + +def test_should_versioninfo_bump_multiple(): + v = parse_version_info("3.4.5-rc.1+build.1") + expected = parse_version_info("3.4.5-rc.2+build.2") + assert v.bump_prerelease().bump_build().bump_build() == expected + expected = parse_version_info("3.4.5-rc.3") + assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == \ + expected + + def test_should_ignore_extensions_for_bump(): assert bump_patch('3.4.5-rc1+build4') == '3.4.6' From ecb07365ea2856e70b241bae73d83c7d485630d5 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 1 Oct 2019 07:34:37 +0200 Subject: [PATCH 013/219] Doc: improve Usage section (#143) * Change titles to using verb instead of noun * Adding version information in docs/conf.py Currently, the version of semver is imported (using from semver import __version__). To avoid importing semver, version string could be managed through bumpversion2. However, this is out of scope for this PR. * Improve entry page * Improve main title with semver's version and a catchy phrase * Move the |latest-version| placeholder to the other placeholders like build status, downloads etc. * Remove the "Welcome bla" heading as it introduces a lonly section and it is mostly useless * Add a short explanation about the "version style" major.minor.patch (cited from semver.org) * Reference the other parts with just the filename (with the .rst extension) and without the title. The title is automatically retrieved from the file, no need to repeat it. This makes the overall doc more consistent. * Change py.test -> pytest * Fix #142: Improve usage documentation * Use descriptive titles (verb+ing) to better find the topic * Structure the content with (sub)sections * Try to structure the different sections with list if there are different options to make it more readable * Document other, possible unknown features * Silence flake8 with "noqa: E402" --- docs/conf.py | 5 +- docs/development.rst | 8 +- docs/index.rst | 33 +++-- docs/install.rst | 4 +- docs/usage.rst | 282 ++++++++++++++++++++++++++++++++++++++----- 5 files changed, 283 insertions(+), 49 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1afd8e7f..786d444e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,6 +20,7 @@ import sys sys.path.insert(0, os.path.abspath('..')) +from semver import __version__ # noqa: E402 # -- General configuration ------------------------------------------------ @@ -57,9 +58,9 @@ # built documents. # # The short X.Y version. -version = '' +version = __version__ # The full version, including alpha/beta/rc tags. -release = '' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/development.rst b/docs/development.rst index 000b2f65..05542d29 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,5 +1,5 @@ -How to Contribute ------------------ +Contributing to semver +====================== When you make changes to the code please run the tests before pushing your code to your fork and opening a `pull request`_: @@ -8,7 +8,7 @@ code to your fork and opening a `pull request`_: python setup.py test -We use `py.test`_ and `tox`_ to run tests against all supported Python +We use `pytest`_ and `tox`_ to run tests against all supported Python versions. All test dependencies are resolved automatically, apart from virtualenv, which for the moment you still may have to install manually: @@ -24,5 +24,5 @@ You can use the ``clean`` command to remove build and test files and folders: .. _pull request: https://github.com/k-bx/python-semver/pulls -.. _py.test: http://pytest.org/ +.. _pytest: http://pytest.org/ .. _tox: http://tox.testrun.org/ diff --git a/docs/index.rst b/docs/index.rst index 84136292..1c15abab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,26 +1,33 @@ -Semver |latest-version| -======================= +Semver |version| -- Semantic Versioning +======================================= -|build-status| |python-support| |downloads| |license| +|build-status| |latest-version| |python-support| |downloads| |license| .. python-semver documentation master file, created by sphinx-quickstart on Tue May 1 16:51:19 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to python-semver's documentation! -========================================= +A Python module for `semantic versioning`_. Simplifies comparing versions. + +The module follows the ``MAJOR.MINOR.PATCH`` style: + +* ``MAJOR`` version when you make incompatible API changes, +* ``MINOR`` version when you add functionality in a backwards compatible manner, and +* ``PATCH`` version when you make backwards compatible bug fixes. + +Additional labels for pre-release and build metadata are supported. + + .. toctree:: :maxdepth: 2 - :caption: Contents: + :caption: Contents - Install - Usage - Development - API - -A Python module for `semantic versioning`_. Simplifies comparing versions. + install + usage + development + api .. |latest-version| image:: https://img.shields.io/pypi/v/semver.svg @@ -41,7 +48,7 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. .. _semantic versioning: http://semver.org/ -Indices and tables +Indices and Tables ================== * :ref:`genindex` diff --git a/docs/install.rst b/docs/install.rst index 26f4e1c0..1abb7ed7 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,5 +1,5 @@ -Installation ------------- +Installing semver +================= For Python 2: diff --git a/docs/usage.rst b/docs/usage.rst index c30ffefb..795cb3ae 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,46 +1,272 @@ -Usage ------ +Using semver +============ -This module provides just couple of functions, main of which are: +The ``semver`` module can store a version in different types: + +* as a string. +* as :class:`semver.VersionInfo`, a dedicated class for a version type. +* as a dictionary. + +Each type can be converted into the other, if the minimum requirements +are met. + + +Creating a Version +------------------ + +A version can be created in different ways: + +* as a complete version string:: + + >>> semver.parse_version_info("3.4.5-pre.2+build.4") + VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") + VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + +* with individual parts:: + + >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') + '3.4.5-pre.2+build.4' + >>> semver.VersionInfo(3, 5) + VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) + + You can pass either an integer or a string for ``major``, ``minor``, or + ``patch``:: + + >>> semver.VersionInfo("3", "5") + VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) + + In the simplest form, ``prerelease`` and ``build`` can also be + integers:: + + >>> semver.VersionInfo(1, 2, 3, 4, 5) + VersionInfo(major=1, minor=2, patch=3, prerelease=4, build=5) + + +Parsing a Version String +------------------------ + +"Parsing" in this context means to identify the different parts in a string. + + +* With :func:`semver.parse_version_info`:: + + >>> semver.parse_version_info("3.4.5-pre.2+build.4") + VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + +* With :func:`semver.VersionInfo.parse` (basically the same as + :func:`semver.parse_version_info`):: + + >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") + VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + +* With :func:`semver.parse`:: + + >>> semver.parse("3.4.5-pre.2+build.4") + {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + + +Accessing Parts of a Version +---------------------------- + +The :class:`semver.VersionInfo` contains attributes to access the different +parts of a version: .. code-block:: python - >>> import semver + >>> v = VersionInfo.parse("3.4.5-pre.2+build.4") + >>> v.major + 3 + >>> v.minor + 4 + >>> v.patch + 5 + >>> v.prerelease + 'pre.2' + >>> v.build + 'build.4' + +However, the attributes are read-only. You cannot change an attribute. +If you do, you get an ``AttributeError``:: + + >>> v.minor = 5 + Traceback (most recent call last) + ... + AttributeError: attribute 'minor' is readonly + +In case you need the different parts of a version stepwise, iterate over the :class:`semver.VersionInfo` instance:: + + >>> for item in VersionInfo.parse("3.4.5-pre.2+build.4"): + ... print(item) + 3 + 4 + 5 + pre.2 + build.4 + >>> list(VersionInfo.parse("3.4.5-pre.2+build.4")) + [3, 4, 5, 'pre.2', 'build.4'] + + +.. _sec.convert.versions: + +Converting Different Version Types +---------------------------------- + +Depending which function you call, you get different types +(as explained in the beginning of this chapter). + +* From a string into :class:`semver.VersionInfo`:: + + >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") + VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + +* From :class:`semver.VersionInfo` into a string:: + + >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4")) + '3.4.5-pre.2+build.4' + +* From a dictionary into :class:`semver.VersionInfo`:: + + >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + >>> semver.VersionInfo(**d) + VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + + As a minimum requirement, your dictionary needs at least the ``major`` + key, others can be omitted. You get a ``TypeError`` if your + dictionary contains invalid keys. + Only ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` + are allowed. + +* From a tuple into :class:`semver.VersionInfo`:: + + >>> t = (3, 5, 6) + >>> semver.VersionInfo(*t) + VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) + +* From a :class:`semver.VersionInfo` into a dictionary:: + + >>> v = semver.VersionInfo(major=3, minor=4, patch=5) + >>> semver.parse(str(v)) + {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': None, 'build': None} + + +Increasing Parts of a Version +----------------------------- + +The ``semver`` module contains the following functions to raise parts of +a version: + +* :func:`semver.bump_major`: raises the major part and set all other parts to + zero. Set ``prerelease`` and ``build`` to ``None``. +* :func:`semver.bump_minor`: raises the minor part and sets ``patch`` to zero. + Set ``prerelease`` and ``build`` to ``None``. +* :func:`semver.bump_patch`: raises the patch part. Set ``prerelease`` and + ``build`` to ``None``. +* :func:`semver.bump_prerelease`: raises the prerelease part and set + ``build`` to ``None``. +* :func:`semver.bump_build`: raises the build part. + +.. code-block:: python + + >>> semver.bump_major("3.4.5-pre.2+build.4") + '4.0.0' + >>> semver.bump_minor("3.4.5-pre.2+build.4") + '3.5.0' + >>> semver.bump_patch("3.4.5-pre.2+build.4") + '3.4.6' + >>> semver.bump_prerelease("3.4.5-pre.2+build.4") + '3.4.5-pre.3' + >>> semver.bump_build("3.4.5-pre.2+build.4") + '3.4.5-pre.2+build.5' + + +Comparing Versions +------------------ + +To compare two versions depends on your type: + +* **Two strings** + + Use :func:`semver.compare`:: + >>> semver.compare("1.0.0", "2.0.0") -1 >>> semver.compare("2.0.0", "1.0.0") 1 >>> semver.compare("2.0.0", "2.0.0") 0 - >>> semver.match("2.0.0", ">=1.0.0") + + The return value is negative if ``version1 < version2``, zero if + ``version1 == version2`` and strictly positive if ``version1 > version2``. + +* **Two** :class:`semver.VersionInfo` **types** + + Use the specific operator. Currently, the operators ``<``, + ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: + + >>> v1 = VersionInfo.parse("3.4.5") + >>> v2 = VersionInfo.parse("3.5.1") + >>> v1 < v2 True - >>> semver.match("1.0.0", ">1.0.0") + >>> v1 > v2 False - >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') - '3.4.5-pre.2+build.4' - >>> version_parts = semver.parse("3.4.5-pre.2+build.4") - >>> version_parts == { - ... 'major': 3, 'minor': 4, 'patch': 5, - ... 'prerelease': 'pre.2', 'build': 'build.4'} + +* **A** :class:`semver.VersionInfo` **type and a** ``tuple`` + + Use the operator as with two :class:`semver.VersionInfo` types:: + + >>> v = VersionInfo.parse("3.4.5") + >>> v > (1, 0) True - >>> version_info = semver.parse_version_info("3.4.5-pre.2+build.4") - >>> # or using static method parse - >>> from semver import VersionInfo - >>> version_info = VersionInfo.parse("3.4.5-pre.2+build.4") - >>> version_info - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - >>> version_info.major - 3 - >>> version_info > (1, 0) + >>> v < (3, 5) True - >>> version_info < (3, 5) + + The opposite does also work:: + + >>> (1, 0) < v True - >>> semver.bump_major("3.4.5") - '4.0.0' - >>> semver.bump_minor("3.4.5") - '3.5.0' - >>> semver.bump_patch("3.4.5") - '3.4.6' + >>> (3, 5) > v + True + +Other types cannot be compared (like dictionaries, lists etc). + +If you need to convert some types into other, refer to :ref:`sec.convert.versions`. + + + +Comparing Versions through an Expression +--------------------------------------- + +If you need a more fine-grained approach of comparing two versions, +use the :func:`semver.match` function. It expects two arguments: + +1. a version string +2. a match expression + +Currently, the match expression supports the following operators: + +* ``<`` smaller than +* ``>`` greater than +* ``>=`` greater or equal than +* ``<=`` smaller or equal than +* ``==`` equal +* ``!=`` not equal + +That gives you the following possibilities to express your condition: + +.. code-block:: python + + >>> semver.match("2.0.0", ">=1.0.0") + True + >>> semver.match("1.0.0", ">1.0.0") + False + + +Getting Minimum and Maximum of two Versions +------------------------------------------- + +.. code-block:: python + >>> semver.max_ver("1.0.0", "2.0.0") '2.0.0' >>> semver.min_ver("1.0.0", "2.0.0") From f60e11cf632635d6a9a2d370896ef29abff10df3 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 1 Oct 2019 13:36:09 +0200 Subject: [PATCH 014/219] Fix #145: add posargs in tox.ini (single test cases) (#146) Fix #146: improve tox.ini/.travis.yml * Changes in tox.ini: * add posargs for pytest. This allows to run only a single test function. * Rename py.test -> pytest as the old spelling py.test will be deprecated in the future. * Changes in .travis.yml: * Add cache for pip to speed up things a little bit * Add tox as a installation requirement * Replace setup.py with tox as it discouraged in https://tox.readthedocs.io/en/latest/example/basic.html#integration-with-setup-py-test-command --- .travis.yml | 6 ++++-- tox.ini | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 633becbe..0082ac4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ +# config file for automatic testing at travis-ci.org language: python +cache: pip install: - pip install --upgrade pip setuptools - - pip install virtualenv -script: python setup.py test + - pip install virtualenv tox +script: tox -v matrix: include: - python: "2.7" diff --git a/tox.ini b/tox.ini index e172f8a1..33b60895 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = pypy [testenv] -commands = py.test +commands = pytest {posargs:} deps = pytest pytest-cov From 7e48417aa86470df755415efbbaedf83f1264f5d Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 1 Oct 2019 13:38:30 +0200 Subject: [PATCH 015/219] Fix #85: Improve contribution section (#147) * Fix #85: Improve contribution section Closes #115 too. This is a mostly a complete rewrite with the following ideas and changes in mind: * Add a brief workflow, starting from forking, cloning, creating a branch, and opening a pull request. * Use "procedure style" and use active voice to make reader aware of an action * Remove intersphinx section for matplotlib (not used) in docs/conf.py * Fix flake8 W391 for Python 3.4 Error message was: W391 blank line at end of file --- CONTRIBUTORS | 1 + docs/conf.py | 5 --- docs/development.rst | 72 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 205de33a..98d1d623 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -33,6 +33,7 @@ Significant contributors * Peter Bittner * robi-wan * T. Jameson Little +* Tom Schraitle * Tuure Laurinolli * Tyler Cross * Zack Lalanne diff --git a/docs/conf.py b/docs/conf.py index 786d444e..c2b49f49 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -171,8 +171,3 @@ author, 'semver', 'One line description of project.', 'Miscellaneous'), ] - - -intersphinx_mapping = { - "matplotlib": ('http://matplotlib.org', None), -} diff --git a/docs/development.rst b/docs/development.rst index 05542d29..ed0bdf89 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,12 +1,21 @@ Contributing to semver ====================== -When you make changes to the code please run the tests before pushing your -code to your fork and opening a `pull request`_: +Do you want to contribute? Great! We would like to give you some +helpful tips and tricks. +When you make changes to the code, we would greatly appreciate if you +consider the following requirements: -.. code-block:: bash +* Make sure your code adheres to the `Semantic Versioning`_ specification. + +* Check if your feature is covered by the Semantic Versioning specification. + If not, ask on its GitHub project https://github.com/semver/semver. + +* Write test cases if you implement a new feature. - python setup.py test +* Test also for side effects of your new feature and run the complete + test suite. +* Document the new feature. We use `pytest`_ and `tox`_ to run tests against all supported Python versions. All test dependencies are resolved automatically, apart from @@ -16,13 +25,60 @@ virtualenv, which for the moment you still may have to install manually: pip install "virtualenv<14.0.0" # <14.0.0 needed for Python 3.2 only -You can use the ``clean`` command to remove build and test files and folders: +We recommend to use the following workflow if you would like to contribute: -.. code-block:: bash +1. Fork our project on GitHub using this link: + https://github.com/k-bx/python-semver/fork + +2. Clone your forked Git repository (replace ``GITHUB_USER`` with your + account name on GitHub):: + + $ git clone git@github.com:GITHUB_USER/python-semver.git + +3. Create a new branch. You can name your branch whatever you like, but we + recommend to use some meaningful name. If your fix is based on a + existing GitHub issue, add also the number. Good examples would be: + + * ``feature/123-improve-foo`` when implementing a new feature + * ``bugfix/123-fix-security-bar`` when dealing with bugfixes + + Use this :command:`git` command:: + + $ git checkout -b feature/NAME_OF_YOUR_FEATURE + +4. Work on your branch. Commit your work. Don't forget to write test cases + for your new feature. + +5. Run the test suite. You have the following options: + + * To run a complete test use the ``setup.py`` script (shown for Python 3):: + + $ python3 setup.py test + + This may create some errors as you probably do not have all Python + versions installed on your system. To restrict it to only installed + version (probably 2.7 and 3.x), pass this options:: + + $ python3 setup.py test -a --skip-missing-interpreters + + * To run a test for a specific Python version, use the + :command:`tox` command, for example, for Python 3.6:: + + $ tox -e py36 + +6. Create a `pull request`_. Describe in the pull request what you did + and why. If you have open questions, ask. + +7. Wait for feedback. If you receive any comments, address these. + +8. After your pull request got accepted, delete your branch. + +9. Use the ``clean`` command to remove build and test files and folders:: - python setup.py clean + $ python setup.py clean .. _pull request: https://github.com/k-bx/python-semver/pulls .. _pytest: http://pytest.org/ -.. _tox: http://tox.testrun.org/ +.. _tox: https://tox.readthedocs.org/ +.. _Semantic Versioning: https://semver.org From 5fb101191ad7b7c0104a6372a29ac75da04771f0 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 2 Oct 2019 15:29:05 +0200 Subject: [PATCH 016/219] Fix #148: Remove & replace python setup.py test (#149) As tox discouraged using "python setup.py test", we shouldn't recommend that in our documentation. Replacing it with "tox". --- docs/development.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/development.rst b/docs/development.rst index ed0bdf89..db15619e 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -49,17 +49,17 @@ We recommend to use the following workflow if you would like to contribute: 4. Work on your branch. Commit your work. Don't forget to write test cases for your new feature. -5. Run the test suite. You have the following options: +5. Run the test suite. You can decide to run the complete test suite or + only part of it: - * To run a complete test use the ``setup.py`` script (shown for Python 3):: + * To run all tests, use:: - $ python3 setup.py test + $ tox - This may create some errors as you probably do not have all Python - versions installed on your system. To restrict it to only installed - version (probably 2.7 and 3.x), pass this options:: + If you have not all Python interpreters installed on your system + it will probably give you some errors. To avoid such errors, use:: - $ python3 setup.py test -a --skip-missing-interpreters + $ tox --skip-missing-interpreters * To run a test for a specific Python version, use the :command:`tox` command, for example, for Python 3.6:: From e3f62e13ab76336cd88bbf0f81ff7eb6c72069b2 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 2 Oct 2019 15:29:29 +0200 Subject: [PATCH 017/219] Fix #136: add testsuite to tar ball (#151) --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 926cc7b1..9ed3b402 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include README.rst include LICENSE.txt +include test_*.py From 1f0b21c8ee76bb6187710b03962d352c114b89c0 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 2 Oct 2019 15:31:01 +0200 Subject: [PATCH 018/219] Improve coverage (#150) --- .coveragerc | 1 + test_semver.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.coveragerc b/.coveragerc index 0b2e8f7a..7ed871ce 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,3 +7,4 @@ branch = True [report] exclude_lines = pragma: no cover + if __name__ == .__main__.: diff --git a/test_semver.py b/test_semver.py index 45ec3405..b694296d 100644 --- a/test_semver.py +++ b/test_semver.py @@ -316,6 +316,10 @@ def test_should_get_max(): assert max_ver('3.4.5', '4.0.2') == '4.0.2' +def test_should_get_max_same(): + assert max_ver('3.4.5', '3.4.5') == '3.4.5' + + def test_should_get_min(): assert min_ver('3.4.5', '4.0.2') == '3.4.5' From 8ee92b498db01ad02833322e88bbb8fe6884d132 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 2 Oct 2019 16:57:52 +0200 Subject: [PATCH 019/219] Fix #135, convert prerelease and build to string (#140) * Fix #135, convert prerelease and build to string Creating VersionInfo(3, 2, 1, 1) succeeds, but fails later when comparing it with another VersionInfo instance (for example, v1 < v2): ``` def split_key(key): > return [convert(c) for c in key.split('.')] E AttributeError: 'int' object has no attribute 'split' ``` * Force major, minor, and patch as int To allow VersionInfo(10) > VersionInfo('2') == True * Enhance test_should_compare_prerelease_and_build_with_numbers Add example from #135 * Simplify test case, thanks to Alexander Co-authored-by: Alexander Grund * Split test case and check types Co-authored-by: Alexander Grund * Move assert line * Move assert line to test_should_be_able_to_use_strings_as_major_minor_patch * Add additional assert, comparing VersionInfo created with strings and with integers Co-authored-by: Alexander Grund * Integrate tests from Alex, many thanks! Co-authored-by: Alexander Grund * Remove v variable to silence flake8 --- semver.py | 10 +++++----- test_semver.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/semver.py b/semver.py index ea0ac81b..93cde842 100644 --- a/semver.py +++ b/semver.py @@ -99,11 +99,11 @@ class VersionInfo(object): __slots__ = ('_major', '_minor', '_patch', '_prerelease', '_build') def __init__(self, major, minor=0, patch=0, prerelease=None, build=None): - self._major = major - self._minor = minor - self._patch = patch - self._prerelease = prerelease - self._build = build + self._major = int(major) + self._minor = int(minor) + self._patch = int(patch) + self._prerelease = None if prerelease is None else str(prerelease) + self._build = None if build is None else str(build) @property def major(self): diff --git a/test_semver.py b/test_semver.py index b694296d..fee77f84 100644 --- a/test_semver.py +++ b/test_semver.py @@ -546,3 +546,37 @@ def test_immutable_unknown_attribute(version): def test_version_info_should_be_iterable(version): assert tuple(version) == (version.major, version.minor, version.patch, version.prerelease, version.build) + + +def test_should_compare_prerelease_and_build_with_numbers(): + assert VersionInfo(major=1, minor=9, patch=1, prerelease=1, build=1) < \ + VersionInfo(major=1, minor=9, patch=1, prerelease=2, build=1) + assert VersionInfo(1, 9, 1, 1, 1) < VersionInfo(1, 9, 1, 2, 1) + assert VersionInfo('2') < VersionInfo(10) + assert VersionInfo('2') < VersionInfo('10') + + +def test_should_be_able_to_use_strings_as_major_minor_patch(): + v = VersionInfo('1', '2', '3') + assert isinstance(v.major, int) + assert isinstance(v.minor, int) + assert isinstance(v.patch, int) + assert v.prerelease is None + assert v.build is None + assert VersionInfo('1', '2', '3') == VersionInfo(1, 2, 3) + + +def test_using_non_numeric_string_as_major_minor_patch_throws(): + with pytest.raises(ValueError): + VersionInfo('a') + with pytest.raises(ValueError): + VersionInfo(1, 'a') + with pytest.raises(ValueError): + VersionInfo(1, 2, 'a') + + +def test_should_be_able_to_use_integers_as_prerelease_build(): + v = VersionInfo(1, 2, 3, 4, 5) + assert isinstance(v.prerelease, str) + assert isinstance(v.build, str) + assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, '4', '5') From 161713f680675f635072ed388f48634e85bdf390 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 5 Oct 2019 17:16:17 +0200 Subject: [PATCH 020/219] Fix #154: Improve Description on PyPI (#155) * Fix #154: Improve Description * Improve README and describe a short session with parsing a version string and access the different parts of a version. * Include README (=Quickstart) as part of the documentation. * Add missing docstrings for properties * Correct HTML theme --- README.rst | 81 ++++++++++++++++++++++++++++++++++++++++++++----- docs/index.rst | 36 +--------------------- docs/readme.rst | 2 ++ docs/usage.rst | 2 +- semver.py | 5 +++ 5 files changed, 83 insertions(+), 43 deletions(-) create mode 100644 docs/readme.rst diff --git a/README.rst b/README.rst index aa81944b..6833b3b4 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,81 @@ -Semver |latest-version| -======================= +Quickstart +========== -|build-status| |python-support| |downloads| |license| |docs| +.. teaser-begin A Python module for `semantic versioning`_. Simplifies comparing versions. +|build-status| |python-support| |downloads| |license| |docs| + +.. teaser-end + + +The module follows the ``MAJOR.MINOR.PATCH`` style: + +* ``MAJOR`` version when you make incompatible API changes, +* ``MINOR`` version when you add functionality in a backwards compatible manner, and +* ``PATCH`` version when you make backwards compatible bug fixes. + +Additional labels for pre-release and build metadata are supported. + +To import this library, use: + +.. code-block:: python + + >>> import semver + +Working with the library is quite straightforward. To turn a version string into the +different parts, use the :func:`semver.parse` function: + +.. code-block:: python + + >>> ver = semver.parse('1.2.3-pre.2+build.4') + >>> ver['major'] + 1 + >>> ver['minor'] + 2 + >>> ver['patch'] + 3 + >>> ver['prerelease'] + 'pre.2' + >>> ver['build'] + 'build.5' + +To raise parts of a version, there are a couple of functions available for +you. The :func:`semver.parse_version_info` function converts a version string +into a :class:`semver.VersionInfo` class. The function +:func:`semver.VersionInfo.bump_major` leaves the original object untouched, but +returns a new :class:`semver.VersionInfo` instance with the raised major part: + +.. code-block:: python + + >>> ver = semver.parse_version_info("3.4.5") + >>> ver.bump_major() + VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) + +It is allowed to concatenate different "bump functions": + +.. code-block:: python + + >>> ver.bump_major().bump_minor() + VersionInfo(major=4, minor=0, patch=1, prerelease=None, build=None) + +To compare two versions, semver provides the :func:`semver.compare` function. +The return value indicates the relationship between the first and second +version: + +.. code-block:: python + + >>> semver.compare("1.0.0", "2.0.0") + -1 + >>> semver.compare("2.0.0", "1.0.0") + 1 + >>> semver.compare("2.0.0", "2.0.0") + 0 + + +There are other functions to discover. Read on! + .. |latest-version| image:: https://img.shields.io/pypi/v/semver.svg :alt: Latest version on PyPI @@ -25,7 +96,3 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. :target: http://python-semver.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. _semantic versioning: http://semver.org/ - -Documentation -------------- -|docs| diff --git a/docs/index.rst b/docs/index.rst index 1c15abab..ecf160d9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,52 +1,18 @@ Semver |version| -- Semantic Versioning ======================================= -|build-status| |latest-version| |python-support| |downloads| |license| - -.. python-semver documentation master file, created by - sphinx-quickstart on Tue May 1 16:51:19 2018. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -A Python module for `semantic versioning`_. Simplifies comparing versions. - -The module follows the ``MAJOR.MINOR.PATCH`` style: - -* ``MAJOR`` version when you make incompatible API changes, -* ``MINOR`` version when you add functionality in a backwards compatible manner, and -* ``PATCH`` version when you make backwards compatible bug fixes. - -Additional labels for pre-release and build metadata are supported. - - .. toctree:: :maxdepth: 2 :caption: Contents + readme install usage development api -.. |latest-version| image:: https://img.shields.io/pypi/v/semver.svg - :alt: Latest version on PyPI - :target: https://pypi.python.org/pypi/semver -.. |build-status| image:: https://travis-ci.org/k-bx/python-semver.svg?branch=master - :alt: Build status - :target: https://travis-ci.org/k-bx/python-semver -.. |python-support| image:: https://img.shields.io/pypi/pyversions/semver.svg - :target: https://pypi.python.org/pypi/semver - :alt: Python versions -.. |downloads| image:: https://img.shields.io/pypi/dm/semver.svg - :alt: Monthly downloads from PyPI - :target: https://pypi.python.org/pypi/semver -.. |license| image:: https://img.shields.io/pypi/l/semver.svg - :alt: Software license - :target: https://github.com/k-bx/python-semver/blob/master/LICENSE.txt -.. _semantic versioning: http://semver.org/ - Indices and Tables ================== diff --git a/docs/readme.rst b/docs/readme.rst new file mode 100644 index 00000000..0aa732a1 --- /dev/null +++ b/docs/readme.rst @@ -0,0 +1,2 @@ +.. include:: ../README.rst + diff --git a/docs/usage.rst b/docs/usage.rst index 795cb3ae..c2a80a73 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -235,7 +235,7 @@ If you need to convert some types into other, refer to :ref:`sec.convert.version Comparing Versions through an Expression ---------------------------------------- +---------------------------------------- If you need a more fine-grained approach of comparing two versions, use the :func:`semver.match` function. It expects two arguments: diff --git a/semver.py b/semver.py index 93cde842..ea02a68d 100644 --- a/semver.py +++ b/semver.py @@ -107,6 +107,7 @@ def __init__(self, major, minor=0, patch=0, prerelease=None, build=None): @property def major(self): + """The major part of a version""" return self._major @major.setter @@ -115,6 +116,7 @@ def major(self, value): @property def minor(self): + """The minor part of a version""" return self._minor @minor.setter @@ -123,6 +125,7 @@ def minor(self, value): @property def patch(self): + """The patch part of a version""" return self._patch @patch.setter @@ -131,6 +134,7 @@ def patch(self, value): @property def prerelease(self): + """The prerelease part of a version""" return self._prerelease @prerelease.setter @@ -139,6 +143,7 @@ def prerelease(self, value): @property def build(self): + """The build part of a version""" return self._build @build.setter From cc050440178e117128c29c29ccd2df2bc52771b9 Mon Sep 17 00:00:00 2001 From: Karol Date: Sat, 5 Oct 2019 17:16:38 +0200 Subject: [PATCH 021/219] Fix #119: Invoke clean from distutils before running our code (#152) --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9deec889..19442380 100755 --- a/setup.py +++ b/setup.py @@ -5,6 +5,10 @@ from os.path import dirname, join from setuptools import setup from setuptools.command.test import test as TestCommand +try: + from setuptools.command.clean import clean as CleanCommand +except ImportError: + from distutils.command.clean import clean as CleanCommand from shlex import split from shutil import rmtree @@ -30,8 +34,9 @@ def run_tests(self): exit(errno) -class Clean(TestCommand): +class Clean(CleanCommand): def run(self): + super().run() delete_in_root = [ 'build', '.cache', From d8da4821856d283deb975c9ba888b97a8c02c2e2 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 5 Oct 2019 17:19:53 +0200 Subject: [PATCH 022/219] Improve doctests (#157) * Introduce conftest.py to add pytest fixture for doctest_namespace See https://docs.pytest.org/en/latest/doctest.html#doctest-namespace-fixture * Remove the "import semver" line in each doctest; this is covered in conftest.py now as a pytest fixture * Extend setup.cfg with --doctest-report ndiff Makes the output a bit nicer in case something goes wrong --- conftest.py | 7 +++++++ semver.py | 32 ++++++++------------------------ setup.cfg | 1 + 3 files changed, 16 insertions(+), 24 deletions(-) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..4f49a137 --- /dev/null +++ b/conftest.py @@ -0,0 +1,7 @@ +import pytest +import semver + + +@pytest.fixture(autouse=True) +def add_semver(doctest_namespace): + doctest_namespace["semver"] = semver diff --git a/semver.py b/semver.py index ea02a68d..d4fbb1dd 100644 --- a/semver.py +++ b/semver.py @@ -49,7 +49,6 @@ def parse(version): if not provided :rtype: dict - >>> import semver >>> ver = semver.parse('3.4.5-pre.2+build.4') >>> ver['major'] 3 @@ -176,7 +175,6 @@ def bump_major(self): :return: new object with the raised major part :rtype: VersionInfo - >>> import semver >>> ver = semver.parse_version_info("3.4.5") >>> ver.bump_major() VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) @@ -190,7 +188,6 @@ def bump_minor(self): :return: new object with the raised minor part :rtype: VersionInfo - >>> import semver >>> ver = semver.parse_version_info("3.4.5") >>> ver.bump_minor() VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) @@ -204,7 +201,6 @@ def bump_patch(self): :return: new object with the raised patch part :rtype: VersionInfo - >>> import semver >>> ver = semver.parse_version_info("3.4.5") >>> ver.bump_patch() VersionInfo(major=3, minor=4, patch=6, prerelease=None, build=None) @@ -219,7 +215,6 @@ def bump_prerelease(self, token='rc'): :return: new object with the raised prerelease part :rtype: str - >>> import semver >>> ver = semver.parse_version_info("3.4.5-rc.1") >>> ver.bump_prerelease() VersionInfo(major=3, minor=4, patch=5, prerelease='rc.2', \ @@ -235,7 +230,6 @@ def bump_build(self, token='build'): :return: new object with the raised build part :rtype: str - >>> import semver >>> ver = semver.parse_version_info("3.4.5-rc.1+build.9") >>> ver.bump_build() VersionInfo(major=3, minor=4, patch=5, prerelease='rc.1', \ @@ -282,14 +276,13 @@ def __hash__(self): def parse(version): """Parse version string to a VersionInfo instance. - >>> from semver import VersionInfo - >>> VersionInfo.parse('3.4.5-pre.2+build.4') + :param version: version string + :return: a :class:`semver.VersionInfo` instance + :rtype: :class:`semver.VersionInfo` + + >>> semver.VersionInfo.parse('3.4.5-pre.2+build.4') VersionInfo(major=3, minor=4, patch=5, \ prerelease='pre.2', build='build.4') - - :param version: version string - :return: a :class:`VersionInfo` instance - :rtype: :class:`VersionInfo` """ return parse_version_info(version) @@ -309,7 +302,6 @@ def parse_version_info(version): :return: a :class:`VersionInfo` instance :rtype: :class:`VersionInfo` - >>> import semver >>> version_info = semver.parse_version_info("3.4.5-pre.2+build.4") >>> version_info.major 3 @@ -385,7 +377,6 @@ def compare(ver1, ver2): zero if ver1 == ver2 and strictly positive if ver1 > ver2 :rtype: int - >>> import semver >>> semver.compare("1.0.0", "2.0.0") -1 >>> semver.compare("2.0.0", "1.0.0") @@ -413,7 +404,6 @@ def match(version, match_expr): :return: True if the expression matches the version, otherwise False :rtype: bool - >>> import semver >>> semver.match("2.0.0", ">=1.0.0") True >>> semver.match("1.0.0", ">1.0.0") @@ -454,7 +444,6 @@ def max_ver(ver1, ver2): :return: the greater version of the two :rtype: :class:`VersionInfo` - >>> import semver >>> semver.max_ver("1.0.0", "2.0.0") '2.0.0' """ @@ -473,7 +462,6 @@ def min_ver(ver1, ver2): :return: the smaller version of the two :rtype: :class:`VersionInfo` - >>> import semver >>> semver.min_ver("1.0.0", "2.0.0") '1.0.0' """ @@ -495,7 +483,6 @@ def format_version(major, minor, patch, prerelease=None, build=None): :return: the formatted string :rtype: str - >>> import semver >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') '3.4.5-pre.2+build.4' """ @@ -529,7 +516,6 @@ def bump_major(version): :return: the raised version string :rtype: str - >>> import semver >>> semver.bump_major("3.4.5") '4.0.0' """ @@ -544,7 +530,6 @@ def bump_minor(version): :return: the raised version string :rtype: str - >>> import semver >>> semver.bump_minor("3.4.5") '3.5.0' """ @@ -559,7 +544,6 @@ def bump_patch(version): :return: the raised version string :rtype: str - >>> import semver >>> semver.bump_patch("3.4.5") '3.4.6' """ @@ -576,7 +560,7 @@ def bump_prerelease(version, token='rc'): :return: the raised version string :rtype: str - >>> bump_prerelease('3.4.5', 'dev') + >>> semver.bump_prerelease('3.4.5', 'dev') '3.4.5-dev.1' """ verinfo = parse(version) @@ -595,7 +579,7 @@ def bump_build(version, token='build'): :return: the raised version string :rtype: str - >>> bump_build('3.4.5-rc.1+build.9') + >>> semver.bump_build('3.4.5-rc.1+build.9') '3.4.5-rc.1+build.10' """ verinfo = parse(version) @@ -613,7 +597,7 @@ def finalize_version(version): :return: the finalized version string :rtype: str - >>> finalize_version('1.2.3-rc.5') + >>> semver.finalize_version('1.2.3-rc.5') '1.2.3' """ verinfo = parse(version) diff --git a/setup.cfg b/setup.cfg index fcaea3c3..ba377b10 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,4 @@ addopts = --cov=semver --cov-report=term-missing --doctest-modules + --doctest-report ndiff From c585f5cb8b9a0d5859a885e94a7e84597a554d67 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 6 Oct 2019 21:26:31 +0200 Subject: [PATCH 023/219] Doc: Clarify Contributing section (#158) * Structure the text with more subsection * Introduce a separate section about "Running the Test Suite" This gives an overview, recommends flake8, and how to run the complete test suite or only a specific test function * Introduce a separate section about "Documenting semver" Gives contributors some helpful advise * Use references (:ref:) to other section This helps to reduce the "big" procedure and make it more readable. * Change wording, correct phrases, clarify text As well as in Python's Zen "Simple is better than complex", this applies to documentation too. * setup.cfg: remove "-q" option Removing it does NOT change the default output. However, you can pass "-v" and run "tox -- -v" to print all test function. This is covered in the documentation now --- docs/api.rst | 2 + docs/development.rst | 175 +++++++++++++++++++++++++++++++++++-------- setup.cfg | 1 - 3 files changed, 144 insertions(+), 34 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index cd8e73b4..0003fefc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,3 +1,5 @@ +.. _api: + API === diff --git a/docs/development.rst b/docs/development.rst index db15619e..be66c14f 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,10 +1,26 @@ Contributing to semver ====================== -Do you want to contribute? Great! We would like to give you some -helpful tips and tricks. -When you make changes to the code, we would greatly appreciate if you -consider the following requirements: +The semver source code is managed using Git and is hosted on GitHub:: + + git clone git://github.com/k-bx/python-semver + + +Reporting Bugs and Feedback +--------------------------- + +If you think you have encountered a bug in semver or have an idea for a new +feature? Great! We like to hear from you. + +First, take the time to look into our GitHub `issues`_ tracker if +this already covered. If not, changes are good that we avoid double work. + + +Fixing Bugs and Implementing New Features +----------------------------------------- + +Before you make changes to the code, we would highly appreciate if you +consider the following general requirements: * Make sure your code adheres to the `Semantic Versioning`_ specification. @@ -15,70 +31,163 @@ consider the following requirements: * Test also for side effects of your new feature and run the complete test suite. -* Document the new feature. -We use `pytest`_ and `tox`_ to run tests against all supported Python -versions. All test dependencies are resolved automatically, apart from -virtualenv, which for the moment you still may have to install manually: +* Document the new feature, see :ref:`doc` for details. -.. code-block:: bash - pip install "virtualenv<14.0.0" # <14.0.0 needed for Python 3.2 only +Modifying the Code +------------------ -We recommend to use the following workflow if you would like to contribute: +We recommend the following workflow: -1. Fork our project on GitHub using this link: +#. Fork our project on GitHub using this link: https://github.com/k-bx/python-semver/fork -2. Clone your forked Git repository (replace ``GITHUB_USER`` with your +#. Clone your forked Git repository (replace ``GITHUB_USER`` with your account name on GitHub):: $ git clone git@github.com:GITHUB_USER/python-semver.git -3. Create a new branch. You can name your branch whatever you like, but we +#. Create a new branch. You can name your branch whatever you like, but we recommend to use some meaningful name. If your fix is based on a existing GitHub issue, add also the number. Good examples would be: - * ``feature/123-improve-foo`` when implementing a new feature - * ``bugfix/123-fix-security-bar`` when dealing with bugfixes + * ``feature/123-improve-foo`` when implementing a new feature in issue 123 + * ``bugfix/234-fix-security-bar`` a bugfixes for issue 234 Use this :command:`git` command:: $ git checkout -b feature/NAME_OF_YOUR_FEATURE -4. Work on your branch. Commit your work. Don't forget to write test cases - for your new feature. +#. Work on your branch. Commit your work. + +#. Write test cases and run the test suite, see :ref:`testsuite` for details. + +#. Create a `pull request`_. Describe in the pull request what you did + and why. If you have open questions, ask. + +#. Wait for feedback. If you receive any comments, address these. -5. Run the test suite. You can decide to run the complete test suite or - only part of it: +#. After your pull request got accepted, delete your branch. - * To run all tests, use:: +#. Use the ``clean`` command to remove build and test files and folders:: + + $ python setup.py clean + + +.. _testsuite: + +Running the Test Suite +---------------------- + +We use `pytest`_ and `tox`_ to run tests against all supported Python +versions. All test dependencies are resolved automatically. + +You can decide to run the complete test suite or only part of it: + +* To run all tests, use:: $ tox - If you have not all Python interpreters installed on your system - it will probably give you some errors. To avoid such errors, use:: + If you have not all Python interpreters installed on your system + it will probably give you some errors (``InterpreterNotFound``). + To avoid such errors, use:: $ tox --skip-missing-interpreters - * To run a test for a specific Python version, use the - :command:`tox` command, for example, for Python 3.6:: + It is possible to use only specific Python versions. Use the ``-e`` + option and one or more abbreviations (``py27`` for Python 2.7, ``py34`` for + Python 3.4 etc.):: - $ tox -e py36 + $ tox -e py34 + $ tox -e py27,py34 -6. Create a `pull request`_. Describe in the pull request what you did - and why. If you have open questions, ask. + To get a complete list, run:: -7. Wait for feedback. If you receive any comments, address these. + $ tox -l -8. After your pull request got accepted, delete your branch. +* To run only a specific test, pytest requires the syntax + ``TEST_FILE::TEST_FUNCTION``. -9. Use the ``clean`` command to remove build and test files and folders:: + For example, the following line tests only the function + :func:`test_immutable_major` in the file :file:`test_semver.py` for all + Python versions:: - $ python setup.py clean + $ tox test_semver.py::test_immutable_major + + By default, pytest prints a dot for each test function only. To + reveal the executed test function, use the following syntax:: + $ tox -- -v + You can combine the specific test function with the ``-e`` option, for + example, to limit the tests for Python 2.7 and 3.6 only:: + + $ tox -e py27,py36 test_semver.py::test_immutable_major + +Our code is checked against `flake8`_ for style guide issues. It is recommended +to run your tests in combination with :command:`flake8`, for example:: + + $ tox -e py27,py36,flake8 + + +.. _doc: + +Documenting semver +------------------ + +Documenting the features of semver is very important. It gives our developers +an overview what is possible with semver, how it "feels", and how it is +used efficiently. + +.. note:: + + To build the documentation locally use the following command:: + + $ tox -e docs + + The built documentation is available in :file:`dist/docs`. + + +A new feature is *not* complete if it isn't proberly documented. A good +documentation includes: + + * **A docstring** + + Each docstring contains a summary line, a linebreak, the description + of its arguments in `Sphinx style`_, and an optional doctest. + The docstring is extracted and reused in the :ref:`api` section. + An appropriate docstring should look like this:: + + def compare(ver1, ver2): + """Compare two versions + + :param ver1: version string 1 + :param ver2: version string 2 + :return: The return value is negative if ver1 < ver2, + zero if ver1 == ver2 and strictly positive if ver1 > ver2 + :rtype: int + + >>> semver.compare("1.0.0", "2.0.0") + -1 + >>> semver.compare("2.0.0", "1.0.0") + 1 + >>> semver.compare("2.0.0", "2.0.0") + 0 + """ + + * **The documentation** + + A docstring is good, but in most cases it's too dense. Describe how + to use your new feature in our documentation. Here you can give your + readers more examples, describe it in a broader context or show + edge cases. + + +.. _flake8: https://flake8.readthedocs.io +.. _issues: https://github.com/k-bx/python-semver/issues .. _pull request: https://github.com/k-bx/python-semver/pulls .. _pytest: http://pytest.org/ -.. _tox: https://tox.readthedocs.org/ .. _Semantic Versioning: https://semver.org +.. _Sphinx style: https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html +.. _tox: https://tox.readthedocs.org/ diff --git a/setup.cfg b/setup.cfg index ba377b10..7967d292 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,6 @@ [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ addopts = - -q --ignore=.eggs/ --no-cov-on-fail --cov=semver From 8a1ae0ddb217b05963b4909860d8f54315d6d0fc Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 8 Oct 2019 20:44:57 +0200 Subject: [PATCH 024/219] Fix #159: Improve Changelog (#162) * Fix #159: Improve Changelog * Rename CHANGELOG -> CHANGELOG.rst * Adapt changelog: * Group the issues into the sections "Bug Fixes", "Features", "Removals", * Try to sort issue number from low to high to make it consistent * Prepare version 2.9.0 as WIP * Include CHANGELOG.rst into documentation (through docs/changelog.rst) * Introduce "extlinks" to docs/conf.py to allow to link to GitHub issues and pull requests. When the text uses :gh:`123` it is automatically converted into a link that points to the corresponding GitHub issue (same for pull requests with :pr:`234`). * Integrate all entries of 2.8.2 into 2.9.0 * Set 2.8.2 as unreleased * Sort all entries of 2.8.2 into 2.9.0 --- CHANGELOG => CHANGELOG.rst | 158 ++++++++++++++++++++++++++----------- docs/changelog.rst | 1 + docs/conf.py | 8 ++ docs/index.rst | 2 +- 4 files changed, 122 insertions(+), 47 deletions(-) rename CHANGELOG => CHANGELOG.rst (50%) create mode 100644 docs/changelog.rst diff --git a/CHANGELOG b/CHANGELOG.rst similarity index 50% rename from CHANGELOG rename to CHANGELOG.rst index f794629a..37dbf7f1 100644 --- a/CHANGELOG +++ b/CHANGELOG.rst @@ -2,41 +2,99 @@ Change Log ########## -Python SemVer library -##################### All notable changes to this code base will be documented in this file, in every released version. -Version 2.8.x -============= + +Version 2.9.0 (WIP) +=================== :Released: 20yy-mm-dd :Maintainer: Sébastien Celles -* Issue #102 (PR #...). Fix comparison between VersionInfo and tuple -* Issue #103 (PR #...). Disallow comparison between VersionInfo and string (and int) +Features +-------- + +* :gh:`85` (:pr:`147`, :pr:`154`): Improved contribution section +* :gh:`104` (:pr:`125`): Added iterator to :func:`semver.VersionInfo` +* :gh:`112`, :gh:`113`: Added Python 3.7 support +* :pr:`120`: Improved test_immutable function with properties +* :pr:`125`: Created :file:`setup.cfg` for pytest and tox +* :gh:`126` (:pr:`127`): Added target for documentation in :file:`tox.ini` +* :gh:`142` (:pr:`143`): Improved usage section +* :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` +* :pr:`157`: Introduce :file:`conftest.py` to improve doctests + +Bug Fixes +--------- + +* :gh:`102`: Fixed comparison between VersionInfo and tuple +* :gh:`103`: Disallow comparison between VersionInfo and string (and int) +* :gh:`121` (:pr:`122`): Use python3 instead of python3.4 in :file:`tox.ini` +* :pr:`123`: Improved :func:`__repr__` and derive class name from :func:`type` +* :gh:`128` (:pr:`129`): Fixed wrong datatypes in docstring for :func:`semver.format_version` +* :gh:`135` (:pr:`140`): Converted prerelease and build to string +* :gh:`136` (:pr:`151`): Added testsuite to tarball +* :gh:`154` (:pr:`155`): Improved README description + +Removals +-------- + +* :gh:`111` (:pr:`110`): Droped Python 3.3 +* :gh:`148` (:pr:`149`): Removed and replaced ``python setup.py test`` + + +Version 2.8.2 +============= +:Released: 2019-05-19 +:Maintainer: Sébastien Celles + +Skipped, not released. + Version 2.8.1 ============= :Released: 2018-07-09 :Maintainer: Sébastien Celles -* Issue #77 (PR #47). Convert multiple tests into pytest.mark.parametrize -* Issue #89 (PR #90). Add doctests. -* Issue #40 (PR #88). Add a static parse method to VersionInfo -* Issue #87 #94 (PR #93). Remove named tuple inheritance. Fix bad rendering in Pandas DataFrame -* Issue #96 (PR #97). Make VersionInfo immutable -* Issue #98 (PR #99). prerelease and build set to None by default +Features +-------- + +* :gh:`40` (:pr:`88`): Added a static parse method to VersionInfo +* :gh:`77` (:pr:`47`): Converted multiple tests into pytest.mark.parametrize +* :gh:`87`, :gh:`94` (:pr:`93`): Removed named tuple inheritance. +* :gh:`89` (:pr:`90`): Added doctests. + +Bug Fixes +--------- + +* :gh:`98` (:pr:`99`): Set prerelease and build to None by default +* :gh:`96` (:pr:`97`): Made VersionInfo immutable + Version 2.8.0 ============= :Released: 2018-05-16 :Maintainer: Sébastien Celles -* Issue #76 (PR #80). Remove Python 2.6 compatibility -* Issue #79 (PR #81 #84). Define and improve a release procedure file -* Issue #72 #73 (PR #75). Implements __str__ and __hash__ -* Issue #82 (PR #83). Rename test.py to test_semver.py so py.test can autodiscover test file + +Changes +------- + +* :gh:`82` (:pr:`83`): Renamed :file:`test.py` to :file:`test_semver.py` so + py.test can autodiscover test file + +Additions +--------- + +* :gh:`79` (:pr:`81`, :pr:`84`): Defined and improve a release procedure file +* :gh:`72`, :gh:`73` (:pr:`75`): Implemented :func:`__str__` and :func:`__hash__` + +Removals +-------- + +* :gh:`76` (:pr:`80`): Removed Python 2.6 compatibility + Version 2.7.9 ============= @@ -44,7 +102,12 @@ Version 2.7.9 :Released: 2017-09-23 :Maintainer: Kostiantyn Rybnikov -* Issue #65 (PR #66). Add finalize_version function + +Additions +--------- + +* :gh:`65` (:pr:`66`): Added :func:`semver.finalize_version` function. + Version 2.7.8 ============= @@ -52,7 +115,8 @@ Version 2.7.8 :Released: 2017-08-25 :Maintainer: Kostiantyn Rybnikov -* PR #62. Support custom default names for pre and build +* :gh:`62`: Support custom default names for pre and build + Version 2.7.7 ============= @@ -60,8 +124,9 @@ Version 2.7.7 :Released: 2017-05-25 :Maintainer: Kostiantyn Rybnikov -* Issue #54 (PR #55) Add comparision between VersionInfo objects -* PR #56. Add support for Python 3.6 +* :gh:`54` (:pr:`55`): Added comparision between VersionInfo objects +* :pr:`56`: Added support for Python 3.6 + Version 2.7.2 ============= @@ -72,14 +137,15 @@ Version 2.7.2 Additions --------- -* Fix issue #37 (Remove trailing zeros from prelease doesn't allow to - parse 0 pre-release version) -* Add ‘parse_version_info’ to parse a version string to a version info - tuple. +* Added :func:`semver.parse_version_info` to parse a version string to a + version info tuple. Bug Fixes --------- - + +* :gh:`37`: Removed trailing zeros from prelease doesn't allow to + parse 0 pre-release version + * Refine parsing to conform more strictly to SemVer 2.0.0. SemVer 2.0.0 specification §9 forbids leading zero on identifiers in @@ -115,7 +181,7 @@ Additions Changes ------- -* Make separate builds for tests on Travis CI. +* Made separate builds for tests on Travis CI. Version 2.4.2 @@ -127,13 +193,13 @@ Version 2.4.2 Changes ------- -* Migrate README document to reStructuredText format. +* Migrated README document to reStructuredText format. -* Use Setuptools for distribution management. +* Used Setuptools for distribution management. -* Migrate test cases to Py.test. +* Migrated test cases to Py.test. -* Add configuration for Tox test runner. +* Added configuration for Tox test runner. Version 2.4.1 @@ -145,7 +211,7 @@ Version 2.4.1 Additions --------- -* [GitHub issue #23] Compare build component of a version. +* :gh:`23`: Compared build component of a version. Version 2.4.0 @@ -157,7 +223,7 @@ Version 2.4.0 Bug Fixes --------- -* [GitHub issue #21] Compare alphanumeric components correctly. +* :gh:`21`: Compared alphanumeric components correctly. Version 2.3.1 @@ -169,7 +235,7 @@ Version 2.3.1 Additions --------- -* Declare granted license name in distribution metadata. +* Declared granted license name in distribution metadata. Version 2.3.0 @@ -181,7 +247,7 @@ Version 2.3.0 Additions --------- -* Add functions to increment prerelease and build components in a +* Added functions to increment prerelease and build components in a version. @@ -194,7 +260,7 @@ Version 2.2.1 Bug Fixes --------- -* Correct comparison when any component includes zero. +* Corrected comparison when any component includes zero. Version 2.2.0 @@ -220,7 +286,7 @@ Version 2.1.2 Bug Fixes --------- -* Restore current README document to distribution manifest. +* Restored current README document to distribution manifest. Version 2.1.1 @@ -232,7 +298,7 @@ Version 2.1.1 Bug Fixes --------- -* Remove absent document from distribution manifest. +* Removed absent document from distribution manifest. Version 2.1.0 @@ -244,23 +310,23 @@ Version 2.1.0 Additions --------- -* Document installation instructions. +* Documented installation instructions. -* Document project home page. +* Documented project home page. -* Add function to format a version string from components. +* Added function to format a version string from components. -* Add functions to increment specific components in a version. +* Added functions to increment specific components in a version. Changes ------- -* Migrate README document to Markdown format. +* Migrated README document to Markdown format. Bug Fixes --------- -* Correct code examples in README document. +* Corrected code examples in README document. Version 2.0.2 @@ -272,9 +338,9 @@ Version 2.0.2 Additions --------- -* Add configuration for Travis continuous integration. +* Added configuration for Travis continuous integration. -* Explicitly declare supported Python versions. +* Explicitly declared supported Python versions. Version 2.0.1 @@ -286,7 +352,7 @@ Version 2.0.1 Bug Fixes --------- -* [GitHub issue #9] Correct comparison of equal version strings. +* :gh:`9`: Fixed comparison of equal version strings. Version 2.0.0 diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 00000000..565b0521 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py index c2b49f49..22f02b70 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,6 +35,7 @@ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', + 'sphinx.ext.extlinks', ] # Add any paths that contain templates here, relative to this directory. @@ -80,6 +81,13 @@ # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False +# Markup to shorten external links +# See https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html +extlinks = {'gh': ('https://github.com/k-bx/python-semver/issues/%s', + '#'), + 'pr': ('https://github.com/k-bx/python-semver/pull/%s', + 'PR #'), + } # -- Options for HTML output ---------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index ecf160d9..8c9153dd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,7 @@ Semver |version| -- Semantic Versioning usage development api - + changelog Indices and Tables From 1f7778aa230f9a54d0989dc4b45bca357f04e16c Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 8 Oct 2019 21:26:19 +0200 Subject: [PATCH 025/219] Add support state for Python 2.7/3.4 for #161 (#163) --- README.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.rst b/README.rst index 6833b3b4..083bb39e 100644 --- a/README.rst +++ b/README.rst @@ -18,6 +18,23 @@ The module follows the ``MAJOR.MINOR.PATCH`` style: Additional labels for pre-release and build metadata are supported. + +.. warning:: + + Major version 3.0.0 of semver will remove support for Python 2.7 and 3.4. + + As anything comes to an end, this project will focus on Python 3.x. + New features and bugfixes will be integrated only into the 3.x.y branch + of semver. + + The last version of semver which supports Python 2.7 and 3.4 will be + 2.9.x. However, keep in mind, version 2.9.x is frozen: no new + features nor backports will be integrated. + + We recommend to upgrade your workflow to Python 3.x to gain support, + bugfixes, and new features. + + To import this library, use: .. code-block:: python From 5d0dfafaa8ba27f5450621f66ec98aac9848bc8e Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 13 Oct 2019 20:58:34 +0200 Subject: [PATCH 026/219] Fix #167: Introdue SEMVER_SPEC_VERSION (#168) Global constant, defines the implemented semantic version specification --- CHANGELOG.rst | 1 + docs/usage.rst | 11 +++++++++++ semver.py | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 37dbf7f1..d12ec79c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Features * :gh:`142` (:pr:`143`): Improved usage section * :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` * :pr:`157`: Introduce :file:`conftest.py` to improve doctests +* :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` Bug Fixes --------- diff --git a/docs/usage.rst b/docs/usage.rst index c2a80a73..b6699956 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -11,6 +11,17 @@ Each type can be converted into the other, if the minimum requirements are met. +Knowing the Implemented semver.org Version +------------------------------------------ + +The semver.org is the authorative specification of how semantical versioning is +definied. To know which version of semver.org is implemented in the semver +libary, use the following constant:: + + >>> semver.SEMVER_SPEC_VERSION + '2.0.0' + + Creating a Version ------------------ diff --git a/semver.py b/semver.py index d4fbb1dd..4cb8d44d 100644 --- a/semver.py +++ b/semver.py @@ -35,6 +35,10 @@ _LAST_NUMBER = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+') +#: Contains the implemented semver.org version of the spec +SEMVER_SPEC_VERSION = "2.0.0" + + if not hasattr(__builtins__, 'cmp'): def cmp(a, b): return (a > b) - (a < b) From 4bc39a2df05d78419b903250a28e569a11e16d24 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 13 Oct 2019 21:00:47 +0200 Subject: [PATCH 027/219] Rework .gitignore (#166) * Taken most entries from https://github.com/github/gitignore/blob/master/Python.gitignore but adapt it this project. * Update CHANGELOG.rst --- .gitignore | 87 ++++++++++++++++++++++++++++++++++++++++++--------- CHANGELOG.rst | 1 + 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 03729020..f0636016 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,73 @@ -/.cache -/.coverage -/.emacs-project -/.idea -/.tox -/.venv* - -/*.egg-info -/.eggs -/build -/dist -/MANIFEST - -*.py[co] +# Files +*.patch +*.diff +*.kate-swp + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +.pytest_cache/ + +# Distribution / packaging +.cache +.emacs-project +.installed.cfg +.idea/ +*.egg +*.egg-info/ +.eggs/ +.Python +.tmp/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Environment +env*/ +venv*/ +.env* +.venv* + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Spyder project settings +.spyderproject +.spyproject + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +coverage.xml +*,cover +.hypothesis/ +.pytest_cache/ + +# Sphinx documentation +doc/_build/ + +# PyBuilder +target/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d12ec79c..c5c78cea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Features * :gh:`142` (:pr:`143`): Improved usage section * :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` * :pr:`157`: Introduce :file:`conftest.py` to improve doctests +* :pr:`166`: Reworked :file:`.gitignore` file * :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` Bug Fixes From b2ca4e58c2653011f24db6168b2b5d1f5fcc81af Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 13 Oct 2019 21:02:32 +0200 Subject: [PATCH 028/219] Improve code coverage (#165) * Add test for (private) _increment_string() function * Adapt .coveragerc: * Introduce option precision * Disable coverage for cmp * Update CHANGELOG.rst --- .coveragerc | 3 +++ CHANGELOG.rst | 1 + test_semver.py | 10 ++++++++++ 3 files changed, 14 insertions(+) diff --git a/.coveragerc b/.coveragerc index 7ed871ce..4903985c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,9 @@ source = semver branch = True [report] +show_missing = true +precision = 1 exclude_lines = pragma: no cover if __name__ == .__main__.: + if not hasattr\(__builtins__, .cmp.\): diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c5c78cea..feb57a1e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Features * :gh:`142` (:pr:`143`): Improved usage section * :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` * :pr:`157`: Introduce :file:`conftest.py` to improve doctests +* :pr:`165`: Improved code coverage * :pr:`166`: Reworked :file:`.gitignore` file * :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` diff --git a/test_semver.py b/test_semver.py index fee77f84..fa45f0cc 100644 --- a/test_semver.py +++ b/test_semver.py @@ -23,6 +23,16 @@ ] +@pytest.mark.parametrize("string,expected", [ + ("rc", "rc"), + ("rc.1", "rc.2"), + ("2x", "3x"), +]) +def test_should_private_increment_string(string, expected): + from semver import _increment_string + assert _increment_string(string) == expected + + @pytest.fixture def version(): return VersionInfo(major=1, minor=2, patch=3, From 4887ecf80fab577f2461c236312aa0e891da86ec Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 13 Oct 2019 21:35:03 +0200 Subject: [PATCH 029/219] Fix #59: Implement command line interface (#164) * Extend setup.py with entry_point key and point to semver.main. The script is named "pysemver" * Introduce 3 new functions: * createparser: creates and returns an argparse.ArgumentParser instance * process: process the CLI arguments and call the requested actions * main: entry point for the application script * Add test cases * sort import lines of semver functions/class with isort tool * sort list of SEMVERFUNCS variable * Extend documentation * Add sphinx-argparse as a doc requirement * Include new cli.rst file which (self)documents the arguments of the semver script with the help of sphinx-argparse * Extend extensions variable in conf.py to be able to use the sphinx-argparse module * Update CHANGELOG.rst --- CHANGELOG.rst | 2 + docs/cli.rst | 45 +++++++++++++++++++ docs/conf.py | 1 + docs/index.rst | 1 + docs/requirements.txt | 1 + semver.py | 100 ++++++++++++++++++++++++++++++++++++++++- setup.py | 3 ++ test_semver.py | 101 ++++++++++++++++++++++++++++++++++-------- 8 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 docs/cli.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index feb57a1e..aa35c93a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,7 @@ Version 2.9.0 (WIP) Features -------- +* :gh:`59` (:pr:`164`): Implemented a command line interface * :gh:`85` (:pr:`147`, :pr:`154`): Improved contribution section * :gh:`104` (:pr:`125`): Added iterator to :func:`semver.VersionInfo` * :gh:`112`, :gh:`113`: Added Python 3.7 support @@ -28,6 +29,7 @@ Features * :pr:`166`: Reworked :file:`.gitignore` file * :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` + Bug Fixes --------- diff --git a/docs/cli.rst b/docs/cli.rst new file mode 100644 index 00000000..9f21bfeb --- /dev/null +++ b/docs/cli.rst @@ -0,0 +1,45 @@ +CLI +=== + +The library provides also a command line interface. This allows to include +the functionality of semver into shell scripts. + +Using the pysemver Script +------------------------- + +The script name is :command:`pysemver` and provides the subcommands ``bump`` +and ``compare``. + +To bump a version, you pass the name of the part (major, minor, patch, prerelease, or +build) and the version string, for example:: + + $ pysemver bump major 1.2.3 + 2.0.0 + $ pysemver bump minor 1.2.3 + 1.3.0 + +If you pass a version string which is not a valid semantical version, you get +an error message:: + + $ pysemver bump build 1.5 + ERROR 1.5 is not valid SemVer string + +To compare two versions, use the ``compare`` subcommand. The result is + +* ``-1`` if first version is smaller than the second version, +* ``0`` if both are the same, +* ``1`` if the first version is greater than the second version. + +For example:: + + $ pysemver compare 1.2.3 2.4.0 + + +.. _interface: + +Interface +--------- + +.. argparse:: + :ref: semver.createparser + :prog: pysemver diff --git a/docs/conf.py b/docs/conf.py index 22f02b70..c4afdf8e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,6 +36,7 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', 'sphinx.ext.extlinks', + 'sphinxarg.ext', ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/index.rst b/docs/index.rst index 8c9153dd..140ebad0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ Semver |version| -- Semantic Versioning readme install usage + cli development api changelog diff --git a/docs/requirements.txt b/docs/requirements.txt index 383acaaa..28467ce6 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ # requirements file for documentation sphinx sphinx_rtd_theme +sphinx-argparse diff --git a/semver.py b/semver.py index 4cb8d44d..bfe1f73c 100644 --- a/semver.py +++ b/semver.py @@ -1,11 +1,13 @@ """ Python helper for Semantic Versioning (http://semver.org/) """ +from __future__ import print_function +import argparse import collections -import re - from functools import wraps +import re +import sys __version__ = '2.8.2' @@ -608,6 +610,100 @@ def finalize_version(version): return format_version(verinfo['major'], verinfo['minor'], verinfo['patch']) +def createparser(): + """Create an :class:`argparse.ArgumentParser` instance + + :return: parser instance + :rtype: :class:`argparse.ArgumentParser` + """ + parser = argparse.ArgumentParser(prog=__package__, + description=__doc__) + s = parser.add_subparsers() + + # create compare subcommand + parser_compare = s.add_parser("compare", + help="Compare two versions" + ) + parser_compare.set_defaults(which="compare") + parser_compare.add_argument("version1", + help="First version" + ) + parser_compare.add_argument("version2", + help="Second version" + ) + + # create bump subcommand + parser_bump = s.add_parser("bump", + help="Bumps a version" + ) + parser_bump.set_defaults(which="bump") + sb = parser_bump.add_subparsers(title="Bump commands", + dest="bump") + + # Create subparsers for the bump subparser: + for p in (sb.add_parser("major", + help="Bump the major part of the version"), + sb.add_parser("minor", + help="Bump the minor part of the version"), + sb.add_parser("patch", + help="Bump the patch part of the version"), + sb.add_parser("prerelease", + help="Bump the prerelease part of the version"), + sb.add_parser("build", + help="Bump the build part of the version")): + p.add_argument("version", + help="Version to raise" + ) + + return parser + + +def process(args): + """Process the input from the CLI + + :param args: The parsed arguments + :type args: :class:`argparse.Namespace` + :param parser: the parser instance + :type parser: :class:`argparse.ArgumentParser` + :return: result of the selected action + :rtype: str + """ + if args.which == "bump": + maptable = {'major': 'bump_major', + 'minor': 'bump_minor', + 'patch': 'bump_patch', + 'prerelease': 'bump_prerelease', + 'build': 'bump_build', + } + ver = parse_version_info(args.version) + # get the respective method and call it + func = getattr(ver, maptable[args.bump]) + return str(func()) + + elif args.which == "compare": + return str(compare(args.version1, args.version2)) + + +def main(cliargs=None): + """Entry point for the application script + + :param list cliargs: Arguments to parse or None (=use :class:`sys.argv`) + :return: error code + :rtype: int + """ + try: + parser = createparser() + args = parser.parse_args(args=cliargs) + # args.parser = parser + result = process(args) + print(result) + return 0 + + except (ValueError, TypeError) as err: + print("ERROR", err, file=sys.stderr) + return 2 + + if __name__ == "__main__": import doctest doctest.testmod() diff --git a/setup.py b/setup.py index 19442380..2003c19f 100755 --- a/setup.py +++ b/setup.py @@ -107,4 +107,7 @@ def read_file(filename): 'clean': Clean, 'test': Tox, }, + entry_points={ + 'console_scripts': ['pysemver = semver:main'], + } ) diff --git a/test_semver.py b/test_semver.py index fa45f0cc..0fb78981 100644 --- a/test_semver.py +++ b/test_semver.py @@ -1,25 +1,30 @@ +from argparse import Namespace import pytest # noqa -from semver import compare -from semver import match -from semver import parse -from semver import format_version -from semver import bump_major -from semver import bump_minor -from semver import bump_patch -from semver import bump_prerelease -from semver import bump_build -from semver import finalize_version -from semver import min_ver -from semver import max_ver -from semver import VersionInfo -from semver import parse_version_info - +from semver import (VersionInfo, + bump_build, + bump_major, + bump_minor, + bump_patch, + bump_prerelease, + compare, + createparser, + finalize_version, + format_version, + main, + match, + max_ver, + min_ver, + parse, + parse_version_info, + process, + ) SEMVERFUNCS = [ - compare, match, parse, format_version, - bump_major, bump_minor, bump_patch, bump_prerelease, bump_build, - max_ver, min_ver, finalize_version + compare, createparser, + bump_build, bump_major, bump_minor, bump_patch, bump_prerelease, + finalize_version, format_version, + match, max_ver, min_ver, parse, process, ] @@ -590,3 +595,63 @@ def test_should_be_able_to_use_integers_as_prerelease_build(): assert isinstance(v.prerelease, str) assert isinstance(v.build, str) assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, '4', '5') + + +@pytest.mark.parametrize("cli,expected", [ + (["bump", "major", "1.2.3"], + Namespace(which='bump', bump='major', version='1.2.3')), + (["bump", "minor", "1.2.3"], + Namespace(which='bump', bump='minor', version='1.2.3')), + (["bump", "patch", "1.2.3"], + Namespace(which='bump', bump='patch', version='1.2.3')), + (["bump", "prerelease", "1.2.3"], + Namespace(which='bump', bump='prerelease', version='1.2.3')), + (["bump", "build", "1.2.3"], + Namespace(which='bump', bump='build', version='1.2.3')), + # --- + (["compare", "1.2.3", "2.1.3"], + Namespace(which='compare', version1='1.2.3', version2='2.1.3')), +]) +def test_should_parse_cli_arguments(cli, expected): + parser = createparser() + assert parser + result = parser.parse_args(cli) + assert result == expected + + +@pytest.mark.parametrize("args,expected", [ + # bump subcommand + (Namespace(which='bump', bump='major', version='1.2.3'), + "2.0.0"), + (Namespace(which='bump', bump='minor', version='1.2.3'), + "1.3.0"), + (Namespace(which='bump', bump='patch', version='1.2.3'), + "1.2.4"), + (Namespace(which='bump', bump='prerelease', version='1.2.3-rc1'), + "1.2.3-rc2"), + (Namespace(which='bump', bump='build', version='1.2.3+build.13'), + "1.2.3+build.14"), + # compare subcommand + (Namespace(which='compare', version1='1.2.3', version2='2.1.3'), + "-1"), + (Namespace(which='compare', version1='1.2.3', version2='1.2.3'), + "0"), + (Namespace(which='compare', version1='2.4.0', version2='2.1.3'), + "1"), +]) +def test_should_process_parsed_cli_arguments(args, expected): + assert process(args) == expected + + +def test_should_process_print(capsys): + rc = main(["bump", "major", "1.2.3"]) + assert rc == 0 + captured = capsys.readouterr() + assert captured.out.rstrip() == "2.0.0" + + +def test_should_process_raise_error(capsys): + rc = main(["bump", "major", "1.2"]) + assert rc != 0 + captured = capsys.readouterr() + assert captured.err.startswith("ERROR") From 2d0f8055270c8a316e4325b5caeac5a05553d623 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 30 Oct 2019 15:19:10 +0100 Subject: [PATCH 030/219] Fix 144: Add replace function (#156) This commit implements: * semver.replace(version: str, **parts: dict) -> str * semver.VersionInfo.replace(**parts: dict) -> VersionInfo * Add test cases * Extend usage documentation to describe new functions * Update CHANGELOG.rst Both do almost the same: it replaces parts of a version string or a VersionInfo object and returns either a new string or a new VersionInfo object (with the changed parts); see the signature above. Co-authored-by: Alexander Grund --- CHANGELOG.rst | 2 ++ docs/usage.rst | 31 +++++++++++++++++++++++++++++++ semver.py | 40 ++++++++++++++++++++++++++++++++++++++++ test_semver.py | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aa35c93a..f667f68a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,8 @@ Features * :pr:`125`: Created :file:`setup.cfg` for pytest and tox * :gh:`126` (:pr:`127`): Added target for documentation in :file:`tox.ini` * :gh:`142` (:pr:`143`): Improved usage section +* :gh:`144` (:pr:`156`): Added :func:`semver.replace` and :func:`semver.VersionInfo.replace` + functions * :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` * :pr:`157`: Introduce :file:`conftest.py` to improve doctests * :pr:`165`: Improved code coverage diff --git a/docs/usage.rst b/docs/usage.rst index b6699956..1ae7d8da 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -118,6 +118,37 @@ In case you need the different parts of a version stepwise, iterate over the :cl [3, 4, 5, 'pre.2', 'build.4'] +Replacing Parts of a Version +---------------------------- + +If you want to replace different parts of a version, but leave other parts +unmodified, use one of the functions :func:`semver.replace` or +:func:`semver.VersionInfo.replace`: + +* From a version string:: + + >>> semver.replace("1.4.5-pre.1+build.6", major=2) + '2.4.5-pre.1+build.6' + +* From a :class:`semver.VersionInfo` instance:: + + >>> version = semver.VersionInfo.parse("1.4.5-pre.1+build.6") + >>> version.replace(major=2, minor=2) + VersionInfo(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') + +If you pass invalid keys you get an exception:: + + >>> semver.replace("1.2.3", invalidkey=2) + Traceback (most recent call last) + ... + TypeError: replace() got 1 unexpected keyword argument(s): invalidkey + >>> version = semver.VersionInfo.parse("1.4.5-pre.1+build.6") + >>> version.replace(invalidkey=2) + Traceback (most recent call last) + ... + TypeError: replace() got 1 unexpected keyword argument(s): invalidkey + + .. _sec.convert.versions: Converting Different Version Types diff --git a/semver.py b/semver.py index bfe1f73c..2bcb4de7 100644 --- a/semver.py +++ b/semver.py @@ -292,6 +292,28 @@ def parse(version): """ return parse_version_info(version) + def replace(self, **parts): + """Replace one or more parts of a version and return a new + :class:`semver.VersionInfo` object, but leave self untouched + + :param dict parts: the parts to be updated. Valid keys are: + ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` + :return: the new :class:`semver.VersionInfo` object with the changed + parts + :raises: TypeError, if ``parts`` contains invalid keys + """ + version = self._asdict() + version.update(parts) + try: + return VersionInfo(**version) + except TypeError: + unknownkeys = set(parts) - set(self._asdict()) + error = ("replace() got %d unexpected keyword " + "argument(s): %s" % (len(unknownkeys), + ", ".join(unknownkeys)) + ) + raise TypeError(error) + def _to_dict(obj): if isinstance(obj, VersionInfo): @@ -704,6 +726,24 @@ def main(cliargs=None): return 2 +def replace(version, **parts): + """Replace one or more parts of a version and return the new string + + :param str version: the version string to replace + :param dict parts: the parts to be updated. Valid keys are: + ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` + :return: the replaced version string + :raises: TypeError, if ``parts`` contains invalid keys + :rtype: str + + >>> import semver + >>> semver.replace("1.2.3", major=2, patch=10) + '2.2.10' + """ + version = parse_version_info(version) + return str(version.replace(**parts)) + + if __name__ == "__main__": import doctest doctest.testmod() diff --git a/test_semver.py b/test_semver.py index 0fb78981..6b1bf579 100644 --- a/test_semver.py +++ b/test_semver.py @@ -18,14 +18,15 @@ parse, parse_version_info, process, + replace, ) SEMVERFUNCS = [ compare, createparser, bump_build, bump_major, bump_minor, bump_patch, bump_prerelease, finalize_version, format_version, - match, max_ver, min_ver, parse, process, -] + match, max_ver, min_ver, parse, process, replace, + ] @pytest.mark.parametrize("string,expected", [ @@ -655,3 +656,45 @@ def test_should_process_raise_error(capsys): assert rc != 0 captured = capsys.readouterr() assert captured.err.startswith("ERROR") + + +@pytest.mark.parametrize("version,parts,expected", [ + ("3.4.5", dict(major=2), '2.4.5'), + ("3.4.5", dict(major="2"), '2.4.5'), + ("3.4.5", dict(major=2, minor=5), '2.5.5'), + ("3.4.5", dict(minor=2), '3.2.5'), + ("3.4.5", dict(major=2, minor=5, patch=10), '2.5.10'), + ("3.4.5", dict(major=2, minor=5, patch=10, prerelease="rc1"), + '2.5.10-rc1'), + ("3.4.5", dict(major=2, minor=5, patch=10, prerelease="rc1", build="b1"), + '2.5.10-rc1+b1'), + ("3.4.5-alpha.1.2", dict(major=2), '2.4.5-alpha.1.2'), + ("3.4.5-alpha.1.2", dict(build="x1"), '3.4.5-alpha.1.2+x1'), + ("3.4.5+build1", dict(major=2), '2.4.5+build1'), +]) +def test_replace_method_replaces_requested_parts(version, parts, expected): + assert replace(version, **parts) == expected + + +def test_replace_raises_TypeError_for_invalid_keyword_arg(): + with pytest.raises(TypeError, match=r"replace\(\).*unknown.*"): + assert replace("1.2.3", unknown="should_raise") + + +@pytest.mark.parametrize("version,parts,expected", [ + ("3.4.5", dict(major=2, minor=5), '2.5.5'), + ("3.4.5", dict(major=2, minor=5, patch=10), '2.5.10'), + ("3.4.5-alpha.1.2", dict(major=2), '2.4.5-alpha.1.2'), + ("3.4.5-alpha.1.2", dict(build="x1"), '3.4.5-alpha.1.2+x1'), + ("3.4.5+build1", dict(major=2), '2.4.5+build1'), +]) +def test_should_return_versioninfo_with_replaced_parts(version, + parts, + expected): + assert VersionInfo.parse(version).replace(**parts) == \ + VersionInfo.parse(expected) + + +def test_replace_raises_ValueError_for_non_numeric_values(): + with pytest.raises(ValueError): + VersionInfo.parse("1.2.3").replace(major="x") From 014d153386b61566d9393ea1daa113e56a4ba03f Mon Sep 17 00:00:00 2001 From: scls19fr Date: Wed, 30 Oct 2019 15:36:41 +0100 Subject: [PATCH 031/219] Release 2.9.0 (#170) * Release 2.9.0 * Fix release procedure --- release-procedure.md | 15 +++++++++++---- semver.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/release-procedure.md b/release-procedure.md index 682f959f..fca90b93 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -1,23 +1,31 @@ -* Verify that latest build was passing https://travis-ci.org/k-bx/python-semver +################# +Release procedure +################# + +* Verify that issues about new release are closed https://github.com/k-bx/python-semver/issues and that no pull requests that should be included in this release haven't been left out https://github.com/k-bx/python-semver/pulls + +* Verify that continuous integration for latest build was passing https://travis-ci.org/k-bx/python-semver * Verify that `__version__` in [semver.py](https://github.com/k-bx/python-semver/blob/master/semver.py) have been updated and follow https://semver.org/ -* Verify that [CHANGELOG](https://github.com/k-bx/python-semver/blob/master/CHANGELOG) have been updated +* Verify that [CHANGELOG](https://github.com/k-bx/python-semver/blob/master/CHANGELOG.rst) have been updated * If one or several supported Python versions have been removed or added, verify that the 3 following files have been updated: * [setup.py](https://github.com/k-bx/python-semver/blob/master/setup.py) * [tox.ini](https://github.com/k-bx/python-semver/blob/master/tox.ini) * [.travis.yml](https://github.com/k-bx/python-semver/blob/master/.travis.yml) -* Verify that doc reflecting new changes have been updated +* Verify that doc reflecting new changes have been updated and are available at https://python-semver.readthedocs.io/en/latest/ If necessary, trigger doc build at https://readthedocs.org/projects/python-semver/ * Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/k-bx/python-semver/blob/master/CONTRIBUTORS) * Tag commit and push to github using command line interface + ```bash git tag -a x.x.x -m 'Version x.x.x' git push python-semver master --tags ``` + or using GitHub web interface available at https://github.com/k-bx/python-semver/releases * Upload to PyPI @@ -29,4 +37,3 @@ twine upload dist/* ``` * Go to https://pypi.org/project/semver/ to verify that new version is online and page is rendered correctly - diff --git a/semver.py b/semver.py index 2bcb4de7..6bc9fcab 100644 --- a/semver.py +++ b/semver.py @@ -10,7 +10,7 @@ import sys -__version__ = '2.8.2' +__version__ = '2.9.0' __author__ = 'Kostiantyn Rybnikov' __author_email__ = 'k-bx@k-bx.com' __maintainer__ = 'Sebastien Celles' From 1bafb9f0aea0117a196eb7d112c3f0a0da783abf Mon Sep 17 00:00:00 2001 From: scls19fr Date: Wed, 30 Oct 2019 16:15:53 +0100 Subject: [PATCH 032/219] Fix long description (#172) * Fix long description * Fix release procedure --- README.rst | 12 ++++++------ release-procedure.md | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 083bb39e..9d8c155d 100644 --- a/README.rst +++ b/README.rst @@ -42,7 +42,7 @@ To import this library, use: >>> import semver Working with the library is quite straightforward. To turn a version string into the -different parts, use the :func:`semver.parse` function: +different parts, use the `semver.parse` function: .. code-block:: python @@ -59,10 +59,10 @@ different parts, use the :func:`semver.parse` function: 'build.5' To raise parts of a version, there are a couple of functions available for -you. The :func:`semver.parse_version_info` function converts a version string -into a :class:`semver.VersionInfo` class. The function -:func:`semver.VersionInfo.bump_major` leaves the original object untouched, but -returns a new :class:`semver.VersionInfo` instance with the raised major part: +you. The `semver.parse_version_info` function converts a version string +into a `semver.VersionInfo` class. The function +`semver.VersionInfo.bump_major` leaves the original object untouched, but +returns a new `semver.VersionInfo` instance with the raised major part: .. code-block:: python @@ -77,7 +77,7 @@ It is allowed to concatenate different "bump functions": >>> ver.bump_major().bump_minor() VersionInfo(major=4, minor=0, patch=1, prerelease=None, build=None) -To compare two versions, semver provides the :func:`semver.compare` function. +To compare two versions, semver provides the `semver.compare` function. The return value indicates the relationship between the first and second version: diff --git a/release-procedure.md b/release-procedure.md index fca90b93..17708175 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -19,14 +19,7 @@ Release procedure * Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/k-bx/python-semver/blob/master/CONTRIBUTORS) -* Tag commit and push to github using command line interface - -```bash -git tag -a x.x.x -m 'Version x.x.x' -git push python-semver master --tags -``` - -or using GitHub web interface available at https://github.com/k-bx/python-semver/releases +* Ensure that long description (ie [README.rst](https://github.com/k-bx/python-semver/blob/master/README.rst)) can be correctly rendered by Pypi using `restview --long-description` * Upload to PyPI @@ -37,3 +30,12 @@ twine upload dist/* ``` * Go to https://pypi.org/project/semver/ to verify that new version is online and page is rendered correctly + +* Tag commit and push to github using command line interface + +```bash +git tag -a x.x.x -m 'Version x.x.x' +git push python-semver master --tags +``` + +or using GitHub web interface available at https://github.com/k-bx/python-semver/releases From 7c2c59c3ae44fdffd2cb93fe98587fc70dc6d422 Mon Sep 17 00:00:00 2001 From: scls19fr Date: Wed, 30 Oct 2019 16:36:13 +0100 Subject: [PATCH 033/219] Update CHANGELOG.rst (#174) --- CHANGELOG.rst | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f667f68a..fad5d48b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,10 +6,23 @@ Change Log All notable changes to this code base will be documented in this file, in every released version. - -Version 2.9.0 (WIP) +Version 2.9.1 (WIP) =================== -:Released: 20yy-mm-dd +:Released: 20xy-xy-xy +:Maintainer: ... + +Features +-------- + +Bug Fixes +--------- + +Removals +-------- + +Version 2.9.0 +============= +:Released: 2019-10-30 :Maintainer: Sébastien Celles Features From 97230efc7d42314e14a5a6917d8c17dfb172bb28 Mon Sep 17 00:00:00 2001 From: scls19fr Date: Wed, 30 Oct 2019 16:38:59 +0100 Subject: [PATCH 034/219] Update release-procedure.md --- release-procedure.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-procedure.md b/release-procedure.md index 17708175..7f53ea40 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -2,13 +2,13 @@ Release procedure ################# -* Verify that issues about new release are closed https://github.com/k-bx/python-semver/issues and that no pull requests that should be included in this release haven't been left out https://github.com/k-bx/python-semver/pulls +* Verify that issues about new release are closed https://github.com/k-bx/python-semver/issues and verify that no pull requests that should be included in this release haven't been left out https://github.com/k-bx/python-semver/pulls * Verify that continuous integration for latest build was passing https://travis-ci.org/k-bx/python-semver * Verify that `__version__` in [semver.py](https://github.com/k-bx/python-semver/blob/master/semver.py) have been updated and follow https://semver.org/ -* Verify that [CHANGELOG](https://github.com/k-bx/python-semver/blob/master/CHANGELOG.rst) have been updated +* Verify that [CHANGELOG](https://github.com/k-bx/python-semver/blob/master/CHANGELOG.rst) have been updated. No WIP should be present in CHANGELOG during release! * If one or several supported Python versions have been removed or added, verify that the 3 following files have been updated: * [setup.py](https://github.com/k-bx/python-semver/blob/master/setup.py) From e2b07af2eb25005eb57a66cc045263c26388fb57 Mon Sep 17 00:00:00 2001 From: scls19fr Date: Tue, 5 Nov 2019 11:48:56 +0100 Subject: [PATCH 035/219] Move to GH org (#178) * Update setup.py * Update conf.py * Update README.rst * Update development.rst * Update release-procedure.md * Update CHANGELOG.rst --- CHANGELOG.rst | 2 ++ README.rst | 6 +++--- docs/conf.py | 4 ++-- docs/development.rst | 8 ++++---- release-procedure.md | 20 ++++++++++---------- setup.py | 4 ++-- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fad5d48b..2baf9cd4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,8 @@ Version 2.9.1 (WIP) Features -------- +* :gh:`177` (:pr:`178`): Fixed repository and CI links (moved https://github.com/k-bx/python-semver/ repository to https://github.com/python-semver/python-semver/) + Bug Fixes --------- diff --git a/README.rst b/README.rst index 9d8c155d..d03e1455 100644 --- a/README.rst +++ b/README.rst @@ -97,9 +97,9 @@ There are other functions to discover. Read on! .. |latest-version| image:: https://img.shields.io/pypi/v/semver.svg :alt: Latest version on PyPI :target: https://pypi.org/project/semver -.. |build-status| image:: https://travis-ci.org/k-bx/python-semver.svg?branch=master +.. |build-status| image:: https://travis-ci.com/python-semver/python-semver.svg?branch=master :alt: Build status - :target: https://travis-ci.org/k-bx/python-semver + :target: https://travis-ci.com/python-semver/python-semver .. |python-support| image:: https://img.shields.io/pypi/pyversions/semver.svg :target: https://pypi.org/project/semver :alt: Python versions @@ -108,7 +108,7 @@ There are other functions to discover. Read on! :target: https://pypi.org/project/semver .. |license| image:: https://img.shields.io/pypi/l/semver.svg :alt: Software license - :target: https://github.com/k-bx/python-semver/blob/master/LICENSE.txt + :target: https://github.com/python-semver/python-semver/blob/master/LICENSE.txt .. |docs| image:: https://readthedocs.org/projects/python-semver/badge/?version=latest :target: http://python-semver.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status diff --git a/docs/conf.py b/docs/conf.py index c4afdf8e..525c6201 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -84,9 +84,9 @@ # Markup to shorten external links # See https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html -extlinks = {'gh': ('https://github.com/k-bx/python-semver/issues/%s', +extlinks = {'gh': ('https://github.com/python-semver/python-semver/issues/%s', '#'), - 'pr': ('https://github.com/k-bx/python-semver/pull/%s', + 'pr': ('https://github.com/python-semver/python-semver/pull/%s', 'PR #'), } diff --git a/docs/development.rst b/docs/development.rst index be66c14f..7ad63d74 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -3,7 +3,7 @@ Contributing to semver The semver source code is managed using Git and is hosted on GitHub:: - git clone git://github.com/k-bx/python-semver + git clone git://github.com/python-semver/python-semver Reporting Bugs and Feedback @@ -41,7 +41,7 @@ Modifying the Code We recommend the following workflow: #. Fork our project on GitHub using this link: - https://github.com/k-bx/python-semver/fork + https://github.com/python-semver/python-semver/fork #. Clone your forked Git repository (replace ``GITHUB_USER`` with your account name on GitHub):: @@ -185,8 +185,8 @@ documentation includes: .. _flake8: https://flake8.readthedocs.io -.. _issues: https://github.com/k-bx/python-semver/issues -.. _pull request: https://github.com/k-bx/python-semver/pulls +.. _issues: https://github.com/python-semver/python-semver/issues +.. _pull request: https://github.com/python-semver/python-semver/pulls .. _pytest: http://pytest.org/ .. _Semantic Versioning: https://semver.org .. _Sphinx style: https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html diff --git a/release-procedure.md b/release-procedure.md index 7f53ea40..52a43fd5 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -2,24 +2,24 @@ Release procedure ################# -* Verify that issues about new release are closed https://github.com/k-bx/python-semver/issues and verify that no pull requests that should be included in this release haven't been left out https://github.com/k-bx/python-semver/pulls +* Verify that issues about new release are closed https://github.com/python-semver/python-semver/issues and verify that no pull requests that should be included in this release haven't been left out https://github.com/python-semver/python-semver/pulls -* Verify that continuous integration for latest build was passing https://travis-ci.org/k-bx/python-semver +* Verify that continuous integration for latest build was passing https://travis-ci.com/python-semver/python-semver -* Verify that `__version__` in [semver.py](https://github.com/k-bx/python-semver/blob/master/semver.py) have been updated and follow https://semver.org/ +* Verify that `__version__` in [semver.py](https://github.com/python-semver/python-semver/blob/master/semver.py) have been updated and follow https://semver.org/ -* Verify that [CHANGELOG](https://github.com/k-bx/python-semver/blob/master/CHANGELOG.rst) have been updated. No WIP should be present in CHANGELOG during release! +* Verify that [CHANGELOG](https://github.com/python-semver/python-semver/blob/master/CHANGELOG.rst) have been updated. No WIP should be present in CHANGELOG during release! * If one or several supported Python versions have been removed or added, verify that the 3 following files have been updated: - * [setup.py](https://github.com/k-bx/python-semver/blob/master/setup.py) - * [tox.ini](https://github.com/k-bx/python-semver/blob/master/tox.ini) - * [.travis.yml](https://github.com/k-bx/python-semver/blob/master/.travis.yml) + * [setup.py](https://github.com/python-semver/python-semver/blob/master/setup.py) + * [tox.ini](https://github.com/python-semver/python-semver/blob/master/tox.ini) + * [.travis.yml](https://github.com/python-semver/python-semver/blob/master/.travis.yml) * Verify that doc reflecting new changes have been updated and are available at https://python-semver.readthedocs.io/en/latest/ If necessary, trigger doc build at https://readthedocs.org/projects/python-semver/ -* Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/k-bx/python-semver/blob/master/CONTRIBUTORS) +* Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/python-semver/python-semver/blob/master/CONTRIBUTORS) -* Ensure that long description (ie [README.rst](https://github.com/k-bx/python-semver/blob/master/README.rst)) can be correctly rendered by Pypi using `restview --long-description` +* Ensure that long description (ie [README.rst](https://github.com/python-semver/python-semver/blob/master/README.rst)) can be correctly rendered by Pypi using `restview --long-description` * Upload to PyPI @@ -38,4 +38,4 @@ git tag -a x.x.x -m 'Version x.x.x' git push python-semver master --tags ``` -or using GitHub web interface available at https://github.com/k-bx/python-semver/releases +or using GitHub web interface available at https://github.com/python-semver/python-semver/releases diff --git a/setup.py b/setup.py index 2003c19f..da0fab65 100755 --- a/setup.py +++ b/setup.py @@ -82,8 +82,8 @@ def read_file(filename): long_description=read_file('README.rst'), author=package.__author__, author_email=package.__author_email__, - url='https://github.com/k-bx/python-semver', - download_url='https://github.com/k-bx/python-semver/downloads', + url='https://github.com/python-semver/python-semver', + download_url='https://github.com/python-semver/python-semver/downloads', py_modules=[package.__name__], include_package_data=True, license='BSD', From 2b5ffb7a48d939569c196b92aa6e8dc7a710469a Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 5 Nov 2019 13:48:42 +0100 Subject: [PATCH 036/219] Add note for GH repo to new python-semver org (#179) * Mentioned in #177 and #178 * Update CHANGELOG.rst --- CHANGELOG.rst | 1 + README.rst | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2baf9cd4..bda9b17c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,7 @@ Features -------- * :gh:`177` (:pr:`178`): Fixed repository and CI links (moved https://github.com/k-bx/python-semver/ repository to https://github.com/python-semver/python-semver/) +* :pr:`179`: Added note about moving this project to the new python-semver organization on GitHub Bug Fixes --------- diff --git a/README.rst b/README.rst index d03e1455..62e6b92a 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,18 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. .. teaser-end +.. note:: + + With version 2.9.0 we've moved the GitHub project. The project is now + located under the organization ``python-semver``. + The complete URL is:: + + https://github.com/python-semver/python-semver + + If you still have an old repository, correct your upstream URL to the new URL:: + + $ git remote set-url upstream git@github.com:python-semver/python-semver.git + The module follows the ``MAJOR.MINOR.PATCH`` style: From 18ea3f8a791984429caf9063fd5103291d3e8e9e Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 13 Nov 2019 09:42:09 +0100 Subject: [PATCH 037/219] Fix #187: Create a squared logo (#189) * Mentioned in issue #177 * Add logo to documentation * Update CHANGELOG Co-Authored-By: scls19fr --- CHANGELOG.rst | 2 + docs/conf.py | 1 + docs/logo.svg | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 docs/logo.svg diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bda9b17c..3eec5d53 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,8 @@ Features * :gh:`177` (:pr:`178`): Fixed repository and CI links (moved https://github.com/k-bx/python-semver/ repository to https://github.com/python-semver/python-semver/) * :pr:`179`: Added note about moving this project to the new python-semver organization on GitHub +* :gh:`187` (:pr:`188`): Added logo for python-semver organization and documentation + Bug Fixes --------- diff --git a/docs/conf.py b/docs/conf.py index 525c6201..8f972ae1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -124,6 +124,7 @@ ] } +html_logo = "logo.svg" # -- Options for HTMLHelp output ------------------------------------------ diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 00000000..b2853465 --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1,229 @@ + + From 985a5a749ba251950e4e7d8400e3866924ca92aa Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 13 Nov 2019 11:10:49 +0100 Subject: [PATCH 038/219] Correct logo width, add white background (#190) * Add custom CSS file to change size of the logo * Add white background --- docs/_static/css/default.css | 6 ++++++ docs/conf.py | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 docs/_static/css/default.css diff --git a/docs/_static/css/default.css b/docs/_static/css/default.css new file mode 100644 index 00000000..ed7cf80a --- /dev/null +++ b/docs/_static/css/default.css @@ -0,0 +1,6 @@ +/* Customize logo width */ + +.wy-side-nav-search > a img.logo { + width: 6em; + background: white; +} diff --git a/docs/conf.py b/docs/conf.py index 8f972ae1..12ae5f35 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -109,6 +109,8 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +html_css_files = ['css/default.css'] + # Custom sidebar templates, must be a dictionary that maps document names # to template names. # From 61f4521fbd8e335c2f39b847f3248219c490b8f2 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Fri, 15 Nov 2019 11:30:55 +0100 Subject: [PATCH 039/219] Fix empty call with "pysemver" and "pysemver bump" (#193) * Raise SystemExit when "pysemver" is called without arguments. * When "pysemver bump" is called without arguments, print the help text for the bump subcommand and exit. * Add test cases for both calls * Update CHANGELOG Without this patch, "pysemver bump" raises an AttributeError: ``` Traceback (most recent call last): File ".tox/py36/bin/pysemver", line 8, in sys.exit(main()) File ".tox/py36/lib/python3.6/site-packages/semver.py", line 726, in main result = process(args) File ".tox/py36/lib/python3.6/site-packages/semver.py", line 693, in process if args.which == "bump": AttributeError: 'Namespace' object has no attribute 'which' ``` --- CHANGELOG.rst | 3 +++ semver.py | 14 +++++++++++--- test_semver.py | 10 ++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3eec5d53..0cd4a8f5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,9 @@ Features Bug Fixes --------- +* :gh:`192` (:pr:`193`): Fixed "pysemver" and "pysemver bump" when called without arguments + + Removals -------- diff --git a/semver.py b/semver.py index 6bc9fcab..da477370 100644 --- a/semver.py +++ b/semver.py @@ -676,7 +676,6 @@ def createparser(): p.add_argument("version", help="Version to raise" ) - return parser @@ -690,13 +689,21 @@ def process(args): :return: result of the selected action :rtype: str """ - if args.which == "bump": + if not hasattr(args, "which"): + args.parser.print_help() + raise SystemExit() + elif args.which == "bump": maptable = {'major': 'bump_major', 'minor': 'bump_minor', 'patch': 'bump_patch', 'prerelease': 'bump_prerelease', 'build': 'bump_build', } + if args.bump is None: + # When bump is called without arguments, + # print the help and exit + args.parser.parse_args([args.which, "-h"]) + ver = parse_version_info(args.version) # get the respective method and call it func = getattr(ver, maptable[args.bump]) @@ -716,7 +723,8 @@ def main(cliargs=None): try: parser = createparser() args = parser.parse_args(args=cliargs) - # args.parser = parser + # Save parser instance: + args.parser = parser result = process(args) print(result) return 0 diff --git a/test_semver.py b/test_semver.py index 6b1bf579..023488da 100644 --- a/test_semver.py +++ b/test_semver.py @@ -658,6 +658,16 @@ def test_should_process_raise_error(capsys): assert captured.err.startswith("ERROR") +def test_should_raise_systemexit_when_called_with_empty_arguments(): + with pytest.raises(SystemExit): + main([]) + + +def test_should_raise_systemexit_when_bump_iscalled_with_empty_arguments(): + with pytest.raises(SystemExit): + main(["bump"]) + + @pytest.mark.parametrize("version,parts,expected", [ ("3.4.5", dict(major=2), '2.4.5'), ("3.4.5", dict(major="2"), '2.4.5'), From dbc2e40f72f945572d2b36812bc08f0bc09a90c1 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 17 Nov 2019 11:47:55 +0100 Subject: [PATCH 040/219] Fix #191: Add manpage for pysemver (#195) * Rename `cli.rst` -> `pysemver.rst` to be consistent with the name of our little script * Add missing --version in pysemver script * Restructure pysemver.rst to reflect manpage style * Add additional target "man" in tox.ini; it's now possible to run "tox -e man". Manpage can be found at `docs/_build/man`. * Ignore `docs/_build/` * Update `CHANGELOG.rst` --- .gitignore | 1 + CHANGELOG.rst | 2 +- docs/Makefile | 2 +- docs/cli.rst | 45 ---------------- docs/conf.py | 10 ++-- docs/index.rst | 2 +- docs/pysemver.rst | 130 ++++++++++++++++++++++++++++++++++++++++++++++ semver.py | 7 ++- tox.ini | 7 +++ 9 files changed, 154 insertions(+), 52 deletions(-) delete mode 100644 docs/cli.rst create mode 100644 docs/pysemver.rst diff --git a/.gitignore b/.gitignore index f0636016..994eb868 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ coverage.xml # Sphinx documentation doc/_build/ +docs/_build/ # PyBuilder target/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0cd4a8f5..e55f1a8e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,7 +17,7 @@ Features * :gh:`177` (:pr:`178`): Fixed repository and CI links (moved https://github.com/k-bx/python-semver/ repository to https://github.com/python-semver/python-semver/) * :pr:`179`: Added note about moving this project to the new python-semver organization on GitHub * :gh:`187` (:pr:`188`): Added logo for python-semver organization and documentation - +* :gh:`191` (:pr:`194`): Created manpage for pysemver Bug Fixes --------- diff --git a/docs/Makefile b/docs/Makefile index a7e3cbaf..ea7c53f0 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = python -msphinx +SPHINXBUILD = sphinx-build SPHINXPROJ = semver SOURCEDIR = . BUILDDIR = _build diff --git a/docs/cli.rst b/docs/cli.rst deleted file mode 100644 index 9f21bfeb..00000000 --- a/docs/cli.rst +++ /dev/null @@ -1,45 +0,0 @@ -CLI -=== - -The library provides also a command line interface. This allows to include -the functionality of semver into shell scripts. - -Using the pysemver Script -------------------------- - -The script name is :command:`pysemver` and provides the subcommands ``bump`` -and ``compare``. - -To bump a version, you pass the name of the part (major, minor, patch, prerelease, or -build) and the version string, for example:: - - $ pysemver bump major 1.2.3 - 2.0.0 - $ pysemver bump minor 1.2.3 - 1.3.0 - -If you pass a version string which is not a valid semantical version, you get -an error message:: - - $ pysemver bump build 1.5 - ERROR 1.5 is not valid SemVer string - -To compare two versions, use the ``compare`` subcommand. The result is - -* ``-1`` if first version is smaller than the second version, -* ``0`` if both are the same, -* ``1`` if the first version is greater than the second version. - -For example:: - - $ pysemver compare 1.2.3 2.4.0 - - -.. _interface: - -Interface ---------- - -.. argparse:: - :ref: semver.createparser - :prog: pysemver diff --git a/docs/conf.py b/docs/conf.py index 12ae5f35..107765f2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,7 +36,6 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', 'sphinx.ext.extlinks', - 'sphinxarg.ext', ] # Add any paths that contain templates here, relative to this directory. @@ -167,9 +166,14 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). +manpage_doc = "pysemver" + man_pages = [ - (master_doc, 'semver', 'python-semver Documentation', - [author], 1) + (manpage_doc, + 'pysemver', + 'Helper script for Semantic Versioning', + ["Thomas Schraitle"], + 1) ] diff --git a/docs/index.rst b/docs/index.rst index 140ebad0..4cc5a966 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,7 @@ Semver |version| -- Semantic Versioning readme install usage - cli + pysemver development api changelog diff --git a/docs/pysemver.rst b/docs/pysemver.rst new file mode 100644 index 00000000..17cd6554 --- /dev/null +++ b/docs/pysemver.rst @@ -0,0 +1,130 @@ +:orphan: + +pysemver |version| +================== + +Synopsis +-------- + +.. _invocation: + +.. code:: bash + + pysemver compare + pysemver bump {major,minor,patch,prerelease,build} + + +Description +----------- + +The semver library provides a command line interface with the name +:command:`pysemver` to make the functionality accessible for shell +scripts. The script supports several subcommands. + + +Global Options +~~~~~~~~~~~~~~ + +.. program:: pysemver + +.. option:: -h, --help + + Display usage summary. + +.. option:: --version + + Show program's version number and exit. + + +Commands +-------- + +.. HINT: Sort the subcommands alphabetically + +pysemver bump +~~~~~~~~~~~~~ + +Bump a version. + +.. code:: bash + + pysemver bump + +.. option:: + + The part to bump. Valid strings can be ``major``, ``minor``, + ``patch``, ``prerelease``, or ``build``. The part has the + following effects: + + * ``major``: Raise the major part of the version and set + minor and patch to zero, remove prerelease and build. + * ``minor``: Raise the minor part of the version and set + patch to zero, remove prerelease and build. + * ``patch``: Raise the patch part of the version and + remove prerelease and build. + * ``prerelease`` Raise the prerelease of the version and + remove the build part. + * ``build``: Raise the build part. + +.. option:: + + The version to bump. + +To bump a version, you pass the name of the part (``major``, ``minor``, +``patch``, ``prerelease``, or ``build``) and the version string. +The bumped version is printed on standard out:: + + $ pysemver bump major 1.2.3 + 2.0.0 + $ pysemver bump minor 1.2.3 + 1.3.0 + +If you pass a version string which is not a valid semantical version, +you get an error message and a return code != 0:: + + $ pysemver bump build 1.5 + ERROR 1.5 is not valid SemVer string + + +pysemver compare +~~~~~~~~~~~~~~~~ + +Compare two versions. + +.. code:: bash + + pysemver compare + +.. option:: + + First version + +.. option:: + + Second version + +When you compare two versions, the result is printed on *standard out*, +to indicates which is the bigger version: + +* ``-1`` if first version is smaller than the second version, +* ``0`` if both versions are the same, +* ``1`` if the first version is greater than the second version. + +The *error code* returned by the script indicates if both versions +are valid (return code 0) or not (return code != 0):: + + $ pysemver compare 1.2.3 2.4.0 + -1 + $ pysemver compare 1.2.3 2.4.0 ; echo $? + 0 + $ pysemver compare 1.2.3 2.4.0 ; echo $? + ERROR 1.2.x is not valid SemVer string + 2 + + +See also +-------- + +:Documentation: https://python-semver.readthedocs.io/ +:Source code: https://github.com/python-semver/python-semver +:Bug tracker: https://github.com/python-semver/python-semver/issues diff --git a/semver.py b/semver.py index da477370..db82585d 100644 --- a/semver.py +++ b/semver.py @@ -640,8 +640,13 @@ def createparser(): """ parser = argparse.ArgumentParser(prog=__package__, description=__doc__) - s = parser.add_subparsers() + parser.add_argument('--version', + action='version', + version='%(prog)s ' + __version__ + ) + + s = parser.add_subparsers() # create compare subcommand parser_compare = s.add_parser("compare", help="Compare two versions" diff --git a/tox.ini b/tox.ini index 33b60895..abca346b 100644 --- a/tox.ini +++ b/tox.ini @@ -22,3 +22,10 @@ basepython = python3 deps = -r{toxinidir}/docs/requirements.txt skip_install = true commands = sphinx-build {posargs:-E} -b html docs dist/docs + +[testenv:man] +whitelist_externals = make +basepython = python3 +deps = sphinx +skip_install = true +commands = make -C docs man From 91cbce94879cd04de6d4ea3ab11929cf13742a39 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 20 Nov 2019 21:32:02 +0100 Subject: [PATCH 041/219] Fix #196: Add distribution specific install hints (#197) * Improve the installation section and add installation instructions for Arch Linux, Debian, Fedora, FreeBSD, openSUSE, and Ubuntu. * Adapt CHANGELOG.rst --- CHANGELOG.rst | 2 ++ docs/install.rst | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e55f1a8e..53079d3c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,8 @@ Features * :pr:`179`: Added note about moving this project to the new python-semver organization on GitHub * :gh:`187` (:pr:`188`): Added logo for python-semver organization and documentation * :gh:`191` (:pr:`194`): Created manpage for pysemver +* :gh:`196` (:pr:`197`): Added distribution specific installation instructions + Bug Fixes --------- diff --git a/docs/install.rst b/docs/install.rst index 1abb7ed7..2e43feb1 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -12,3 +12,79 @@ For Python 3: .. code-block:: bash pip3 install semver + + +.. note:: + + Some Linux distributions can have outdated packages. + These outdated packages does not contain the latest bug fixes or new features. + If you need a newer package, you have these option: + + * Ask the maintainer to update the package. + * Update the package for your favorite distribution and submit it. + * Use a Python virtual environment and :command:`pip install`. + + +Arch Linux +---------- + +1. Enable the community repositories first: + + .. code-block:: ini + + [community] + Include = /etc/pacman.d/mirrorlist + +2. Install the package:: + + $ pacman -Sy python-semver + + +Debian +------ + +1. Update the package index:: + + $ sudo apt-get update + +2. Install the package:: + + $ sudo apt-get install python3-semver + + +Fedora +------ + +.. code-block:: bash + + $ dnf install python3-semver + + +FreeBSD +------- + +.. code-block:: bash + + +openSUSE +-------- + +1. Enable the the ``devel:languages:python`` repository on the Open Build Service (replace ```` with the preferred openSUSE release):: + + $ zypper addrepo https://download.opensuse.org/repositories/devel:/languages:/python/openSUSE_Leap_/devel:languages:python.repo + +2. Install the package:: + + $ zypper --repo devel_languages_python python3-semver + + +Ubuntu +------ + +1. Update the package index:: + + $ sudo apt-get update + +2. Install the package:: + + $ sudo apt-get install python3-semver From 05948ec47b224eae20b96348e55099bbaf7b1e8e Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 24 Nov 2019 16:51:23 +0100 Subject: [PATCH 042/219] Doc: Correct missing parts in installation section (#199) * Add missing install command for FreeBSD * Introduce a "Pip" and "Linux Distributions" subsection * Change header level of every distribution (to make it appear under the "Linux Distributions" subsection) * Move note into "Linux Distributions" subsection --- docs/install.rst | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index 2e43feb1..437e3c3d 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,6 +1,10 @@ Installing semver ================= + +Pip +--- + For Python 2: .. code-block:: bash @@ -13,6 +17,8 @@ For Python 3: pip3 install semver +Linux Distributions +------------------- .. note:: @@ -26,7 +32,7 @@ For Python 3: Arch Linux ----------- +^^^^^^^^^^ 1. Enable the community repositories first: @@ -41,7 +47,7 @@ Arch Linux Debian ------- +^^^^^^ 1. Update the package index:: @@ -53,7 +59,7 @@ Debian Fedora ------- +^^^^^^ .. code-block:: bash @@ -61,13 +67,14 @@ Fedora FreeBSD -------- +^^^^^^^ .. code-block:: bash + $ pkg install py36-semver openSUSE --------- +^^^^^^^^ 1. Enable the the ``devel:languages:python`` repository on the Open Build Service (replace ```` with the preferred openSUSE release):: @@ -79,7 +86,7 @@ openSUSE Ubuntu ------- +^^^^^^ 1. Update the package index:: From 9e4ebcfa33ec1ea43b900c20e55ba0e399f70ca5 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 24 Nov 2019 18:24:01 +0100 Subject: [PATCH 043/219] Extend #186: Add GitHub Action for black formatter (#200) The GH Action does basically this: 1. Setup Python 3.7 2. Install dependencies (mainly black) 3. Run black and create a diff file 4. Upload the diff as artifact Currently, it does not any pip caching (I had some problems with that; it didn't work reliably). --- .github/workflows/black-formatting.yml | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/black-formatting.yml diff --git a/.github/workflows/black-formatting.yml b/.github/workflows/black-formatting.yml new file mode 100644 index 00000000..83ad2892 --- /dev/null +++ b/.github/workflows/black-formatting.yml @@ -0,0 +1,49 @@ +name: Black Formatting + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Output env variables + run: | + echo "GITHUB_WORKFLOW=${GITHUB_WORKFLOW}" + echo "GITHUB_ACTION=$GITHUB_ACTION" + echo "GITHUB_ACTIONS=$GITHUB_ACTIONS" + echo "GITHUB_ACTOR=$GITHUB_ACTOR" + echo "GITHUB_REPOSITORY=$GITHUB_REPOSITORY" + echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME" + echo "GITHUB_EVENT_PATH=$GITHUB_EVENT_PATH" + echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" + echo "GITHUB_SHA=$GITHUB_SHA" + echo "GITHUB_REF=$GITHUB_REF" + echo "GITHUB_HEAD_REF=$GITHUB_HEAD_REF" + echo "GITHUB_BASE_REF=$GITHUB_BASE_REF" + echo "::debug::---Start content of file $GITHUB_EVENT_PATH" + cat $GITHUB_EVENT_PATH + echo "\n" + echo "::debug::---end" + + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip black + + - name: Run black + id: black + run: | + black . > project.diff + echo "::set-output name=rc::$?" + + - name: Upload diff artifact + uses: actions/upload-artifact@v1 + with: + name: black-project-diff + path: project.diff From 017e29633562acc8ae9a76c7c8389424263dafd0 Mon Sep 17 00:00:00 2001 From: Karol Date: Sun, 24 Nov 2019 19:46:37 +0100 Subject: [PATCH 044/219] Fix #194 and #114: Update semver regex to version from semver.org (#198) Also, fix problem with invalid Python 2.7 super call. --- semver.py | 18 +++++++++--------- setup.py | 2 +- test_semver.py | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/semver.py b/semver.py index db82585d..af7dec66 100644 --- a/semver.py +++ b/semver.py @@ -19,18 +19,18 @@ _REGEX = re.compile( r""" ^ - (?P(?:0|[1-9][0-9]*)) + (?P0|[1-9]\d*) \. - (?P(?:0|[1-9][0-9]*)) + (?P0|[1-9]\d*) \. - (?P(?:0|[1-9][0-9]*)) - (\-(?P - (?:0|[1-9A-Za-z-][0-9A-Za-z-]*) - (\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))* + (?P0|[1-9]\d*) + (?:-(?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* ))? - (\+(?P - [0-9A-Za-z-]+ - (\.[0-9A-Za-z-]+)* + (?:\+(?P + [0-9a-zA-Z-]+ + (?:\.[0-9a-zA-Z-]+)* ))? $ """, re.VERBOSE) diff --git a/setup.py b/setup.py index da0fab65..7c31635b 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ def run_tests(self): class Clean(CleanCommand): def run(self): - super().run() + super(CleanCommand, self).run() delete_in_root = [ 'build', '.cache', diff --git a/test_semver.py b/test_semver.py index 023488da..053031f6 100644 --- a/test_semver.py +++ b/test_semver.py @@ -70,6 +70,30 @@ def test_fordocstrings(func): 'prerelease': 'alpha-1', 'build': 'build.11.e0f985a', }), + ("0.1.0-0f", + { + 'major': 0, + 'minor': 1, + 'patch': 0, + 'prerelease': '0f', + 'build': None, + }), + ("0.0.0-0foo.1", + { + 'major': 0, + 'minor': 0, + 'patch': 0, + 'prerelease': '0foo.1', + 'build': None, + }), + ("0.0.0-0foo.1+build.1", + { + 'major': 0, + 'minor': 0, + 'patch': 0, + 'prerelease': '0foo.1', + 'build': 'build.1', + }), ]) def test_should_parse_version(version, expected): result = parse(version) From dfc2270c680221e59568abc37f101e4e051f66a0 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 25 Nov 2019 09:31:05 +0100 Subject: [PATCH 045/219] Fix #201: Reformat source code with black (#202) * Fix #201: Reformat source code with black * Add `pyproject.toml` containing black configuration * Add batch image in `README.rst` * Reformat source code with `black` * Add config for flake8 Use max-line-length of 88 (default for black) --- CHANGELOG.rst | 2 +- README.rst | 5 +- docs/conf.py | 92 +++--- pyproject.toml | 20 ++ semver.py | 214 +++++++------- setup.cfg | 11 + setup.py | 71 ++--- test_semver.py | 765 +++++++++++++++++++++++++++---------------------- 8 files changed, 642 insertions(+), 538 deletions(-) create mode 100644 pyproject.toml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 53079d3c..df88ddcc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,7 +19,7 @@ Features * :gh:`187` (:pr:`188`): Added logo for python-semver organization and documentation * :gh:`191` (:pr:`194`): Created manpage for pysemver * :gh:`196` (:pr:`197`): Added distribution specific installation instructions - +* :gh:`201` (:pr:`202`): Reformatted source code with black Bug Fixes --------- diff --git a/README.rst b/README.rst index 62e6b92a..5c35b423 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Quickstart A Python module for `semantic versioning`_. Simplifies comparing versions. -|build-status| |python-support| |downloads| |license| |docs| +|build-status| |python-support| |downloads| |license| |docs| |black| .. teaser-end @@ -125,3 +125,6 @@ There are other functions to discover. Read on! :target: http://python-semver.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. _semantic versioning: http://semver.org/ +.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Black Formatter diff --git a/docs/conf.py b/docs/conf.py index 107765f2..a07c94a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,8 @@ # import os import sys -sys.path.insert(0, os.path.abspath('..')) + +sys.path.insert(0, os.path.abspath("..")) from semver import __version__ # noqa: E402 @@ -32,27 +33,27 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.napoleon', - 'sphinx.ext.extlinks', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.extlinks", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'python-semver' -copyright = '2018, Kostiantyn Rybnikov and all' -author = 'Kostiantyn Rybnikov and all' +project = "python-semver" +copyright = "2018, Kostiantyn Rybnikov and all" +author = "Kostiantyn Rybnikov and all" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -73,21 +74,20 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # Markup to shorten external links # See https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html -extlinks = {'gh': ('https://github.com/python-semver/python-semver/issues/%s', - '#'), - 'pr': ('https://github.com/python-semver/python-semver/pull/%s', - 'PR #'), - } +extlinks = { + "gh": ("https://github.com/python-semver/python-semver/issues/%s", "#"), + "pr": ("https://github.com/python-semver/python-semver/pull/%s", "PR #"), +} # -- Options for HTML output ---------------------------------------------- @@ -95,7 +95,7 @@ # a list of builtin themes. # # html_theme = 'alabaster' -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -106,9 +106,9 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] -html_css_files = ['css/default.css'] +html_css_files = ["css/default.css"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -116,12 +116,12 @@ # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - 'donate.html', + "**": [ + "about.html", + "navigation.html", + "relations.html", # needs 'show_related': True theme option to display + "searchbox.html", + "donate.html", ] } @@ -130,7 +130,7 @@ # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'semverdoc' +htmlhelp_basename = "semverdoc" # -- Options for LaTeX output --------------------------------------------- @@ -139,15 +139,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -157,8 +154,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'semver.tex', 'python-semver Documentation', - 'Kostiantyn Rybnikov and all', 'manual'), + ( + master_doc, + "semver.tex", + "python-semver Documentation", + "Kostiantyn Rybnikov and all", + "manual", + ) ] @@ -169,11 +171,13 @@ manpage_doc = "pysemver" man_pages = [ - (manpage_doc, - 'pysemver', - 'Helper script for Semantic Versioning', - ["Thomas Schraitle"], - 1) + ( + manpage_doc, + "pysemver", + "Helper script for Semantic Versioning", + ["Thomas Schraitle"], + 1, + ) ] @@ -183,7 +187,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'semver', 'python-semver Documentation', - author, 'semver', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "semver", + "python-semver Documentation", + author, + "semver", + "One line description of project.", + "Miscellaneous", + ) ] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..eca41891 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.black] +line-length = 88 +target-version = ['py37'] +include = '\.pyi?$' +# diff = true +exclude = ''' +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.mypy_cache + | \.tox + | \.venv + | \.env + | _build + | build + | dist + )/ +) +''' diff --git a/semver.py b/semver.py index af7dec66..efe1102e 100644 --- a/semver.py +++ b/semver.py @@ -10,14 +10,14 @@ import sys -__version__ = '2.9.0' -__author__ = 'Kostiantyn Rybnikov' -__author_email__ = 'k-bx@k-bx.com' -__maintainer__ = 'Sebastien Celles' +__version__ = "2.9.0" +__author__ = "Kostiantyn Rybnikov" +__author_email__ = "k-bx@k-bx.com" +__maintainer__ = "Sebastien Celles" __maintainer_email__ = "s.celles@gmail.com" _REGEX = re.compile( - r""" + r""" ^ (?P0|[1-9]\d*) \. @@ -33,15 +33,18 @@ (?:\.[0-9a-zA-Z-]+)* ))? $ - """, re.VERBOSE) + """, + re.VERBOSE, +) -_LAST_NUMBER = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+') +_LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") #: Contains the implemented semver.org version of the spec SEMVER_SPEC_VERSION = "2.0.0" -if not hasattr(__builtins__, 'cmp'): +if not hasattr(__builtins__, "cmp"): + def cmp(a, b): return (a > b) - (a < b) @@ -69,26 +72,29 @@ def parse(version): """ match = _REGEX.match(version) if match is None: - raise ValueError('%s is not valid SemVer string' % version) + raise ValueError("%s is not valid SemVer string" % version) version_parts = match.groupdict() - version_parts['major'] = int(version_parts['major']) - version_parts['minor'] = int(version_parts['minor']) - version_parts['patch'] = int(version_parts['patch']) + version_parts["major"] = int(version_parts["major"]) + version_parts["minor"] = int(version_parts["minor"]) + version_parts["patch"] = int(version_parts["patch"]) return version_parts def comparator(operator): """ Wrap a VersionInfo binary op method in a type-check """ + @wraps(operator) def wrapper(self, other): comparable_types = (VersionInfo, dict, tuple) if not isinstance(other, comparable_types): - raise TypeError("other type %r must be in %r" - % (type(other), comparable_types)) + raise TypeError( + "other type %r must be in %r" % (type(other), comparable_types) + ) return operator(self, other) + return wrapper @@ -101,7 +107,8 @@ class VersionInfo(object): :param str prerelease: an optional prerelease string :param str build: an optional build string """ - __slots__ = ('_major', '_minor', '_patch', '_prerelease', '_build') + + __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") def __init__(self, major, minor=0, patch=0, prerelease=None, build=None): self._major = int(major) @@ -156,17 +163,18 @@ def build(self, value): raise AttributeError("attribute 'build' is readonly") def _astuple(self): - return (self.major, self.minor, self.patch, - self.prerelease, self.build) + return (self.major, self.minor, self.patch, self.prerelease, self.build) def _asdict(self): - return collections.OrderedDict(( - ("major", self.major), - ("minor", self.minor), - ("patch", self.patch), - ("prerelease", self.prerelease), - ("build", self.build) - )) + return collections.OrderedDict( + ( + ("major", self.major), + ("minor", self.minor), + ("patch", self.patch), + ("prerelease", self.prerelease), + ("build", self.build), + ) + ) def __iter__(self): """Implement iter(self).""" @@ -213,7 +221,7 @@ def bump_patch(self): """ return parse_version_info(bump_patch(str(self))) - def bump_prerelease(self, token='rc'): + def bump_prerelease(self, token="rc"): """Raise the prerelease part of the version, return a new object but leave self untouched @@ -228,7 +236,7 @@ def bump_prerelease(self, token='rc'): """ return parse_version_info(bump_prerelease(str(self), token)) - def bump_build(self, token='build'): + def bump_build(self, token="build"): """Raise the build part of the version, return a new object but leave self untouched @@ -268,8 +276,7 @@ def __ge__(self, other): return _compare_by_keys(self._asdict(), _to_dict(other)) >= 0 def __repr__(self): - s = ", ".join("%s=%r" % (key, val) - for key, val in self._asdict().items()) + s = ", ".join("%s=%r" % (key, val) for key, val in self._asdict().items()) return "%s(%s)" % (type(self).__name__, s) def __str__(self): @@ -308,10 +315,10 @@ def replace(self, **parts): return VersionInfo(**version) except TypeError: unknownkeys = set(parts) - set(self._asdict()) - error = ("replace() got %d unexpected keyword " - "argument(s): %s" % (len(unknownkeys), - ", ".join(unknownkeys)) - ) + error = "replace() got %d unexpected keyword " "argument(s): %s" % ( + len(unknownkeys), + ", ".join(unknownkeys), + ) raise TypeError(error) @@ -344,18 +351,22 @@ def parse_version_info(version): """ parts = parse(version) version_info = VersionInfo( - parts['major'], parts['minor'], parts['patch'], - parts['prerelease'], parts['build']) + parts["major"], + parts["minor"], + parts["patch"], + parts["prerelease"], + parts["build"], + ) return version_info def _nat_cmp(a, b): def convert(text): - return int(text) if re.match('^[0-9]+$', text) else text + return int(text) if re.match("^[0-9]+$", text) else text def split_key(key): - return [convert(c) for c in key.split('.')] + return [convert(c) for c in key.split(".")] def cmp_prerelease_tag(a, b): if isinstance(a, int) and isinstance(b, int): @@ -367,7 +378,7 @@ def cmp_prerelease_tag(a, b): else: return cmp(a, b) - a, b = a or '', b or '' + a, b = a or "", b or "" a_parts, b_parts = split_key(a), split_key(b) for sub_a, sub_b in zip(a_parts, b_parts): cmp_result = cmp_prerelease_tag(sub_a, sub_b) @@ -378,12 +389,12 @@ def cmp_prerelease_tag(a, b): def _compare_by_keys(d1, d2): - for key in ['major', 'minor', 'patch']: + for key in ["major", "minor", "patch"]: v = cmp(d1.get(key), d2.get(key)) if v: return v - rc1, rc2 = d1.get('prerelease'), d2.get('prerelease') + rc1, rc2 = d1.get("prerelease"), d2.get("prerelease") rccmp = _nat_cmp(rc1, rc2) if not rccmp: @@ -438,24 +449,26 @@ def match(version, match_expr): False """ prefix = match_expr[:2] - if prefix in ('>=', '<=', '==', '!='): + if prefix in (">=", "<=", "==", "!="): match_version = match_expr[2:] - elif prefix and prefix[0] in ('>', '<'): + elif prefix and prefix[0] in (">", "<"): prefix = prefix[0] match_version = match_expr[1:] else: - raise ValueError("match_expr parameter should be in format , " - "where is one of " - "['<', '>', '==', '<=', '>=', '!=']. " - "You provided: %r" % match_expr) + raise ValueError( + "match_expr parameter should be in format , " + "where is one of " + "['<', '>', '==', '<=', '>=', '!=']. " + "You provided: %r" % match_expr + ) possibilities_dict = { - '>': (1,), - '<': (-1,), - '==': (0,), - '!=': (-1, 1), - '>=': (0, 1), - '<=': (-1, 0) + ">": (1,), + "<": (-1,), + "==": (0,), + "!=": (-1, 1), + ">=": (0, 1), + "<=": (-1, 0), } possibilities = possibilities_dict[prefix] @@ -533,7 +546,7 @@ def _increment_string(string): if match: next_ = str(int(match.group(1)) + 1) start, end = match.span(1) - string = string[:max(end - len(next_), start)] + next_ + string[end:] + string = string[: max(end - len(next_), start)] + next_ + string[end:] return string @@ -548,7 +561,7 @@ def bump_major(version): '4.0.0' """ verinfo = parse(version) - return format_version(verinfo['major'] + 1, 0, 0) + return format_version(verinfo["major"] + 1, 0, 0) def bump_minor(version): @@ -562,7 +575,7 @@ def bump_minor(version): '3.5.0' """ verinfo = parse(version) - return format_version(verinfo['major'], verinfo['minor'] + 1, 0) + return format_version(verinfo["major"], verinfo["minor"] + 1, 0) def bump_patch(version): @@ -576,11 +589,10 @@ def bump_patch(version): '3.4.6' """ verinfo = parse(version) - return format_version(verinfo['major'], verinfo['minor'], - verinfo['patch'] + 1) + return format_version(verinfo["major"], verinfo["minor"], verinfo["patch"] + 1) -def bump_prerelease(version, token='rc'): +def bump_prerelease(version, token="rc"): """Raise the prerelease part of the version :param version: version string @@ -592,14 +604,15 @@ def bump_prerelease(version, token='rc'): '3.4.5-dev.1' """ verinfo = parse(version) - verinfo['prerelease'] = _increment_string( - verinfo['prerelease'] or (token or 'rc') + '.0' + verinfo["prerelease"] = _increment_string( + verinfo["prerelease"] or (token or "rc") + ".0" + ) + return format_version( + verinfo["major"], verinfo["minor"], verinfo["patch"], verinfo["prerelease"] ) - return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'], - verinfo['prerelease']) -def bump_build(version, token='build'): +def bump_build(version, token="build"): """Raise the build part of the version :param version: version string @@ -611,11 +624,14 @@ def bump_build(version, token='build'): '3.4.5-rc.1+build.10' """ verinfo = parse(version) - verinfo['build'] = _increment_string( - verinfo['build'] or (token or 'build') + '.0' + verinfo["build"] = _increment_string(verinfo["build"] or (token or "build") + ".0") + return format_version( + verinfo["major"], + verinfo["minor"], + verinfo["patch"], + verinfo["prerelease"], + verinfo["build"], ) - return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'], - verinfo['prerelease'], verinfo['build']) def finalize_version(version): @@ -629,7 +645,7 @@ def finalize_version(version): '1.2.3' """ verinfo = parse(version) - return format_version(verinfo['major'], verinfo['minor'], verinfo['patch']) + return format_version(verinfo["major"], verinfo["minor"], verinfo["patch"]) def createparser(): @@ -638,49 +654,33 @@ def createparser(): :return: parser instance :rtype: :class:`argparse.ArgumentParser` """ - parser = argparse.ArgumentParser(prog=__package__, - description=__doc__) + parser = argparse.ArgumentParser(prog=__package__, description=__doc__) - parser.add_argument('--version', - action='version', - version='%(prog)s ' + __version__ - ) + parser.add_argument( + "--version", action="version", version="%(prog)s " + __version__ + ) s = parser.add_subparsers() # create compare subcommand - parser_compare = s.add_parser("compare", - help="Compare two versions" - ) + parser_compare = s.add_parser("compare", help="Compare two versions") parser_compare.set_defaults(which="compare") - parser_compare.add_argument("version1", - help="First version" - ) - parser_compare.add_argument("version2", - help="Second version" - ) + parser_compare.add_argument("version1", help="First version") + parser_compare.add_argument("version2", help="Second version") # create bump subcommand - parser_bump = s.add_parser("bump", - help="Bumps a version" - ) + parser_bump = s.add_parser("bump", help="Bumps a version") parser_bump.set_defaults(which="bump") - sb = parser_bump.add_subparsers(title="Bump commands", - dest="bump") + sb = parser_bump.add_subparsers(title="Bump commands", dest="bump") # Create subparsers for the bump subparser: - for p in (sb.add_parser("major", - help="Bump the major part of the version"), - sb.add_parser("minor", - help="Bump the minor part of the version"), - sb.add_parser("patch", - help="Bump the patch part of the version"), - sb.add_parser("prerelease", - help="Bump the prerelease part of the version"), - sb.add_parser("build", - help="Bump the build part of the version")): - p.add_argument("version", - help="Version to raise" - ) + for p in ( + sb.add_parser("major", help="Bump the major part of the version"), + sb.add_parser("minor", help="Bump the minor part of the version"), + sb.add_parser("patch", help="Bump the patch part of the version"), + sb.add_parser("prerelease", help="Bump the prerelease part of the version"), + sb.add_parser("build", help="Bump the build part of the version"), + ): + p.add_argument("version", help="Version to raise") return parser @@ -698,12 +698,13 @@ def process(args): args.parser.print_help() raise SystemExit() elif args.which == "bump": - maptable = {'major': 'bump_major', - 'minor': 'bump_minor', - 'patch': 'bump_patch', - 'prerelease': 'bump_prerelease', - 'build': 'bump_build', - } + maptable = { + "major": "bump_major", + "minor": "bump_minor", + "patch": "bump_patch", + "prerelease": "bump_prerelease", + "build": "bump_build", + } if args.bump is None: # When bump is called without arguments, # print the help and exit @@ -759,4 +760,5 @@ def replace(version, **parts): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/setup.cfg b/setup.cfg index 7967d292..b8a0ea49 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,14 @@ addopts = --cov-report=term-missing --doctest-modules --doctest-report ndiff + +[flake8] +max-line-length = 88 +exclude = + .env, + .eggs, + .tox, + .git, + __pycache__, + build, + dist \ No newline at end of file diff --git a/setup.py b/setup.py index 7c31635b..77f19e73 100755 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ from os.path import dirname, join from setuptools import setup from setuptools.command.test import test as TestCommand + try: from setuptools.command.clean import clean as CleanCommand except ImportError: @@ -14,7 +15,7 @@ class Tox(TestCommand): - user_options = [('tox-args=', 'a', "Arguments to pass to tox")] + user_options = [("tox-args=", "a", "Arguments to pass to tox")] def initialize_options(self): TestCommand.initialize_options(self) @@ -27,6 +28,7 @@ def finalize_options(self): def run_tests(self): from tox import cmdline + args = self.tox_args if args: args = split(self.tox_args) @@ -37,35 +39,25 @@ def run_tests(self): class Clean(CleanCommand): def run(self): super(CleanCommand, self).run() - delete_in_root = [ - 'build', - '.cache', - 'dist', - '.eggs', - '*.egg-info', - '.tox', - ] - delete_everywhere = [ - '__pycache__', - '*.pyc', - ] + delete_in_root = ["build", ".cache", "dist", ".eggs", "*.egg-info", ".tox"] + delete_everywhere = ["__pycache__", "*.pyc"] for candidate in delete_in_root: rmtree_glob(candidate) - for visible_dir in glob('[A-Za-z0-9]*'): + for visible_dir in glob("[A-Za-z0-9]*"): for candidate in delete_everywhere: rmtree_glob(join(visible_dir, candidate)) - rmtree_glob(join(visible_dir, '*', candidate)) + rmtree_glob(join(visible_dir, "*", candidate)) def rmtree_glob(file_glob): for fobj in glob(file_glob): try: rmtree(fobj) - print('%s/ removed ...' % fobj) + print("%s/ removed ..." % fobj) except OSError: try: remove(fobj) - print('%s removed ...' % fobj) + print("%s removed ..." % fobj) except OSError: pass @@ -79,35 +71,30 @@ def read_file(filename): name=package.__name__, version=package.__version__, description=package.__doc__.strip(), - long_description=read_file('README.rst'), + long_description=read_file("README.rst"), author=package.__author__, author_email=package.__author_email__, - url='https://github.com/python-semver/python-semver', - download_url='https://github.com/python-semver/python-semver/downloads', + url="https://github.com/python-semver/python-semver", + download_url="https://github.com/python-semver/python-semver/downloads", py_modules=[package.__name__], include_package_data=True, - license='BSD', + license="BSD", classifiers=[ - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Software Development :: Libraries :: Python Modules', + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Topic :: Software Development :: Libraries :: Python Modules", ], - tests_require=['tox', 'virtualenv'], - cmdclass={ - 'clean': Clean, - 'test': Tox, - }, - entry_points={ - 'console_scripts': ['pysemver = semver:main'], - } + tests_require=["tox", "virtualenv"], + cmdclass={"clean": Clean, "test": Tox}, + entry_points={"console_scripts": ["pysemver = semver:main"]}, ) diff --git a/test_semver.py b/test_semver.py index 053031f6..b3d0929d 100644 --- a/test_semver.py +++ b/test_semver.py @@ -1,154 +1,181 @@ from argparse import Namespace import pytest # noqa -from semver import (VersionInfo, - bump_build, - bump_major, - bump_minor, - bump_patch, - bump_prerelease, - compare, - createparser, - finalize_version, - format_version, - main, - match, - max_ver, - min_ver, - parse, - parse_version_info, - process, - replace, - ) +from semver import ( + VersionInfo, + bump_build, + bump_major, + bump_minor, + bump_patch, + bump_prerelease, + compare, + createparser, + finalize_version, + format_version, + main, + match, + max_ver, + min_ver, + parse, + parse_version_info, + process, + replace, +) SEMVERFUNCS = [ - compare, createparser, - bump_build, bump_major, bump_minor, bump_patch, bump_prerelease, - finalize_version, format_version, - match, max_ver, min_ver, parse, process, replace, - ] - - -@pytest.mark.parametrize("string,expected", [ - ("rc", "rc"), - ("rc.1", "rc.2"), - ("2x", "3x"), -]) + compare, + createparser, + bump_build, + bump_major, + bump_minor, + bump_patch, + bump_prerelease, + finalize_version, + format_version, + match, + max_ver, + min_ver, + parse, + process, + replace, +] + + +@pytest.mark.parametrize( + "string,expected", [("rc", "rc"), ("rc.1", "rc.2"), ("2x", "3x")] +) def test_should_private_increment_string(string, expected): from semver import _increment_string + assert _increment_string(string) == expected @pytest.fixture def version(): - return VersionInfo(major=1, minor=2, patch=3, - prerelease='alpha.1.2', build='build.11.e0f985a') + return VersionInfo( + major=1, minor=2, patch=3, prerelease="alpha.1.2", build="build.11.e0f985a" + ) -@pytest.mark.parametrize("func", SEMVERFUNCS, - ids=[func.__name__ for func in SEMVERFUNCS]) +@pytest.mark.parametrize( + "func", SEMVERFUNCS, ids=[func.__name__ for func in SEMVERFUNCS] +) def test_fordocstrings(func): assert func.__doc__, "Need a docstring for function %r" % func.__name -@pytest.mark.parametrize("version,expected", [ - # no. 1 - ("1.2.3-alpha.1.2+build.11.e0f985a", - { - 'major': 1, - 'minor': 2, - 'patch': 3, - 'prerelease': 'alpha.1.2', - 'build': 'build.11.e0f985a', - }), - # no. 2 - ("1.2.3-alpha-1+build.11.e0f985a", - { - 'major': 1, - 'minor': 2, - 'patch': 3, - 'prerelease': 'alpha-1', - 'build': 'build.11.e0f985a', - }), - ("0.1.0-0f", - { - 'major': 0, - 'minor': 1, - 'patch': 0, - 'prerelease': '0f', - 'build': None, - }), - ("0.0.0-0foo.1", - { - 'major': 0, - 'minor': 0, - 'patch': 0, - 'prerelease': '0foo.1', - 'build': None, - }), - ("0.0.0-0foo.1+build.1", - { - 'major': 0, - 'minor': 0, - 'patch': 0, - 'prerelease': '0foo.1', - 'build': 'build.1', - }), -]) +@pytest.mark.parametrize( + "version,expected", + [ + # no. 1 + ( + "1.2.3-alpha.1.2+build.11.e0f985a", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "alpha.1.2", + "build": "build.11.e0f985a", + }, + ), + # no. 2 + ( + "1.2.3-alpha-1+build.11.e0f985a", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "alpha-1", + "build": "build.11.e0f985a", + }, + ), + ( + "0.1.0-0f", + {"major": 0, "minor": 1, "patch": 0, "prerelease": "0f", "build": None}, + ), + ( + "0.0.0-0foo.1", + {"major": 0, "minor": 0, "patch": 0, "prerelease": "0foo.1", "build": None}, + ), + ( + "0.0.0-0foo.1+build.1", + { + "major": 0, + "minor": 0, + "patch": 0, + "prerelease": "0foo.1", + "build": "build.1", + }, + ), + ], +) def test_should_parse_version(version, expected): result = parse(version) assert result == expected -@pytest.mark.parametrize("version,expected", [ - # no. 1 - ("1.2.3-rc.0+build.0", - { - 'major': 1, - 'minor': 2, - 'patch': 3, - 'prerelease': 'rc.0', - 'build': 'build.0', - }), - # no. 2 - ("1.2.3-rc.0.0+build.0", - { - 'major': 1, - 'minor': 2, - 'patch': 3, - 'prerelease': 'rc.0.0', - 'build': 'build.0', - }), -]) +@pytest.mark.parametrize( + "version,expected", + [ + # no. 1 + ( + "1.2.3-rc.0+build.0", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "rc.0", + "build": "build.0", + }, + ), + # no. 2 + ( + "1.2.3-rc.0.0+build.0", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "rc.0.0", + "build": "build.0", + }, + ), + ], +) def test_should_parse_zero_prerelease(version, expected): result = parse(version) assert result == expected -@pytest.mark.parametrize("left,right", [ - ("1.0.0", "2.0.0"), - ('1.0.0-alpha', '1.0.0-alpha.1'), - ('1.0.0-alpha.1', '1.0.0-alpha.beta'), - ('1.0.0-alpha.beta', '1.0.0-beta'), - ('1.0.0-beta', '1.0.0-beta.2'), - ('1.0.0-beta.2', '1.0.0-beta.11'), - ('1.0.0-beta.11', '1.0.0-rc.1'), - ('1.0.0-rc.1', '1.0.0'), -]) +@pytest.mark.parametrize( + "left,right", + [ + ("1.0.0", "2.0.0"), + ("1.0.0-alpha", "1.0.0-alpha.1"), + ("1.0.0-alpha.1", "1.0.0-alpha.beta"), + ("1.0.0-alpha.beta", "1.0.0-beta"), + ("1.0.0-beta", "1.0.0-beta.2"), + ("1.0.0-beta.2", "1.0.0-beta.11"), + ("1.0.0-beta.11", "1.0.0-rc.1"), + ("1.0.0-rc.1", "1.0.0"), + ], +) def test_should_get_less(left, right): assert compare(left, right) == -1 -@pytest.mark.parametrize("left,right", [ - ("2.0.0", "1.0.0"), - ('1.0.0-alpha.1', '1.0.0-alpha'), - ('1.0.0-alpha.beta', '1.0.0-alpha.1'), - ('1.0.0-beta', '1.0.0-alpha.beta'), - ('1.0.0-beta.2', '1.0.0-beta'), - ('1.0.0-beta.11', '1.0.0-beta.2'), - ('1.0.0-rc.1', '1.0.0-beta.11'), - ('1.0.0', '1.0.0-rc.1') -]) +@pytest.mark.parametrize( + "left,right", + [ + ("2.0.0", "1.0.0"), + ("1.0.0-alpha.1", "1.0.0-alpha"), + ("1.0.0-alpha.beta", "1.0.0-alpha.1"), + ("1.0.0-beta", "1.0.0-alpha.beta"), + ("1.0.0-beta.2", "1.0.0-beta"), + ("1.0.0-beta.11", "1.0.0-beta.2"), + ("1.0.0-rc.1", "1.0.0-beta.11"), + ("1.0.0", "1.0.0-rc.1"), + ], +) def test_should_get_greater(left, right): assert compare(left, right) == 1 @@ -161,34 +188,38 @@ def test_should_no_match_simple(): assert match("2.3.7", ">=2.3.8") is False -@pytest.mark.parametrize("left,right,expected", [ - ("2.3.7", "!=2.3.8", True), - ("2.3.7", "!=2.3.6", True), - ("2.3.7", "!=2.3.7", False), -]) +@pytest.mark.parametrize( + "left,right,expected", + [ + ("2.3.7", "!=2.3.8", True), + ("2.3.7", "!=2.3.6", True), + ("2.3.7", "!=2.3.7", False), + ], +) def test_should_match_not_equal(left, right, expected): assert match(left, right) is expected -@pytest.mark.parametrize("left,right,expected", [ - ("2.3.7", "<2.4.0", True), - ("2.3.7", ">2.3.5", True), - ("2.3.7", "<=2.3.9", True), - ("2.3.7", ">=2.3.5", True), - ("2.3.7", "==2.3.7", True), - ("2.3.7", "!=2.3.7", False), -]) -def test_should_not_raise_value_error_for_expected_match_expression(left, - right, - expected): +@pytest.mark.parametrize( + "left,right,expected", + [ + ("2.3.7", "<2.4.0", True), + ("2.3.7", ">2.3.5", True), + ("2.3.7", "<=2.3.9", True), + ("2.3.7", ">=2.3.5", True), + ("2.3.7", "==2.3.7", True), + ("2.3.7", "!=2.3.7", False), + ], +) +def test_should_not_raise_value_error_for_expected_match_expression( + left, right, expected +): assert match(left, right) is expected -@pytest.mark.parametrize("left,right", [ - ("2.3.7", "=2.3.7"), - ("2.3.7", "~2.3.7"), - ("2.3.7", "^2.3.7"), -]) +@pytest.mark.parametrize( + "left,right", [("2.3.7", "=2.3.7"), ("2.3.7", "~2.3.7"), ("2.3.7", "^2.3.7")] +) def test_should_raise_value_error_for_unexpected_match_expression(left, right): with pytest.raises(ValueError): match(left, right) @@ -200,21 +231,17 @@ def test_should_raise_value_error_for_zero_prefixed_versions(version): parse(version) -@pytest.mark.parametrize("left,right", [ - ('foo', 'bar'), - ('1.0', '1.0.0'), - ('1.x', '1.0.0'), -]) +@pytest.mark.parametrize( + "left,right", [("foo", "bar"), ("1.0", "1.0.0"), ("1.x", "1.0.0")] +) def test_should_raise_value_error_for_invalid_value(left, right): with pytest.raises(ValueError): compare(left, right) -@pytest.mark.parametrize("left,right", [ - ('1.0.0', ''), - ('1.0.0', '!'), - ('1.0.0', '1.0.0'), -]) +@pytest.mark.parametrize( + "left,right", [("1.0.0", ""), ("1.0.0", "!"), ("1.0.0", "1.0.0")] +) def test_should_raise_value_error_for_invalid_match_expression(left, right): with pytest.raises(ValueError): match(left, right) @@ -229,78 +256,88 @@ def test_should_follow_specification_comparison(): and in backward too. """ chain = [ - '1.0.0-alpha', '1.0.0-alpha.1', '1.0.0-beta.2', '1.0.0-beta.11', - '1.0.0-rc.1', '1.0.0', '1.3.7+build', + "1.0.0-alpha", + "1.0.0-alpha.1", + "1.0.0-beta.2", + "1.0.0-beta.11", + "1.0.0-rc.1", + "1.0.0", + "1.3.7+build", ] versions = zip(chain[:-1], chain[1:]) for low_version, high_version in versions: - assert compare(low_version, high_version) == -1, \ - '%s should be lesser than %s' % (low_version, high_version) - assert compare(high_version, low_version) == 1, \ - '%s should be higher than %s' % (high_version, low_version) + assert ( + compare(low_version, high_version) == -1 + ), "%s should be lesser than %s" % (low_version, high_version) + assert ( + compare(high_version, low_version) == 1 + ), "%s should be higher than %s" % (high_version, low_version) -@pytest.mark.parametrize("left,right", [ - ('1.0.0-beta.2', '1.0.0-beta.11'), -]) +@pytest.mark.parametrize("left,right", [("1.0.0-beta.2", "1.0.0-beta.11")]) def test_should_compare_rc_builds(left, right): assert compare(left, right) == -1 -@pytest.mark.parametrize("left,right", [ - ('1.0.0-rc.1', '1.0.0'), - ('1.0.0-rc.1+build.1', '1.0.0'), -]) +@pytest.mark.parametrize( + "left,right", [("1.0.0-rc.1", "1.0.0"), ("1.0.0-rc.1+build.1", "1.0.0")] +) def test_should_compare_release_candidate_with_release(left, right): assert compare(left, right) == -1 -@pytest.mark.parametrize("left,right", [ - ('2.0.0', '2.0.0'), - ('1.1.9-rc.1', '1.1.9-rc.1'), - ('1.1.9+build.1', '1.1.9+build.1'), - ('1.1.9-rc.1+build.1', '1.1.9-rc.1+build.1'), -]) +@pytest.mark.parametrize( + "left,right", + [ + ("2.0.0", "2.0.0"), + ("1.1.9-rc.1", "1.1.9-rc.1"), + ("1.1.9+build.1", "1.1.9+build.1"), + ("1.1.9-rc.1+build.1", "1.1.9-rc.1+build.1"), + ], +) def test_should_say_equal_versions_are_equal(left, right): assert compare(left, right) == 0 -@pytest.mark.parametrize("left,right,expected", [ - ('1.1.9-rc.1', '1.1.9-rc.1+build.1', 0), - ('1.1.9-rc.1', '1.1.9+build.1', -1), -]) +@pytest.mark.parametrize( + "left,right,expected", + [("1.1.9-rc.1", "1.1.9-rc.1+build.1", 0), ("1.1.9-rc.1", "1.1.9+build.1", -1)], +) def test_should_compare_versions_with_build_and_release(left, right, expected): assert compare(left, right) == expected -@pytest.mark.parametrize("left,right,expected", [ - ('1.0.0+build.1', '1.0.0', 0), - ('1.0.0-alpha.1+build.1', '1.0.0-alpha.1', 0), - ('1.0.0+build.1', '1.0.0-alpha.1', 1), - ('1.0.0+build.1', '1.0.0-alpha.1+build.1', 1), -]) +@pytest.mark.parametrize( + "left,right,expected", + [ + ("1.0.0+build.1", "1.0.0", 0), + ("1.0.0-alpha.1+build.1", "1.0.0-alpha.1", 0), + ("1.0.0+build.1", "1.0.0-alpha.1", 1), + ("1.0.0+build.1", "1.0.0-alpha.1+build.1", 1), + ], +) def test_should_ignore_builds_on_compare(left, right, expected): assert compare(left, right) == expected def test_should_correctly_format_version(): - assert format_version(3, 4, 5) == '3.4.5' - assert format_version(3, 4, 5, 'rc.1') == '3.4.5-rc.1' - assert format_version(3, 4, 5, prerelease='rc.1') == '3.4.5-rc.1' - assert format_version(3, 4, 5, build='build.4') == '3.4.5+build.4' - assert format_version(3, 4, 5, 'rc.1', 'build.4') == '3.4.5-rc.1+build.4' + assert format_version(3, 4, 5) == "3.4.5" + assert format_version(3, 4, 5, "rc.1") == "3.4.5-rc.1" + assert format_version(3, 4, 5, prerelease="rc.1") == "3.4.5-rc.1" + assert format_version(3, 4, 5, build="build.4") == "3.4.5+build.4" + assert format_version(3, 4, 5, "rc.1", "build.4") == "3.4.5-rc.1+build.4" def test_should_bump_major(): - assert bump_major('3.4.5') == '4.0.0' + assert bump_major("3.4.5") == "4.0.0" def test_should_bump_minor(): - assert bump_minor('3.4.5') == '3.5.0' + assert bump_minor("3.4.5") == "3.5.0" def test_should_bump_patch(): - assert bump_patch('3.4.5') == '3.4.6' + assert bump_patch("3.4.5") == "3.4.6" def test_should_versioninfo_bump_major_and_minor(): @@ -344,159 +381,172 @@ def test_should_versioninfo_bump_multiple(): expected = parse_version_info("3.4.5-rc.2+build.2") assert v.bump_prerelease().bump_build().bump_build() == expected expected = parse_version_info("3.4.5-rc.3") - assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == \ - expected + assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == expected def test_should_ignore_extensions_for_bump(): - assert bump_patch('3.4.5-rc1+build4') == '3.4.6' + assert bump_patch("3.4.5-rc1+build4") == "3.4.6" def test_should_get_max(): - assert max_ver('3.4.5', '4.0.2') == '4.0.2' + assert max_ver("3.4.5", "4.0.2") == "4.0.2" def test_should_get_max_same(): - assert max_ver('3.4.5', '3.4.5') == '3.4.5' + assert max_ver("3.4.5", "3.4.5") == "3.4.5" def test_should_get_min(): - assert min_ver('3.4.5', '4.0.2') == '3.4.5' + assert min_ver("3.4.5", "4.0.2") == "3.4.5" def test_should_get_min_same(): - assert min_ver('3.4.5', '3.4.5') == '3.4.5' + assert min_ver("3.4.5", "3.4.5") == "3.4.5" def test_should_get_more_rc1(): assert compare("1.0.0-rc1", "1.0.0-rc0") == 1 -@pytest.mark.parametrize("left,right,expected", [ - ('1.2.3-rc.2', '1.2.3-rc.10', '1.2.3-rc.2'), - ('1.2.3-rc2', '1.2.3-rc10', '1.2.3-rc10'), - # identifiers with letters or hyphens are compared lexically in ASCII sort - # order. - ('1.2.3-Rc10', '1.2.3-rc10', '1.2.3-Rc10'), - # Numeric identifiers always have lower precedence than non-numeric - # identifiers. - ('1.2.3-2', '1.2.3-rc', '1.2.3-2'), - # A larger set of pre-release fields has a higher precedence than a - # smaller set, if all of the preceding identifiers are equal. - ('1.2.3-rc.2.1', '1.2.3-rc.2', '1.2.3-rc.2'), - # When major, minor, and patch are equal, a pre-release version has lower - # precedence than a normal version. - ('1.2.3', '1.2.3-1', '1.2.3-1'), - ('1.0.0-alpha', '1.0.0-alpha.1', '1.0.0-alpha') -]) +@pytest.mark.parametrize( + "left,right,expected", + [ + ("1.2.3-rc.2", "1.2.3-rc.10", "1.2.3-rc.2"), + ("1.2.3-rc2", "1.2.3-rc10", "1.2.3-rc10"), + # identifiers with letters or hyphens are compared lexically in ASCII sort + # order. + ("1.2.3-Rc10", "1.2.3-rc10", "1.2.3-Rc10"), + # Numeric identifiers always have lower precedence than non-numeric + # identifiers. + ("1.2.3-2", "1.2.3-rc", "1.2.3-2"), + # A larger set of pre-release fields has a higher precedence than a + # smaller set, if all of the preceding identifiers are equal. + ("1.2.3-rc.2.1", "1.2.3-rc.2", "1.2.3-rc.2"), + # When major, minor, and patch are equal, a pre-release version has lower + # precedence than a normal version. + ("1.2.3", "1.2.3-1", "1.2.3-1"), + ("1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-alpha"), + ], +) def test_prerelease_order(left, right, expected): assert min_ver(left, right) == expected -@pytest.mark.parametrize("version,token,expected", [ - ('3.4.5-rc.9', None, '3.4.5-rc.10'), - ('3.4.5', None, '3.4.5-rc.1'), - ('3.4.5', 'dev', '3.4.5-dev.1'), - ('3.4.5', '', '3.4.5-rc.1'), -]) +@pytest.mark.parametrize( + "version,token,expected", + [ + ("3.4.5-rc.9", None, "3.4.5-rc.10"), + ("3.4.5", None, "3.4.5-rc.1"), + ("3.4.5", "dev", "3.4.5-dev.1"), + ("3.4.5", "", "3.4.5-rc.1"), + ], +) def test_should_bump_prerelease(version, token, expected): token = "rc" if not token else token assert bump_prerelease(version, token) == expected def test_should_ignore_build_on_prerelease_bump(): - assert bump_prerelease('3.4.5-rc.1+build.4') == '3.4.5-rc.2' - - -@pytest.mark.parametrize("version,expected", [ - ('3.4.5-rc.1+build.9', '3.4.5-rc.1+build.10'), - ('3.4.5-rc.1+0009.dev', '3.4.5-rc.1+0010.dev'), - ('3.4.5-rc.1', '3.4.5-rc.1+build.1'), - ('3.4.5', '3.4.5+build.1'), -]) + assert bump_prerelease("3.4.5-rc.1+build.4") == "3.4.5-rc.2" + + +@pytest.mark.parametrize( + "version,expected", + [ + ("3.4.5-rc.1+build.9", "3.4.5-rc.1+build.10"), + ("3.4.5-rc.1+0009.dev", "3.4.5-rc.1+0010.dev"), + ("3.4.5-rc.1", "3.4.5-rc.1+build.1"), + ("3.4.5", "3.4.5+build.1"), + ], +) def test_should_bump_build(version, expected): assert bump_build(version) == expected -@pytest.mark.parametrize("version,expected", [ - ('1.2.3', '1.2.3'), - ('1.2.3-rc.5', '1.2.3'), - ('1.2.3+build.2', '1.2.3'), - ('1.2.3-rc.1+build.5', '1.2.3'), - ('1.2.3-alpha', '1.2.3'), - ('1.2.0', '1.2.0'), -]) +@pytest.mark.parametrize( + "version,expected", + [ + ("1.2.3", "1.2.3"), + ("1.2.3-rc.5", "1.2.3"), + ("1.2.3+build.2", "1.2.3"), + ("1.2.3-rc.1+build.5", "1.2.3"), + ("1.2.3-alpha", "1.2.3"), + ("1.2.0", "1.2.0"), + ], +) def test_should_finalize_version(version, expected): assert finalize_version(version) == expected def test_should_compare_version_info_objects(): v1 = VersionInfo(major=0, minor=10, patch=4) - v2 = VersionInfo( - major=0, minor=10, patch=4, prerelease='beta.1', build=None) + v2 = VersionInfo(major=0, minor=10, patch=4, prerelease="beta.1", build=None) # use `not` to enforce using comparision operators assert v1 != v2 assert v1 > v2 assert v1 >= v2 - assert not(v1 < v2) - assert not(v1 <= v2) - assert not(v1 == v2) + assert not (v1 < v2) + assert not (v1 <= v2) + assert not (v1 == v2) v3 = VersionInfo(major=0, minor=10, patch=4) - assert not(v1 != v3) - assert not(v1 > v3) + assert not (v1 != v3) + assert not (v1 > v3) assert v1 >= v3 - assert not(v1 < v3) + assert not (v1 < v3) assert v1 <= v3 assert v1 == v3 v4 = VersionInfo(major=0, minor=10, patch=5) assert v1 != v4 - assert not(v1 > v4) - assert not(v1 >= v4) + assert not (v1 > v4) + assert not (v1 >= v4) assert v1 < v4 assert v1 <= v4 - assert not(v1 == v4) + assert not (v1 == v4) def test_should_compare_version_dictionaries(): v1 = VersionInfo(major=0, minor=10, patch=4) - v2 = dict(major=0, minor=10, patch=4, prerelease='beta.1', build=None) + v2 = dict(major=0, minor=10, patch=4, prerelease="beta.1", build=None) assert v1 != v2 assert v1 > v2 assert v1 >= v2 - assert not(v1 < v2) - assert not(v1 <= v2) - assert not(v1 == v2) + assert not (v1 < v2) + assert not (v1 <= v2) + assert not (v1 == v2) v3 = dict(major=0, minor=10, patch=4) - assert not(v1 != v3) - assert not(v1 > v3) + assert not (v1 != v3) + assert not (v1 > v3) assert v1 >= v3 - assert not(v1 < v3) + assert not (v1 < v3) assert v1 <= v3 assert v1 == v3 v4 = dict(major=0, minor=10, patch=5) assert v1 != v4 - assert not(v1 > v4) - assert not(v1 >= v4) + assert not (v1 > v4) + assert not (v1 >= v4) assert v1 < v4 assert v1 <= v4 - assert not(v1 == v4) + assert not (v1 == v4) def test_should_compare_version_tuples(): - v0 = VersionInfo(major=0, minor=4, patch=5, - prerelease='pre.2', build='build.4') - v1 = VersionInfo(major=3, minor=4, patch=5, - prerelease='pre.2', build='build.4') - for t in ((1, 0, 0), (1, 0), (1,), (1, 0, 0, 'pre.2'), - (1, 0, 0, 'pre.2', 'build.4')): + v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + for t in ( + (1, 0, 0), + (1, 0), + (1,), + (1, 0, 0, "pre.2"), + (1, 0, 0, "pre.2", "build.4"), + ): assert v0 < t assert v0 <= t assert v0 != t @@ -513,8 +563,7 @@ def test_should_compare_version_tuples(): def test_should_not_allow_to_compare_version_with_string(): - v1 = VersionInfo(major=3, minor=4, patch=5, - prerelease='pre.2', build='build.4') + v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") with pytest.raises(TypeError): v1 > "1.0.0" with pytest.raises(TypeError): @@ -522,8 +571,7 @@ def test_should_not_allow_to_compare_version_with_string(): def test_should_not_allow_to_compare_version_with_int(): - v1 = VersionInfo(major=3, minor=4, patch=5, - prerelease='pre.2', build='build.4') + v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") with pytest.raises(TypeError): v1 > 1 with pytest.raises(TypeError): @@ -531,8 +579,8 @@ def test_should_not_allow_to_compare_version_with_int(): def test_should_compare_prerelease_with_numbers_and_letters(): - v1 = VersionInfo(major=1, minor=9, patch=1, prerelease='1unms', build=None) - v2 = VersionInfo(major=1, minor=9, patch=1, prerelease=None, build='1asd') + v1 = VersionInfo(major=1, minor=9, patch=1, prerelease="1unms", build=None) + v2 = VersionInfo(major=1, minor=9, patch=1, prerelease=None, build="1asd") assert v1 < v2 assert compare("1.9.1-1unms", "1.9.1+1") == -1 @@ -567,76 +615,96 @@ def test_immutable_patch(version): def test_immutable_prerelease(version): - with pytest.raises(AttributeError, - match="attribute 'prerelease' is readonly"): - version.prerelease = 'alpha.9.9' + with pytest.raises(AttributeError, match="attribute 'prerelease' is readonly"): + version.prerelease = "alpha.9.9" def test_immutable_build(version): with pytest.raises(AttributeError, match="attribute 'build' is readonly"): - version.build = 'build.99.e0f985a' + version.build = "build.99.e0f985a" def test_immutable_unknown_attribute(version): # "no new attribute can be set" with pytest.raises(AttributeError): - version.new_attribute = 'forbidden' + version.new_attribute = "forbidden" def test_version_info_should_be_iterable(version): - assert tuple(version) == (version.major, version.minor, version.patch, - version.prerelease, version.build) + assert tuple(version) == ( + version.major, + version.minor, + version.patch, + version.prerelease, + version.build, + ) def test_should_compare_prerelease_and_build_with_numbers(): - assert VersionInfo(major=1, minor=9, patch=1, prerelease=1, build=1) < \ - VersionInfo(major=1, minor=9, patch=1, prerelease=2, build=1) + assert VersionInfo(major=1, minor=9, patch=1, prerelease=1, build=1) < VersionInfo( + major=1, minor=9, patch=1, prerelease=2, build=1 + ) assert VersionInfo(1, 9, 1, 1, 1) < VersionInfo(1, 9, 1, 2, 1) - assert VersionInfo('2') < VersionInfo(10) - assert VersionInfo('2') < VersionInfo('10') + assert VersionInfo("2") < VersionInfo(10) + assert VersionInfo("2") < VersionInfo("10") def test_should_be_able_to_use_strings_as_major_minor_patch(): - v = VersionInfo('1', '2', '3') + v = VersionInfo("1", "2", "3") assert isinstance(v.major, int) assert isinstance(v.minor, int) assert isinstance(v.patch, int) assert v.prerelease is None assert v.build is None - assert VersionInfo('1', '2', '3') == VersionInfo(1, 2, 3) + assert VersionInfo("1", "2", "3") == VersionInfo(1, 2, 3) def test_using_non_numeric_string_as_major_minor_patch_throws(): with pytest.raises(ValueError): - VersionInfo('a') + VersionInfo("a") with pytest.raises(ValueError): - VersionInfo(1, 'a') + VersionInfo(1, "a") with pytest.raises(ValueError): - VersionInfo(1, 2, 'a') + VersionInfo(1, 2, "a") def test_should_be_able_to_use_integers_as_prerelease_build(): v = VersionInfo(1, 2, 3, 4, 5) assert isinstance(v.prerelease, str) assert isinstance(v.build, str) - assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, '4', '5') - - -@pytest.mark.parametrize("cli,expected", [ - (["bump", "major", "1.2.3"], - Namespace(which='bump', bump='major', version='1.2.3')), - (["bump", "minor", "1.2.3"], - Namespace(which='bump', bump='minor', version='1.2.3')), - (["bump", "patch", "1.2.3"], - Namespace(which='bump', bump='patch', version='1.2.3')), - (["bump", "prerelease", "1.2.3"], - Namespace(which='bump', bump='prerelease', version='1.2.3')), - (["bump", "build", "1.2.3"], - Namespace(which='bump', bump='build', version='1.2.3')), - # --- - (["compare", "1.2.3", "2.1.3"], - Namespace(which='compare', version1='1.2.3', version2='2.1.3')), -]) + assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, "4", "5") + + +@pytest.mark.parametrize( + "cli,expected", + [ + ( + ["bump", "major", "1.2.3"], + Namespace(which="bump", bump="major", version="1.2.3"), + ), + ( + ["bump", "minor", "1.2.3"], + Namespace(which="bump", bump="minor", version="1.2.3"), + ), + ( + ["bump", "patch", "1.2.3"], + Namespace(which="bump", bump="patch", version="1.2.3"), + ), + ( + ["bump", "prerelease", "1.2.3"], + Namespace(which="bump", bump="prerelease", version="1.2.3"), + ), + ( + ["bump", "build", "1.2.3"], + Namespace(which="bump", bump="build", version="1.2.3"), + ), + # --- + ( + ["compare", "1.2.3", "2.1.3"], + Namespace(which="compare", version1="1.2.3", version2="2.1.3"), + ), + ], +) def test_should_parse_cli_arguments(cli, expected): parser = createparser() assert parser @@ -644,26 +712,24 @@ def test_should_parse_cli_arguments(cli, expected): assert result == expected -@pytest.mark.parametrize("args,expected", [ - # bump subcommand - (Namespace(which='bump', bump='major', version='1.2.3'), - "2.0.0"), - (Namespace(which='bump', bump='minor', version='1.2.3'), - "1.3.0"), - (Namespace(which='bump', bump='patch', version='1.2.3'), - "1.2.4"), - (Namespace(which='bump', bump='prerelease', version='1.2.3-rc1'), - "1.2.3-rc2"), - (Namespace(which='bump', bump='build', version='1.2.3+build.13'), - "1.2.3+build.14"), - # compare subcommand - (Namespace(which='compare', version1='1.2.3', version2='2.1.3'), - "-1"), - (Namespace(which='compare', version1='1.2.3', version2='1.2.3'), - "0"), - (Namespace(which='compare', version1='2.4.0', version2='2.1.3'), - "1"), -]) +@pytest.mark.parametrize( + "args,expected", + [ + # bump subcommand + (Namespace(which="bump", bump="major", version="1.2.3"), "2.0.0"), + (Namespace(which="bump", bump="minor", version="1.2.3"), "1.3.0"), + (Namespace(which="bump", bump="patch", version="1.2.3"), "1.2.4"), + (Namespace(which="bump", bump="prerelease", version="1.2.3-rc1"), "1.2.3-rc2"), + ( + Namespace(which="bump", bump="build", version="1.2.3+build.13"), + "1.2.3+build.14", + ), + # compare subcommand + (Namespace(which="compare", version1="1.2.3", version2="2.1.3"), "-1"), + (Namespace(which="compare", version1="1.2.3", version2="1.2.3"), "0"), + (Namespace(which="compare", version1="2.4.0", version2="2.1.3"), "1"), + ], +) def test_should_process_parsed_cli_arguments(args, expected): assert process(args) == expected @@ -692,20 +758,25 @@ def test_should_raise_systemexit_when_bump_iscalled_with_empty_arguments(): main(["bump"]) -@pytest.mark.parametrize("version,parts,expected", [ - ("3.4.5", dict(major=2), '2.4.5'), - ("3.4.5", dict(major="2"), '2.4.5'), - ("3.4.5", dict(major=2, minor=5), '2.5.5'), - ("3.4.5", dict(minor=2), '3.2.5'), - ("3.4.5", dict(major=2, minor=5, patch=10), '2.5.10'), - ("3.4.5", dict(major=2, minor=5, patch=10, prerelease="rc1"), - '2.5.10-rc1'), - ("3.4.5", dict(major=2, minor=5, patch=10, prerelease="rc1", build="b1"), - '2.5.10-rc1+b1'), - ("3.4.5-alpha.1.2", dict(major=2), '2.4.5-alpha.1.2'), - ("3.4.5-alpha.1.2", dict(build="x1"), '3.4.5-alpha.1.2+x1'), - ("3.4.5+build1", dict(major=2), '2.4.5+build1'), -]) +@pytest.mark.parametrize( + "version,parts,expected", + [ + ("3.4.5", dict(major=2), "2.4.5"), + ("3.4.5", dict(major="2"), "2.4.5"), + ("3.4.5", dict(major=2, minor=5), "2.5.5"), + ("3.4.5", dict(minor=2), "3.2.5"), + ("3.4.5", dict(major=2, minor=5, patch=10), "2.5.10"), + ("3.4.5", dict(major=2, minor=5, patch=10, prerelease="rc1"), "2.5.10-rc1"), + ( + "3.4.5", + dict(major=2, minor=5, patch=10, prerelease="rc1", build="b1"), + "2.5.10-rc1+b1", + ), + ("3.4.5-alpha.1.2", dict(major=2), "2.4.5-alpha.1.2"), + ("3.4.5-alpha.1.2", dict(build="x1"), "3.4.5-alpha.1.2+x1"), + ("3.4.5+build1", dict(major=2), "2.4.5+build1"), + ], +) def test_replace_method_replaces_requested_parts(version, parts, expected): assert replace(version, **parts) == expected @@ -715,18 +786,18 @@ def test_replace_raises_TypeError_for_invalid_keyword_arg(): assert replace("1.2.3", unknown="should_raise") -@pytest.mark.parametrize("version,parts,expected", [ - ("3.4.5", dict(major=2, minor=5), '2.5.5'), - ("3.4.5", dict(major=2, minor=5, patch=10), '2.5.10'), - ("3.4.5-alpha.1.2", dict(major=2), '2.4.5-alpha.1.2'), - ("3.4.5-alpha.1.2", dict(build="x1"), '3.4.5-alpha.1.2+x1'), - ("3.4.5+build1", dict(major=2), '2.4.5+build1'), -]) -def test_should_return_versioninfo_with_replaced_parts(version, - parts, - expected): - assert VersionInfo.parse(version).replace(**parts) == \ - VersionInfo.parse(expected) +@pytest.mark.parametrize( + "version,parts,expected", + [ + ("3.4.5", dict(major=2, minor=5), "2.5.5"), + ("3.4.5", dict(major=2, minor=5, patch=10), "2.5.10"), + ("3.4.5-alpha.1.2", dict(major=2), "2.4.5-alpha.1.2"), + ("3.4.5-alpha.1.2", dict(build="x1"), "3.4.5-alpha.1.2+x1"), + ("3.4.5+build1", dict(major=2), "2.4.5+build1"), + ], +) +def test_should_return_versioninfo_with_replaced_parts(version, parts, expected): + assert VersionInfo.parse(version).replace(**parts) == VersionInfo.parse(expected) def test_replace_raises_ValueError_for_non_numeric_values(): From 8169c12e4df4fd7a30c9b09fa75239ade47750c8 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 25 Nov 2019 10:24:21 +0100 Subject: [PATCH 046/219] Fix #203: Use --check option for black (infra) (#204) * Remove uploading the diff file as artifact * Use `--check` to test our source code --- .github/workflows/black-formatting.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/black-formatting.yml b/.github/workflows/black-formatting.yml index 83ad2892..25b34f21 100644 --- a/.github/workflows/black-formatting.yml +++ b/.github/workflows/black-formatting.yml @@ -39,11 +39,5 @@ jobs: - name: Run black id: black run: | - black . > project.diff + black --check . echo "::set-output name=rc::$?" - - - name: Upload diff artifact - uses: actions/upload-artifact@v1 - with: - name: black-project-diff - path: project.diff From 63b9163ed2837cffadbdfa5c777d6521a4e84761 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 7 Dec 2019 21:04:21 +0100 Subject: [PATCH 047/219] Improve consistency in tox.ini (#206) * Describe each target with `description` keyword; useful when running tox as `tox -a -v` * Move whitelist_externals to `textenv` section * Use `make` in `docs` target instead of running `sphinx-build` directly * Introduce black target to check for changes in formatting * Use `posargs` for flake8 --- tox.ini | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index abca346b..6f7d06e4 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ envlist = pypy [testenv] +description = Run test suite +whitelist_externals = make commands = pytest {posargs:} deps = pytest @@ -12,19 +14,27 @@ deps = setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 +[testenv:black] +description = Check for formatting changes +basepython = python3 +deps = black +commands = black --check {posargs:.} + [testenv:flake8] +description = Check code style basepython = python3 deps = flake8 -commands = flake8 +commands = flake8 {posargs:} [testenv:docs] +description = Build HTML documentation basepython = python3 deps = -r{toxinidir}/docs/requirements.txt skip_install = true -commands = sphinx-build {posargs:-E} -b html docs dist/docs +commands = make -C docs html [testenv:man] -whitelist_externals = make +description = Build the manpage basepython = python3 deps = sphinx skip_install = true From 445f47ab652ce9cc4f3b55e32733192a4cc7df7c Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 7 Dec 2019 23:04:23 +0100 Subject: [PATCH 048/219] Add python_requires keyword in setup.py (#207) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 77f19e73..992860f9 100755 --- a/setup.py +++ b/setup.py @@ -94,6 +94,7 @@ def read_file(filename): "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries :: Python Modules", ], + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", tests_require=["tox", "virtualenv"], cmdclass={"clean": Clean, "test": Tox}, entry_points={"console_scripts": ["pysemver = semver:main"]}, From f2c23f63822077fde271fd8e876d608b8d69189f Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 10 Dec 2019 06:44:34 +0100 Subject: [PATCH 049/219] Fix #208: Introduce VersionInfo.isvalid() function (#209) * VersionInfo.isvalid(cls, version:str) -> bool * Add test case * Describe function in documentation * Amend pysemver script with "check" subcommand * Update manpage (pysemver.rst) * Update `CHANGELOG.rst` --- CHANGELOG.rst | 4 +++- docs/pysemver.rst | 24 ++++++++++++++++++- docs/usage.rst | 21 +++++++++++++++++ semver.py | 32 ++++++++++++++++++++++++- test_semver.py | 60 ++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 127 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index df88ddcc..8eb124d8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,9 @@ Features * :gh:`191` (:pr:`194`): Created manpage for pysemver * :gh:`196` (:pr:`197`): Added distribution specific installation instructions * :gh:`201` (:pr:`202`): Reformatted source code with black +* :gh:`208` (:pr:`209`): Introduce new function :func:`semver.VersionInfo.isvalid` + and extend :command:`pysemver` with :command:`check` subcommand + Bug Fixes --------- @@ -54,7 +57,6 @@ Features * :pr:`166`: Reworked :file:`.gitignore` file * :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` - Bug Fixes --------- diff --git a/docs/pysemver.rst b/docs/pysemver.rst index 17cd6554..881000e1 100644 --- a/docs/pysemver.rst +++ b/docs/pysemver.rst @@ -86,6 +86,29 @@ you get an error message and a return code != 0:: ERROR 1.5 is not valid SemVer string +pysemver check +~~~~~~~~~~~~~~ + +Checks if a string is a valid semver version. + +.. code:: bash + + pysemver check + +.. option:: + + The version string to check. + +The *error code* returned by the script indicates if the +version is valid (=0) or not (!=0):: + + $ pysemver check 1.2.3; echo $? + 0 + $ pysemver check 2.1; echo $? + ERROR Invalid version '2.1' + 2 + + pysemver compare ~~~~~~~~~~~~~~~~ @@ -121,7 +144,6 @@ are valid (return code 0) or not (return code != 0):: ERROR 1.2.x is not valid SemVer string 2 - See also -------- diff --git a/docs/usage.rst b/docs/usage.rst index 1ae7d8da..013391f0 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -53,6 +53,13 @@ A version can be created in different ways: >>> semver.VersionInfo(1, 2, 3, 4, 5) VersionInfo(major=1, minor=2, patch=3, prerelease=4, build=5) +If you pass an invalid version string you will get a ``ValueError``:: + + >>> semver.parse("1.2") + Traceback (most recent call last) + ... + ValueError: 1.2 is not valid SemVer string + Parsing a Version String ------------------------ @@ -77,6 +84,20 @@ Parsing a Version String {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} +Checking for a Valid Semver Version +----------------------------------- + +If you need to check a string if it is a valid semver version, use the +classmethod :func:`semver.VersionInfo.isvalid`: + +.. code-block:: python + + >>> VersionInfo.isvalid("1.0.0") + True + >>> VersionInfo.isvalid("invalid") + False + + Accessing Parts of a Version ---------------------------- diff --git a/semver.py b/semver.py index efe1102e..59c54f4c 100644 --- a/semver.py +++ b/semver.py @@ -321,6 +321,21 @@ def replace(self, **parts): ) raise TypeError(error) + @classmethod + def isvalid(cls, version): + """Check if the string is a valid semver version + + :param str version: the version string to check + :return: True if the version string is a valid semver version, False + otherwise. + :rtype: bool + """ + try: + cls.parse(version) + return True + except ValueError: + return False + def _to_dict(obj): if isinstance(obj, VersionInfo): @@ -681,6 +696,14 @@ def createparser(): sb.add_parser("build", help="Bump the build part of the version"), ): p.add_argument("version", help="Version to raise") + + # Create the check subcommand + parser_check = s.add_parser( + "check", help="Checks if a string is a valid semver version" + ) + parser_check.set_defaults(which="check") + parser_check.add_argument("version", help="Version to check") + return parser @@ -697,6 +720,7 @@ def process(args): if not hasattr(args, "which"): args.parser.print_help() raise SystemExit() + elif args.which == "bump": maptable = { "major": "bump_major", @@ -718,6 +742,11 @@ def process(args): elif args.which == "compare": return str(compare(args.version1, args.version2)) + elif args.which == "check": + if VersionInfo.isvalid(args.version): + return None + raise ValueError("Invalid version %r" % args.version) + def main(cliargs=None): """Entry point for the application script @@ -732,7 +761,8 @@ def main(cliargs=None): # Save parser instance: args.parser = parser result = process(args) - print(result) + if result is not None: + print(result) return 0 except (ValueError, TypeError) as err: diff --git a/test_semver.py b/test_semver.py index b3d0929d..8ca6985c 100644 --- a/test_semver.py +++ b/test_semver.py @@ -1,4 +1,5 @@ from argparse import Namespace +from contextlib import contextmanager import pytest # noqa from semver import ( @@ -41,6 +42,11 @@ ] +@contextmanager +def does_not_raise(item): + yield item + + @pytest.mark.parametrize( "string,expected", [("rc", "rc"), ("rc.1", "rc.2"), ("2x", "3x")] ) @@ -703,6 +709,8 @@ def test_should_be_able_to_use_integers_as_prerelease_build(): ["compare", "1.2.3", "2.1.3"], Namespace(which="compare", version1="1.2.3", version2="2.1.3"), ), + # --- + (["check", "1.2.3"], Namespace(which="check", version="1.2.3")), ], ) def test_should_parse_cli_arguments(cli, expected): @@ -713,25 +721,50 @@ def test_should_parse_cli_arguments(cli, expected): @pytest.mark.parametrize( - "args,expected", + "args,expectation", [ # bump subcommand - (Namespace(which="bump", bump="major", version="1.2.3"), "2.0.0"), - (Namespace(which="bump", bump="minor", version="1.2.3"), "1.3.0"), - (Namespace(which="bump", bump="patch", version="1.2.3"), "1.2.4"), - (Namespace(which="bump", bump="prerelease", version="1.2.3-rc1"), "1.2.3-rc2"), + ( + Namespace(which="bump", bump="major", version="1.2.3"), + does_not_raise("2.0.0"), + ), + ( + Namespace(which="bump", bump="minor", version="1.2.3"), + does_not_raise("1.3.0"), + ), + ( + Namespace(which="bump", bump="patch", version="1.2.3"), + does_not_raise("1.2.4"), + ), + ( + Namespace(which="bump", bump="prerelease", version="1.2.3-rc1"), + does_not_raise("1.2.3-rc2"), + ), ( Namespace(which="bump", bump="build", version="1.2.3+build.13"), - "1.2.3+build.14", + does_not_raise("1.2.3+build.14"), ), # compare subcommand - (Namespace(which="compare", version1="1.2.3", version2="2.1.3"), "-1"), - (Namespace(which="compare", version1="1.2.3", version2="1.2.3"), "0"), - (Namespace(which="compare", version1="2.4.0", version2="2.1.3"), "1"), + ( + Namespace(which="compare", version1="1.2.3", version2="2.1.3"), + does_not_raise("-1"), + ), + ( + Namespace(which="compare", version1="1.2.3", version2="1.2.3"), + does_not_raise("0"), + ), + ( + Namespace(which="compare", version1="2.4.0", version2="2.1.3"), + does_not_raise("1"), + ), + # check subcommand + (Namespace(which="check", version="1.2.3"), does_not_raise(None)), + (Namespace(which="check", version="1.2"), pytest.raises(ValueError)), ], ) -def test_should_process_parsed_cli_arguments(args, expected): - assert process(args) == expected +def test_should_process_parsed_cli_arguments(args, expectation): + with expectation as expected: + assert process(args) == expected def test_should_process_print(capsys): @@ -803,3 +836,8 @@ def test_should_return_versioninfo_with_replaced_parts(version, parts, expected) def test_replace_raises_ValueError_for_non_numeric_values(): with pytest.raises(ValueError): VersionInfo.parse("1.2.3").replace(major="x") + + +def test_should_versioninfo_isvalid(): + assert VersionInfo.isvalid("1.0.0") is True + assert VersionInfo.isvalid("foo") is False From 177f08037aa6fc54a5db57dba01e0d8ea3bdc787 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 19 Jan 2020 19:37:47 +0100 Subject: [PATCH 050/219] Improve docstrings according to PEP257 (#212) * Use `docformatter` from https://github.com/myint/docformatter/ * Reformat `semver.py` to be compatible with PEP257 docstrings with `docformatter -i --pre-summary-newline semver.py` * `tox.ini` - Introduce `docstrings` target which calls `docformatter` to check for PEP257 compatible docstrings - Introduce a new `check` target which calls `black`, `flake8`, and `docstrings` * `.travis.yml` - Run `check` target instead of `flake8` - Add `before_install` section to install python3-dev package for black --- .travis.yml | 10 ++++- CHANGELOG.rst | 2 +- semver.py | 101 +++++++++++++++++++++++++++++++------------------- tox.ini | 18 +++++++++ 4 files changed, 90 insertions(+), 41 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0082ac4d..54165f6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,16 @@ # config file for automatic testing at travis-ci.org language: python cache: pip + +before_install: + sudo apt-get install -y python3-dev + install: - pip install --upgrade pip setuptools - pip install virtualenv tox + script: tox -v + matrix: include: - python: "2.7" @@ -13,8 +19,8 @@ matrix: - python: "3.4" env: TOXENV=py34 - - python: "3.4" - env: TOXENV=flake8 + - python: "3.6" + env: TOXENV=checks - python: "3.5" env: TOXENV=py35 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8eb124d8..aa89e107 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,7 +22,7 @@ Features * :gh:`201` (:pr:`202`): Reformatted source code with black * :gh:`208` (:pr:`209`): Introduce new function :func:`semver.VersionInfo.isvalid` and extend :command:`pysemver` with :command:`check` subcommand - +* :pr:`212`: Improve docstrings according to PEP257 Bug Fixes --------- diff --git a/semver.py b/semver.py index 59c54f4c..41d315ea 100644 --- a/semver.py +++ b/semver.py @@ -1,6 +1,4 @@ -""" -Python helper for Semantic Versioning (http://semver.org/) -""" +"""Python helper for Semantic Versioning (http://semver.org/)""" from __future__ import print_function import argparse @@ -50,7 +48,8 @@ def cmp(a, b): def parse(version): - """Parse version to major, minor, patch, pre-release, build parts. + """ + Parse version to major, minor, patch, pre-release, build parts. :param version: version string :return: dictionary with the keys 'build', 'major', 'minor', 'patch', @@ -84,7 +83,7 @@ def parse(version): def comparator(operator): - """ Wrap a VersionInfo binary op method in a type-check """ + """Wrap a VersionInfo binary op method in a type-check.""" @wraps(operator) def wrapper(self, other): @@ -100,6 +99,8 @@ def wrapper(self, other): class VersionInfo(object): """ + A semver compatible version class. + :param int major: version when you make incompatible API changes. :param int minor: version when you add functionality in a backwards-compatible manner. @@ -119,7 +120,7 @@ def __init__(self, major, minor=0, patch=0, prerelease=None, build=None): @property def major(self): - """The major part of a version""" + """The major part of a version.""" return self._major @major.setter @@ -128,7 +129,7 @@ def major(self, value): @property def minor(self): - """The minor part of a version""" + """The minor part of a version.""" return self._minor @minor.setter @@ -137,7 +138,7 @@ def minor(self, value): @property def patch(self): - """The patch part of a version""" + """The patch part of a version.""" return self._patch @patch.setter @@ -146,7 +147,7 @@ def patch(self, value): @property def prerelease(self): - """The prerelease part of a version""" + """The prerelease part of a version.""" return self._prerelease @prerelease.setter @@ -155,7 +156,7 @@ def prerelease(self, value): @property def build(self): - """The build part of a version""" + """The build part of a version.""" return self._build @build.setter @@ -183,8 +184,9 @@ def __iter__(self): yield v def bump_major(self): - """Raise the major part of the version, return a new object - but leave self untouched + """ + Raise the major part of the version, return a new object but leave self + untouched. :return: new object with the raised major part :rtype: VersionInfo @@ -196,8 +198,9 @@ def bump_major(self): return parse_version_info(bump_major(str(self))) def bump_minor(self): - """Raise the minor part of the version, return a new object - but leave self untouched + """ + Raise the minor part of the version, return a new object but leave self + untouched. :return: new object with the raised minor part :rtype: VersionInfo @@ -209,8 +212,9 @@ def bump_minor(self): return parse_version_info(bump_minor(str(self))) def bump_patch(self): - """Raise the patch part of the version, return a new object - but leave self untouched + """ + Raise the patch part of the version, return a new object but leave self + untouched. :return: new object with the raised patch part :rtype: VersionInfo @@ -222,8 +226,9 @@ def bump_patch(self): return parse_version_info(bump_patch(str(self))) def bump_prerelease(self, token="rc"): - """Raise the prerelease part of the version, return a new object - but leave self untouched + """ + Raise the prerelease part of the version, return a new object but leave + self untouched. :param token: defaults to 'rc' :return: new object with the raised prerelease part @@ -237,8 +242,9 @@ def bump_prerelease(self, token="rc"): return parse_version_info(bump_prerelease(str(self), token)) def bump_build(self, token="build"): - """Raise the build part of the version, return a new object - but leave self untouched + """ + Raise the build part of the version, return a new object but leave self + untouched. :param token: defaults to 'build' :return: new object with the raised build part @@ -287,7 +293,8 @@ def __hash__(self): @staticmethod def parse(version): - """Parse version string to a VersionInfo instance. + """ + Parse version string to a VersionInfo instance. :param version: version string :return: a :class:`semver.VersionInfo` instance @@ -323,7 +330,8 @@ def replace(self, **parts): @classmethod def isvalid(cls, version): - """Check if the string is a valid semver version + """ + Check if the string is a valid semver version. :param str version: the version string to check :return: True if the version string is a valid semver version, False @@ -346,7 +354,8 @@ def _to_dict(obj): def parse_version_info(version): - """Parse version string to a VersionInfo instance. + """ + Parse version string to a VersionInfo instance. :param version: version string :return: a :class:`VersionInfo` instance @@ -423,7 +432,8 @@ def _compare_by_keys(d1, d2): def compare(ver1, ver2): - """Compare two versions + """ + Compare two versions. :param ver1: version string 1 :param ver2: version string 2 @@ -445,7 +455,8 @@ def compare(ver1, ver2): def match(version, match_expr): - """Compare two versions through a comparison + """ + Compare two versions through a comparison. :param str version: a version string :param str match_expr: operator and version; valid operators are @@ -493,7 +504,8 @@ def match(version, match_expr): def max_ver(ver1, ver2): - """Returns the greater version of two versions + """ + Returns the greater version of two versions. :param ver1: version string 1 :param ver2: version string 2 @@ -511,7 +523,8 @@ def max_ver(ver1, ver2): def min_ver(ver1, ver2): - """Returns the smaller version of two versions + """ + Returns the smaller version of two versions. :param ver1: version string 1 :param ver2: version string 2 @@ -529,7 +542,8 @@ def min_ver(ver1, ver2): def format_version(major, minor, patch, prerelease=None, build=None): - """Format a version according to the Semantic Versioning specification + """ + Format a version according to the Semantic Versioning specification. :param int major: the required major part of a version :param int minor: the required minor part of a version @@ -555,6 +569,7 @@ def format_version(major, minor, patch, prerelease=None, build=None): def _increment_string(string): """ Look for the last sequence of number(s) in a string and increment, from: + http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 """ match = _LAST_NUMBER.search(string) @@ -566,7 +581,8 @@ def _increment_string(string): def bump_major(version): - """Raise the major part of the version + """ + Raise the major part of the version. :param: version string :return: the raised version string @@ -580,7 +596,8 @@ def bump_major(version): def bump_minor(version): - """Raise the minor part of the version + """ + Raise the minor part of the version. :param: version string :return: the raised version string @@ -594,7 +611,8 @@ def bump_minor(version): def bump_patch(version): - """Raise the patch part of the version + """ + Raise the patch part of the version. :param: version string :return: the raised version string @@ -608,7 +626,8 @@ def bump_patch(version): def bump_prerelease(version, token="rc"): - """Raise the prerelease part of the version + """ + Raise the prerelease part of the version. :param version: version string :param token: defaults to 'rc' @@ -628,7 +647,8 @@ def bump_prerelease(version, token="rc"): def bump_build(version, token="build"): - """Raise the build part of the version + """ + Raise the build part of the version. :param version: version string :param token: defaults to 'build' @@ -650,7 +670,8 @@ def bump_build(version, token="build"): def finalize_version(version): - """Remove any prerelease and build metadata from the version + """ + Remove any prerelease and build metadata from the version. :param version: version string :return: the finalized version string @@ -664,7 +685,8 @@ def finalize_version(version): def createparser(): - """Create an :class:`argparse.ArgumentParser` instance + """ + Create an :class:`argparse.ArgumentParser` instance. :return: parser instance :rtype: :class:`argparse.ArgumentParser` @@ -708,7 +730,8 @@ def createparser(): def process(args): - """Process the input from the CLI + """ + Process the input from the CLI. :param args: The parsed arguments :type args: :class:`argparse.Namespace` @@ -749,7 +772,8 @@ def process(args): def main(cliargs=None): - """Entry point for the application script + """ + Entry point for the application script. :param list cliargs: Arguments to parse or None (=use :class:`sys.argv`) :return: error code @@ -771,7 +795,8 @@ def main(cliargs=None): def replace(version, **parts): - """Replace one or more parts of a version and return the new string + """ + Replace one or more parts of a version and return the new string. :param str version: the version string to replace :param dict parts: the parts to be updated. Valid keys are: diff --git a/tox.ini b/tox.ini index 6f7d06e4..397c2440 100644 --- a/tox.ini +++ b/tox.ini @@ -26,6 +26,24 @@ basepython = python3 deps = flake8 commands = flake8 {posargs:} +[testenv:docstrings] +description = Check for PEP257 compatible docstrings +basepython = python3 +deps = docformatter +commands = docformatter --check {posargs:--pre-summary-newline semver.py} + +[testenv:checks] +description = Run code style checks +basepython = python3 +deps = + {[testenv:black]deps} + {[testenv:flake8]deps} + {[testenv:docstrings]deps} +commands = + {[testenv:black]commands} + {[testenv:flake8]commands} + {[testenv:docstrings]commands} + [testenv:docs] description = Build HTML documentation basepython = python3 From 5755f9a58cbe0178f83e7fdff1464cafbe7fb7d2 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 19 Jan 2020 19:39:50 +0100 Subject: [PATCH 051/219] Improve pysemver subcommands (#214) * Use separate cmd_* functions for each subcommand * Replace which with func keyword * Use func keyword to store the specific cmd_* function * Adapt tests * (Re)format with black * Clarify return code in manpage (add new section "Return Code") => Easier to extend --- docs/pysemver.rst | 32 ++++++++++++++--- semver.py | 90 ++++++++++++++++++++++++++++++++--------------- test_semver.py | 74 ++++++++++++++++---------------------- 3 files changed, 118 insertions(+), 78 deletions(-) diff --git a/docs/pysemver.rst b/docs/pysemver.rst index 881000e1..3c247b62 100644 --- a/docs/pysemver.rst +++ b/docs/pysemver.rst @@ -133,17 +133,39 @@ to indicates which is the bigger version: * ``0`` if both versions are the same, * ``1`` if the first version is greater than the second version. -The *error code* returned by the script indicates if both versions -are valid (return code 0) or not (return code != 0):: + +Return Code +----------- + +The *return code* of the script (accessible by ``$?`` from the Bash) +indicates if the subcommand returned successfully nor not. It is *not* +meant as the result of the subcommand. + +The result of the subcommand is printed on the standard out channel +("stdout" or ``0``), any error messages to standard error ("stderr" or +``2``). + +For example, to compare two versions, the command expects two valid +semver versions:: $ pysemver compare 1.2.3 2.4.0 -1 - $ pysemver compare 1.2.3 2.4.0 ; echo $? + $ echo $? 0 - $ pysemver compare 1.2.3 2.4.0 ; echo $? - ERROR 1.2.x is not valid SemVer string + +The return code is zero, but the result is ``-1``. + +However, if you pass invalid versions, you get this situation:: + + $ pysemver compare 1.2.3 2.4 + ERROR 2.4 is not valid SemVer string + $ echo $? 2 +If you use the :command:`pysemver` in your own scripts, check the +return code first before you process the standard output. + + See also -------- diff --git a/semver.py b/semver.py index 41d315ea..3ae2e6f5 100644 --- a/semver.py +++ b/semver.py @@ -684,6 +684,61 @@ def finalize_version(version): return format_version(verinfo["major"], verinfo["minor"], verinfo["patch"]) +def cmd_bump(args): + """ + Subcommand: Bumps a version. + + Synopsis: bump + can be major, minor, patch, prerelease, or build + + :param args: The parsed arguments + :type args: :class:`argparse.Namespace` + :return: the new, bumped version + """ + maptable = { + "major": "bump_major", + "minor": "bump_minor", + "patch": "bump_patch", + "prerelease": "bump_prerelease", + "build": "bump_build", + } + if args.bump is None: + # When bump is called without arguments, + # print the help and exit + args.parser.parse_args(["bump", "-h"]) + + ver = parse_version_info(args.version) + # get the respective method and call it + func = getattr(ver, maptable[args.bump]) + return str(func()) + + +def cmd_check(args): + """ + Subcommand: Checks if a string is a valid semver version. + + Synopsis: check + + :param args: The parsed arguments + :type args: :class:`argparse.Namespace` + """ + if VersionInfo.isvalid(args.version): + return None + raise ValueError("Invalid version %r" % args.version) + + +def cmd_compare(args): + """ + Subcommand: Compare two versions + + Synopsis: compare + + :param args: The parsed arguments + :type args: :class:`argparse.Namespace` + """ + return str(compare(args.version1, args.version2)) + + def createparser(): """ Create an :class:`argparse.ArgumentParser` instance. @@ -700,13 +755,13 @@ def createparser(): s = parser.add_subparsers() # create compare subcommand parser_compare = s.add_parser("compare", help="Compare two versions") - parser_compare.set_defaults(which="compare") + parser_compare.set_defaults(func=cmd_compare) parser_compare.add_argument("version1", help="First version") parser_compare.add_argument("version2", help="Second version") # create bump subcommand parser_bump = s.add_parser("bump", help="Bumps a version") - parser_bump.set_defaults(which="bump") + parser_bump.set_defaults(func=cmd_bump) sb = parser_bump.add_subparsers(title="Bump commands", dest="bump") # Create subparsers for the bump subparser: @@ -723,7 +778,7 @@ def createparser(): parser_check = s.add_parser( "check", help="Checks if a string is a valid semver version" ) - parser_check.set_defaults(which="check") + parser_check.set_defaults(func=cmd_check) parser_check.add_argument("version", help="Version to check") return parser @@ -740,35 +795,12 @@ def process(args): :return: result of the selected action :rtype: str """ - if not hasattr(args, "which"): + if not hasattr(args, "func"): args.parser.print_help() raise SystemExit() - elif args.which == "bump": - maptable = { - "major": "bump_major", - "minor": "bump_minor", - "patch": "bump_patch", - "prerelease": "bump_prerelease", - "build": "bump_build", - } - if args.bump is None: - # When bump is called without arguments, - # print the help and exit - args.parser.parse_args([args.which, "-h"]) - - ver = parse_version_info(args.version) - # get the respective method and call it - func = getattr(ver, maptable[args.bump]) - return str(func()) - - elif args.which == "compare": - return str(compare(args.version1, args.version2)) - - elif args.which == "check": - if VersionInfo.isvalid(args.version): - return None - raise ValueError("Invalid version %r" % args.version) + # Call the respective function object: + return args.func(args) def main(cliargs=None): diff --git a/test_semver.py b/test_semver.py index 8ca6985c..ed7d80bc 100644 --- a/test_semver.py +++ b/test_semver.py @@ -9,6 +9,9 @@ bump_minor, bump_patch, bump_prerelease, + cmd_bump, + cmd_check, + cmd_compare, compare, createparser, finalize_version, @@ -684,87 +687,70 @@ def test_should_be_able_to_use_integers_as_prerelease_build(): @pytest.mark.parametrize( "cli,expected", [ - ( - ["bump", "major", "1.2.3"], - Namespace(which="bump", bump="major", version="1.2.3"), - ), - ( - ["bump", "minor", "1.2.3"], - Namespace(which="bump", bump="minor", version="1.2.3"), - ), - ( - ["bump", "patch", "1.2.3"], - Namespace(which="bump", bump="patch", version="1.2.3"), - ), + (["bump", "major", "1.2.3"], Namespace(bump="major", version="1.2.3")), + (["bump", "minor", "1.2.3"], Namespace(bump="minor", version="1.2.3")), + (["bump", "patch", "1.2.3"], Namespace(bump="patch", version="1.2.3")), ( ["bump", "prerelease", "1.2.3"], - Namespace(which="bump", bump="prerelease", version="1.2.3"), - ), - ( - ["bump", "build", "1.2.3"], - Namespace(which="bump", bump="build", version="1.2.3"), + Namespace(bump="prerelease", version="1.2.3"), ), + (["bump", "build", "1.2.3"], Namespace(bump="build", version="1.2.3")), # --- - ( - ["compare", "1.2.3", "2.1.3"], - Namespace(which="compare", version1="1.2.3", version2="2.1.3"), - ), + (["compare", "1.2.3", "2.1.3"], Namespace(version1="1.2.3", version2="2.1.3")), # --- - (["check", "1.2.3"], Namespace(which="check", version="1.2.3")), + (["check", "1.2.3"], Namespace(version="1.2.3")), ], ) def test_should_parse_cli_arguments(cli, expected): parser = createparser() assert parser result = parser.parse_args(cli) + del result.func assert result == expected @pytest.mark.parametrize( - "args,expectation", + "func,args,expectation", [ # bump subcommand + (cmd_bump, Namespace(bump="major", version="1.2.3"), does_not_raise("2.0.0")), + (cmd_bump, Namespace(bump="minor", version="1.2.3"), does_not_raise("1.3.0")), + (cmd_bump, Namespace(bump="patch", version="1.2.3"), does_not_raise("1.2.4")), ( - Namespace(which="bump", bump="major", version="1.2.3"), - does_not_raise("2.0.0"), - ), - ( - Namespace(which="bump", bump="minor", version="1.2.3"), - does_not_raise("1.3.0"), - ), - ( - Namespace(which="bump", bump="patch", version="1.2.3"), - does_not_raise("1.2.4"), - ), - ( - Namespace(which="bump", bump="prerelease", version="1.2.3-rc1"), + cmd_bump, + Namespace(bump="prerelease", version="1.2.3-rc1"), does_not_raise("1.2.3-rc2"), ), ( - Namespace(which="bump", bump="build", version="1.2.3+build.13"), + cmd_bump, + Namespace(bump="build", version="1.2.3+build.13"), does_not_raise("1.2.3+build.14"), ), # compare subcommand ( - Namespace(which="compare", version1="1.2.3", version2="2.1.3"), + cmd_compare, + Namespace(version1="1.2.3", version2="2.1.3"), does_not_raise("-1"), ), ( - Namespace(which="compare", version1="1.2.3", version2="1.2.3"), + cmd_compare, + Namespace(version1="1.2.3", version2="1.2.3"), does_not_raise("0"), ), ( - Namespace(which="compare", version1="2.4.0", version2="2.1.3"), + cmd_compare, + Namespace(version1="2.4.0", version2="2.1.3"), does_not_raise("1"), ), # check subcommand - (Namespace(which="check", version="1.2.3"), does_not_raise(None)), - (Namespace(which="check", version="1.2"), pytest.raises(ValueError)), + (cmd_check, Namespace(version="1.2.3"), does_not_raise(None)), + (cmd_check, Namespace(version="1.2"), pytest.raises(ValueError)), ], ) -def test_should_process_parsed_cli_arguments(args, expectation): +def test_should_process_parsed_cli_arguments(func, args, expectation): with expectation as expected: - assert process(args) == expected + result = func(args) + assert result == expected def test_should_process_print(capsys): From b5ccad691c396a5a34ad3a18486f451e00377da0 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 19 Jan 2020 19:48:36 +0100 Subject: [PATCH 052/219] Fix #210: how to deal with invalid versions (#215) * Document how to deal with invalid versions * Use coerce(version) as an example * Update CHANGELOG.rst Co-authored-by: scls19fr --- CHANGELOG.rst | 1 + docs/usage.rst | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aa89e107..474cdc8a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,7 @@ Features * :gh:`201` (:pr:`202`): Reformatted source code with black * :gh:`208` (:pr:`209`): Introduce new function :func:`semver.VersionInfo.isvalid` and extend :command:`pysemver` with :command:`check` subcommand +* :gh:`210` (:pr:`215`): Document how to deal with invalid versions * :pr:`212`: Improve docstrings according to PEP257 Bug Fixes diff --git a/docs/usage.rst b/docs/usage.rst index 013391f0..f7b5da39 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -334,3 +334,75 @@ Getting Minimum and Maximum of two Versions '2.0.0' >>> semver.min_ver("1.0.0", "2.0.0") '1.0.0' + + +Dealing with Invalid Versions +----------------------------- + +As semver follows the semver specification, it cannot parse version +strings which are considered "invalid" by that specification. The semver +library cannot know all the possible variations so you need to help the +library a bit. + +For example, if you have a version string ``v1.2`` would be an invalid +semver version. +However, "basic" version strings consisting of major, minor, +and patch part, can be easy to convert. The following function extract this +information and returns a tuple with two items: + +.. code-block:: python + + import re + + BASEVERSION = re.compile( + r"""[vV]? + (?P0|[1-9]\d*) + (\. + (?P0|[1-9]\d*) + (\. + (?P0|[1-9]\d*) + )? + )? + """, + re.VERBOSE, + ) + def coerce(version): + """ + Convert an incomplete version string into a semver-compatible VersionInfo + object + + * Tries to detect a "basic" version string (``major.minor.patch``). + * If not enough components can be found, missing components are + set to zero to obtain a valid semver version. + + :param str version: the version string to convert + :return: a tuple with a :class:`VersionInfo` instance (or ``None`` + if it's not a version) and the rest of the string which doesn't + belong to a basic version. + :rtype: tuple(:class:`VersionInfo` | None, str) + """ + match = BASEVERSION.search(version) + if not match: + return (None, version) + + ver = { + key: 0 if value is None else value + for key, value in match.groupdict().items() + } + ver = semver.VersionInfo(**ver) + rest = match.string[match.end() :] + return ver, rest + +The function returns a *tuple*, containing a :class:`VersionInfo` +instance or None as the first element and the rest as the second element. +The second element (the rest) can be used to make further adjustments. + +For example: + +.. code-block:: python + + >>> coerce("v1.2") + (VersionInfo(major=1, minor=2, patch=0, prerelease=None, build=None), '') + >>> coerce("v2.5.2-bla") + (VersionInfo(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') + From 3f92aa5494252387807fefc6083c090cbc67098d Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 16 Feb 2020 20:30:07 +0100 Subject: [PATCH 053/219] Create semver version 2.9.1 (#219) * Raise version number in `__version__` * Update CHANGELOG * Mention TestPyPI in `release-procedure.md` * MANIFEST.in: * Exclude `.travis.yml` * Exclude `.github` directory (pretty useless in an archive/wheel) * Exclude `docs/_build` directory * Exclude temporary Python files like `__pycache__`, `*.py[cod]` * Include all `*.txt` and `*.rst` files Co-authored-by: Sebastien Celles Co-authored-by: scls19fr --- CHANGELOG.rst | 12 ++++++++---- MANIFEST.in | 10 ++++++++-- release-procedure.md | 13 +++++++++++++ semver.py | 4 ++-- setup.py | 1 + 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 474cdc8a..a10cc03a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,10 +6,11 @@ Change Log All notable changes to this code base will be documented in this file, in every released version. -Version 2.9.1 (WIP) -=================== -:Released: 20xy-xy-xy -:Maintainer: ... + +Version 2.9.1 +============= +:Released: 2020-02-16 +:Maintainer: Tom Schraitle Features -------- @@ -34,6 +35,9 @@ Bug Fixes Removals -------- +not available + + Version 2.9.0 ============= :Released: 2019-10-30 diff --git a/MANIFEST.in b/MANIFEST.in index 9ed3b402..80257f1f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,9 @@ -include README.rst -include LICENSE.txt +include *.rst +include *.txt include test_*.py + +exclude .travis.yml +prune docs/_build +recursive-exclude .github * + +global-exclude *.py[cod] __pycache__ *.so *.dylib diff --git a/release-procedure.md b/release-procedure.md index 52a43fd5..8d223fd3 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -21,6 +21,19 @@ Release procedure * Ensure that long description (ie [README.rst](https://github.com/python-semver/python-semver/blob/master/README.rst)) can be correctly rendered by Pypi using `restview --long-description` +* Upload it to TestPyPI first: + +```bash +git clean -xfd +python setup.py register sdist bdist_wheel --universal +twine upload --repository-url https://test.pypi.org/legacy/ dist/* +``` + + If you have a `~/.pypirc` with a `testpyi` section, the upload can be + simplified: + + twine upload --repository testpyi dist/* + * Upload to PyPI ```bash diff --git a/semver.py b/semver.py index 3ae2e6f5..46f3a2e5 100644 --- a/semver.py +++ b/semver.py @@ -8,10 +8,10 @@ import sys -__version__ = "2.9.0" +__version__ = "2.9.1" __author__ = "Kostiantyn Rybnikov" __author_email__ = "k-bx@k-bx.com" -__maintainer__ = "Sebastien Celles" +__maintainer__ = ["Sebastien Celles", "Tom Schraitle"] __maintainer_email__ = "s.celles@gmail.com" _REGEX = re.compile( diff --git a/setup.py b/setup.py index 992860f9..d73690ee 100755 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ def read_file(filename): include_package_data=True, license="BSD", classifiers=[ + # See https://pypi.org/pypi?%3Aaction=list_classifiers "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", From 687891655d548f87d7404718bb7f420b586cf8f3 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 29 Feb 2020 22:37:21 +0100 Subject: [PATCH 054/219] Fix #224: Replace super() call (#226) In class clean, replace super(CleanCommand, self).run() with CleanCommand.run(self) Co-authored-by: Dennis Menschel Co-authored-by: Dennis Menschel --- CHANGELOG.rst | 19 +++++++++++++++++++ setup.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a10cc03a..03a18b7f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,25 @@ Change Log All notable changes to this code base will be documented in this file, in every released version. +Version 2.9.x (WIP) +=================== + +:Released: 2020-xx-yy +:Maintainer: + +Features +-------- + +Bug Fixes +--------- + +* :gh:`224` (:pr:`226`): Replaced in class ``clean``, ``super(CleanCommand, self).run()`` with + ``CleanCommand.run(self)`` + + +Removals +-------- + Version 2.9.1 ============= diff --git a/setup.py b/setup.py index d73690ee..15829461 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ def run_tests(self): class Clean(CleanCommand): def run(self): - super(CleanCommand, self).run() + CleanCommand.run(self) delete_in_root = ["build", ".cache", "dist", ".eggs", "*.egg-info", ".tox"] delete_everywhere = ["__pycache__", "*.pyc"] for candidate in delete_in_root: From 6669ba055e166cf4f32b30ebbef43ad634acc7c8 Mon Sep 17 00:00:00 2001 From: Thomas Schraitle Date: Sat, 14 Mar 2020 17:37:16 +0100 Subject: [PATCH 055/219] Integrate better doctest integration into pytest * Fix typos in `README.rst`. * Move `coerce()` function into separate file; this was needed so it can be both included into the documentation and inside `conftest.py`. * In `docs/usage.rst`: - Fix typos - Add missing semver module name as prefix - Slightly rewrite some doctests which involves dicts (unfortunately, order matters still in Python2) * In `setup.cfg`: - Add `--doctest-glob` option to look for all `*.rst` files. - Add `testpaths` key to restrict testing paths to current dir and `docs`. * Update `CHANGELOG.rst` --- CHANGELOG.rst | 9 ++++-- README.rst | 4 +-- conftest.py | 6 ++++ docs/coerce.py | 42 ++++++++++++++++++++++++ docs/development.rst | 1 + docs/usage.rst | 77 +++++++++++--------------------------------- setup.cfg | 5 +-- 7 files changed, 80 insertions(+), 64 deletions(-) create mode 100644 docs/coerce.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 03a18b7f..2ae377d9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,10 +18,15 @@ Features Bug Fixes --------- -* :gh:`224` (:pr:`226`): Replaced in class ``clean``, ``super(CleanCommand, self).run()`` with - ``CleanCommand.run(self)`` +* :gh:`224` (:pr:`226`): In ``setup.py``, replaced in class ``clean``, + ``super(CleanCommand, self).run()`` with ``CleanCommand.run(self)`` +Additions +--------- + +* :pr:`228`: Added better doctest integration + Removals -------- diff --git a/README.rst b/README.rst index 5c35b423..862fbdc6 100644 --- a/README.rst +++ b/README.rst @@ -68,7 +68,7 @@ different parts, use the `semver.parse` function: >>> ver['prerelease'] 'pre.2' >>> ver['build'] - 'build.5' + 'build.4' To raise parts of a version, there are a couple of functions available for you. The `semver.parse_version_info` function converts a version string @@ -87,7 +87,7 @@ It is allowed to concatenate different "bump functions": .. code-block:: python >>> ver.bump_major().bump_minor() - VersionInfo(major=4, minor=0, patch=1, prerelease=None, build=None) + VersionInfo(major=4, minor=1, patch=0, prerelease=None, build=None) To compare two versions, semver provides the `semver.compare` function. The return value indicates the relationship between the first and second diff --git a/conftest.py b/conftest.py index 4f49a137..3e05cb52 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,13 @@ import pytest import semver +import sys + +sys.path.insert(0, "docs") + +from coerce import coerce # noqa:E402 @pytest.fixture(autouse=True) def add_semver(doctest_namespace): doctest_namespace["semver"] = semver + doctest_namespace["coerce"] = coerce diff --git a/docs/coerce.py b/docs/coerce.py new file mode 100644 index 00000000..3e5eb21b --- /dev/null +++ b/docs/coerce.py @@ -0,0 +1,42 @@ +import re +import semver + +BASEVERSION = re.compile( + r"""[vV]? + (?P0|[1-9]\d*) + (\. + (?P0|[1-9]\d*) + (\. + (?P0|[1-9]\d*) + )? + )? + """, + re.VERBOSE, +) + + +def coerce(version): + """ + Convert an incomplete version string into a semver-compatible VersionInfo + object + + * Tries to detect a "basic" version string (``major.minor.patch``). + * If not enough components can be found, missing components are + set to zero to obtain a valid semver version. + + :param str version: the version string to convert + :return: a tuple with a :class:`VersionInfo` instance (or ``None`` + if it's not a version) and the rest of the string which doesn't + belong to a basic version. + :rtype: tuple(:class:`VersionInfo` | None, str) + """ + match = BASEVERSION.search(version) + if not match: + return (None, version) + + ver = { + key: 0 if value is None else value for key, value in match.groupdict().items() + } + ver = semver.VersionInfo(**ver) + rest = match.string[match.end() :] # noqa:E203 + return ver, rest diff --git a/docs/development.rst b/docs/development.rst index 7ad63d74..dad14641 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -174,6 +174,7 @@ documentation includes: 1 >>> semver.compare("2.0.0", "2.0.0") 0 + """ * **The documentation** diff --git a/docs/usage.rst b/docs/usage.rst index f7b5da39..dd05fa1c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -51,12 +51,12 @@ A version can be created in different ways: integers:: >>> semver.VersionInfo(1, 2, 3, 4, 5) - VersionInfo(major=1, minor=2, patch=3, prerelease=4, build=5) + VersionInfo(major=1, minor=2, patch=3, prerelease='4', build='5') If you pass an invalid version string you will get a ``ValueError``:: >>> semver.parse("1.2") - Traceback (most recent call last) + Traceback (most recent call last): ... ValueError: 1.2 is not valid SemVer string @@ -80,8 +80,8 @@ Parsing a Version String * With :func:`semver.parse`:: - >>> semver.parse("3.4.5-pre.2+build.4") - {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + >>> semver.parse("3.4.5-pre.2+build.4") == {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + True Checking for a Valid Semver Version @@ -92,9 +92,9 @@ classmethod :func:`semver.VersionInfo.isvalid`: .. code-block:: python - >>> VersionInfo.isvalid("1.0.0") + >>> semver.VersionInfo.isvalid("1.0.0") True - >>> VersionInfo.isvalid("invalid") + >>> semver.VersionInfo.isvalid("invalid") False @@ -106,7 +106,7 @@ parts of a version: .. code-block:: python - >>> v = VersionInfo.parse("3.4.5-pre.2+build.4") + >>> v = semver.VersionInfo.parse("3.4.5-pre.2+build.4") >>> v.major 3 >>> v.minor @@ -122,20 +122,20 @@ However, the attributes are read-only. You cannot change an attribute. If you do, you get an ``AttributeError``:: >>> v.minor = 5 - Traceback (most recent call last) + Traceback (most recent call last): ... AttributeError: attribute 'minor' is readonly In case you need the different parts of a version stepwise, iterate over the :class:`semver.VersionInfo` instance:: - >>> for item in VersionInfo.parse("3.4.5-pre.2+build.4"): + >>> for item in semver.VersionInfo.parse("3.4.5-pre.2+build.4"): ... print(item) 3 4 5 pre.2 build.4 - >>> list(VersionInfo.parse("3.4.5-pre.2+build.4")) + >>> list(semver.VersionInfo.parse("3.4.5-pre.2+build.4")) [3, 4, 5, 'pre.2', 'build.4'] @@ -160,12 +160,12 @@ unmodified, use one of the functions :func:`semver.replace` or If you pass invalid keys you get an exception:: >>> semver.replace("1.2.3", invalidkey=2) - Traceback (most recent call last) + Traceback (most recent call last): ... TypeError: replace() got 1 unexpected keyword argument(s): invalidkey >>> version = semver.VersionInfo.parse("1.4.5-pre.1+build.6") >>> version.replace(invalidkey=2) - Traceback (most recent call last) + Traceback (most recent call last): ... TypeError: replace() got 1 unexpected keyword argument(s): invalidkey @@ -209,8 +209,8 @@ Depending which function you call, you get different types * From a :class:`semver.VersionInfo` into a dictionary:: >>> v = semver.VersionInfo(major=3, minor=4, patch=5) - >>> semver.parse(str(v)) - {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': None, 'build': None} + >>> semver.parse(str(v)) == {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': None, 'build': None} + True Increasing Parts of a Version @@ -267,8 +267,8 @@ To compare two versions depends on your type: Use the specific operator. Currently, the operators ``<``, ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: - >>> v1 = VersionInfo.parse("3.4.5") - >>> v2 = VersionInfo.parse("3.5.1") + >>> v1 = semver.VersionInfo.parse("3.4.5") + >>> v2 = semver.VersionInfo.parse("3.5.1") >>> v1 < v2 True >>> v1 > v2 @@ -278,7 +278,7 @@ To compare two versions depends on your type: Use the operator as with two :class:`semver.VersionInfo` types:: - >>> v = VersionInfo.parse("3.4.5") + >>> v = semver.VersionInfo.parse("3.4.5") >>> v > (1, 0) True >>> v < (3, 5) @@ -350,48 +350,9 @@ However, "basic" version strings consisting of major, minor, and patch part, can be easy to convert. The following function extract this information and returns a tuple with two items: -.. code-block:: python +.. literalinclude:: coerce.py + :language: python - import re - - BASEVERSION = re.compile( - r"""[vV]? - (?P0|[1-9]\d*) - (\. - (?P0|[1-9]\d*) - (\. - (?P0|[1-9]\d*) - )? - )? - """, - re.VERBOSE, - ) - def coerce(version): - """ - Convert an incomplete version string into a semver-compatible VersionInfo - object - - * Tries to detect a "basic" version string (``major.minor.patch``). - * If not enough components can be found, missing components are - set to zero to obtain a valid semver version. - - :param str version: the version string to convert - :return: a tuple with a :class:`VersionInfo` instance (or ``None`` - if it's not a version) and the rest of the string which doesn't - belong to a basic version. - :rtype: tuple(:class:`VersionInfo` | None, str) - """ - match = BASEVERSION.search(version) - if not match: - return (None, version) - - ver = { - key: 0 if value is None else value - for key, value in match.groupdict().items() - } - ver = semver.VersionInfo(**ver) - rest = match.string[match.end() :] - return ver, rest The function returns a *tuple*, containing a :class:`VersionInfo` instance or None as the first element and the rest as the second element. diff --git a/setup.cfg b/setup.cfg index b8a0ea49..6ab7e562 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,11 @@ [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ +testpaths = . docs addopts = - --ignore=.eggs/ --no-cov-on-fail --cov=semver --cov-report=term-missing + --doctest-glob='*.rst' --doctest-modules --doctest-report ndiff @@ -17,4 +18,4 @@ exclude = .git, __pycache__, build, - dist \ No newline at end of file + dist From 1e6a25180ea6b01ef109e9e65a0460cf60469eaf Mon Sep 17 00:00:00 2001 From: Thomas Schraitle Date: Mon, 16 Mar 2020 10:59:17 +0100 Subject: [PATCH 056/219] Add version information in some functions * Use ".. versionadded::" RST directive in docstrings to make it more visible when something was added * Minor wording fix in docstrings (versions -> version strings) --- semver.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/semver.py b/semver.py index 46f3a2e5..aca7242e 100644 --- a/semver.py +++ b/semver.py @@ -307,8 +307,12 @@ def parse(version): return parse_version_info(version) def replace(self, **parts): - """Replace one or more parts of a version and return a new - :class:`semver.VersionInfo` object, but leave self untouched + """ + Replace one or more parts of a version and return a new + :class:`semver.VersionInfo` object, but leave self untouched + + .. versionadded:: 2.9.0 + Added :func:`VersionInfo.replace` :param dict parts: the parts to be updated. Valid keys are: ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` @@ -333,6 +337,8 @@ def isvalid(cls, version): """ Check if the string is a valid semver version. + .. versionadded:: 2.9.1 + :param str version: the version string to check :return: True if the version string is a valid semver version, False otherwise. @@ -357,6 +363,9 @@ def parse_version_info(version): """ Parse version string to a VersionInfo instance. + .. versionadded:: 2.7.2 + Added :func:`parse_version_info` + :param version: version string :return: a :class:`VersionInfo` instance :rtype: :class:`VersionInfo` @@ -433,7 +442,7 @@ def _compare_by_keys(d1, d2): def compare(ver1, ver2): """ - Compare two versions. + Compare two versions strings. :param ver1: version string 1 :param ver2: version string 2 @@ -456,7 +465,7 @@ def compare(ver1, ver2): def match(version, match_expr): """ - Compare two versions through a comparison. + Compare two versions strings through a comparison. :param str version: a version string :param str match_expr: operator and version; valid operators are @@ -505,7 +514,7 @@ def match(version, match_expr): def max_ver(ver1, ver2): """ - Returns the greater version of two versions. + Returns the greater version of two versions strings. :param ver1: version string 1 :param ver2: version string 2 @@ -524,7 +533,7 @@ def max_ver(ver1, ver2): def min_ver(ver1, ver2): """ - Returns the smaller version of two versions. + Returns the smaller version of two versions strings. :param ver1: version string 1 :param ver2: version string 2 @@ -543,7 +552,7 @@ def min_ver(ver1, ver2): def format_version(major, minor, patch, prerelease=None, build=None): """ - Format a version according to the Semantic Versioning specification. + Format a version string according to the Semantic Versioning specification. :param int major: the required major part of a version :param int minor: the required minor part of a version @@ -582,7 +591,7 @@ def _increment_string(string): def bump_major(version): """ - Raise the major part of the version. + Raise the major part of the version string. :param: version string :return: the raised version string @@ -597,7 +606,7 @@ def bump_major(version): def bump_minor(version): """ - Raise the minor part of the version. + Raise the minor part of the version string. :param: version string :return: the raised version string @@ -612,7 +621,7 @@ def bump_minor(version): def bump_patch(version): """ - Raise the patch part of the version. + Raise the patch part of the version string. :param: version string :return: the raised version string @@ -627,7 +636,7 @@ def bump_patch(version): def bump_prerelease(version, token="rc"): """ - Raise the prerelease part of the version. + Raise the prerelease part of the version string. :param version: version string :param token: defaults to 'rc' @@ -648,7 +657,7 @@ def bump_prerelease(version, token="rc"): def bump_build(version, token="build"): """ - Raise the build part of the version. + Raise the build part of the version string. :param version: version string :param token: defaults to 'build' @@ -671,7 +680,10 @@ def bump_build(version, token="build"): def finalize_version(version): """ - Remove any prerelease and build metadata from the version. + Remove any prerelease and build metadata from the version string. + + .. versionadded:: 2.7.9 + Added :func:`finalize_version` :param version: version string :return: the finalized version string @@ -830,6 +842,9 @@ def replace(version, **parts): """ Replace one or more parts of a version and return the new string. + .. versionadded:: 2.9.0 + Added :func:`replace` + :param str version: the version string to replace :param dict parts: the parts to be updated. Valid keys are: ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` From 5b8bb16cbd124ae7bf79a11abd61743f5d564c71 Mon Sep 17 00:00:00 2001 From: Thomas Schraitle Date: Sun, 15 Mar 2020 18:54:48 +0100 Subject: [PATCH 057/219] Fix #225: Deprecate module level functions * Add test cases - Add additional test case for "check" - test_should_process_check_iscalled_with_valid_version - Test also missing finalize_version - Test the warning more thoroughly with pytest.warns instead of just pytest.deprecated_call * In `setup.cfg`, add deprecation warnings filter for pytest * Implement DeprecationWarning with warnings module and the new decorator `deprecated` * Output a DeprecationWarning for the following functions: - semver.bump_{major,minor,patch,prerelease,build} - semver.format_version - semver.finalize_version - semver.parse - semver.parse_version_info - semver.replace - semver.VersionInfo._asdict - semver.VersionInfo._astuple Add also a deprecation notice in the docstrings of these functions * Introduce new public functions: - semver.VersionInfo.to_dict (from former _asdict) - semver.VersionInfo.to_tuple (from former _astuple) - Keep _asdict and _astuple as a (deprecated) function for compatibility reasons * Update CHANGELOG.rst * Update usage documentation: - Move some information to make them more useful for for the reader - Add deprecation warning - Explain how to replace deprecated functions - Explain how to display deprecation warnings from semver * Improve documentation of deprecated functions - List deprecated module level functions - Make recommendation and show equivalent code - Mention that deprecated functions will be replaced in semver 3. That means, all deprecated function will be still available in semver 2.x.y. * Move _increment_string into VersionInfo class - Makes removing deprecating functions easier as, for example, bump_prerelease is no longer dependant from an "external" function. - Move _LAST_NUMBER regex into VersionInfo class - Implement _increment_string as a staticmethod Co-authored-by: Karol Co-authored-by: scls19fr Co-authored-by: George Sakkis --- .gitignore | 8 +- CHANGELOG.rst | 16 ++ docs/usage.rst | 254 ++++++++++++++++++++++++------ semver.py | 408 ++++++++++++++++++++++++++++++++----------------- setup.cfg | 2 + test_semver.py | 58 ++++++- 6 files changed, 556 insertions(+), 190 deletions(-) diff --git a/.gitignore b/.gitignore index 994eb868..2ef76af8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ -# Files +# Patch/Diff Files *.patch *.diff -*.kate-swp # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] .pytest_cache/ +*$py.class # Distribution / packaging .cache @@ -72,3 +72,7 @@ docs/_build/ # PyBuilder target/ + +# Backup files +*~ +*.kate-swp diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2ae377d9..48e3d82b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,8 +27,24 @@ Additions * :pr:`228`: Added better doctest integration + Removals -------- +* :gh:`225` (:pr:`229`): Output a DeprecationWarning for the following functions: + + - ``semver.parse`` + - ``semver.parse_version_info`` + - ``semver.format_version`` + - ``semver.bump_{major,minor,patch,prerelease,build}`` + - ``semver.finalize_version`` + - ``semver.replace`` + - ``semver.VersionInfo._asdict`` (use the new, public available + function ``semver.VersionInfo.to_dict()``) + - ``semver.VersionInfo._astuple`` (use the new, public available + function ``semver.VersionInfo.to_tuple()``) + + These deprecated functions will be removed in semver 3. + Version 2.9.1 diff --git a/docs/usage.rst b/docs/usage.rst index dd05fa1c..cdf08b9a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -14,9 +14,10 @@ are met. Knowing the Implemented semver.org Version ------------------------------------------ -The semver.org is the authorative specification of how semantical versioning is -definied. To know which version of semver.org is implemented in the semver -libary, use the following constant:: +The semver.org page is the authorative specification of how semantical +versioning is definied. +To know which version of semver.org is implemented in the semver libary, +use the following constant:: >>> semver.SEMVER_SPEC_VERSION '2.0.0' @@ -25,35 +26,81 @@ libary, use the following constant:: Creating a Version ------------------ -A version can be created in different ways: +Due to historical reasons, the semver project offers two ways of +creating a version: -* as a complete version string:: +* through an object oriented approach with the :class:`semver.VersionInfo` + class. This is the preferred method when using semver. + +* through module level functions and builtin datatypes (usually strings + and dicts). + These method are still available for compatibility reasons, but are + marked as deprecated. Using one of these will emit a DeprecationWarning. + + +.. warning:: **Deprecation Warning** + + Module level functions are marked as *deprecated* in version 2.9.2 now. + These functions will be removed in semver 3. + For details, see the sections :ref:`sec_replace_deprecated_functions` and + :ref:`sec_display_deprecation_warnings`. + + +A :class:`semver.VersionInfo` instance can be created in different ways: + + +* From a string:: - >>> semver.parse_version_info("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') -* with individual parts:: +* From individual parts by a dictionary:: - >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') - '3.4.5-pre.2+build.4' - >>> semver.VersionInfo(3, 5) - VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) + >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + >>> semver.VersionInfo(**d) + VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + + As a minimum requirement, your dictionary needs at least the ``major`` + key, others can be omitted. You get a ``TypeError`` if your + dictionary contains invalid keys. + Only the keys ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` + are allowed. + +* From a tuple:: + + >>> t = (3, 5, 6) + >>> semver.VersionInfo(*t) + VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) You can pass either an integer or a string for ``major``, ``minor``, or ``patch``:: - >>> semver.VersionInfo("3", "5") - VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) + >>> semver.VersionInfo("3", "5", 6) + VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) + +The old, deprecated module level functions are still available. If you +need them, they return different builtin objects (string and dictionary). +Keep in mind, once you have converted a version into a string or dictionary, +it's an ordinary builtin object. It's not a special version object like +the :class:`semver.VersionInfo` class anymore. + +Depending on your use case, the following methods are available: + +* From individual version parts into a string + + In some cases you only need a string from your version data:: + + >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') + '3.4.5-pre.2+build.4' + +* From a string into a dictionary - In the simplest form, ``prerelease`` and ``build`` can also be - integers:: + To access individual parts, you can use the function :func:`semver.parse`:: - >>> semver.VersionInfo(1, 2, 3, 4, 5) - VersionInfo(major=1, minor=2, patch=3, prerelease='4', build='5') + >>> semver.parse("3.4.5-pre.2+build.4") + OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', 'pre.2'), ('build', 'build.4')]) -If you pass an invalid version string you will get a ``ValueError``:: + If you pass an invalid version string you will get a ``ValueError``:: >>> semver.parse("1.2") Traceback (most recent call last): @@ -172,45 +219,30 @@ If you pass invalid keys you get an exception:: .. _sec.convert.versions: -Converting Different Version Types ----------------------------------- +Converting a VersionInfo instance into Different Types +------------------------------------------------------ -Depending which function you call, you get different types -(as explained in the beginning of this chapter). +Sometimes it is needed to convert a :class:`semver.VersionInfo` instance into +a different type. For example, for displaying or to access all parts. -* From a string into :class:`semver.VersionInfo`:: - - >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') +It is possible to convert a :class:`semver.VersionInfo` instance: -* From :class:`semver.VersionInfo` into a string:: +* Into a string with the builtin function :func:`str`:: >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4")) '3.4.5-pre.2+build.4' -* From a dictionary into :class:`semver.VersionInfo`:: +* Into a dictionary with :func:`semver.VersionInfo.to_dict`:: - >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - >>> semver.VersionInfo(**d) - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - - As a minimum requirement, your dictionary needs at least the ``major`` - key, others can be omitted. You get a ``TypeError`` if your - dictionary contains invalid keys. - Only ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` - are allowed. - -* From a tuple into :class:`semver.VersionInfo`:: - - >>> t = (3, 5, 6) - >>> semver.VersionInfo(*t) - VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) + >>> v = semver.VersionInfo(major=3, minor=4, patch=5) + >>> v.to_dict() + OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) -* From a :class:`semver.VersionInfo` into a dictionary:: +* Into a tuple with :func:`semver.VersionInfo.to_tuple`:: - >>> v = semver.VersionInfo(major=3, minor=4, patch=5) - >>> semver.parse(str(v)) == {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': None, 'build': None} - True + >>> v = semver.VersionInfo(major=5, minor=4, patch=2) + >>> v.to_tuple() + (5, 4, 2, None, None) Increasing Parts of a Version @@ -362,8 +394,132 @@ For example: .. code-block:: python - >>> coerce("v1.2") + >>> coerce("v1.2") (VersionInfo(major=1, minor=2, patch=0, prerelease=None, build=None), '') >>> coerce("v2.5.2-bla") (VersionInfo(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') + +.. _sec_replace_deprecated_functions: + +Replacing Deprecated Functions +------------------------------ + +The development team of semver has decided to deprecate certain functions on +the module level. The preferred way of using semver is through the +:class:`semver.VersionInfo` class. + +The deprecated functions can still be used in version 2.x.y. In version 3 of +semver, the deprecated functions will be removed. + +The following list shows the deprecated functions and how you can replace +them with code which is compatible for future versions: + + +* :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` + + Replace them with the respective methods of the :class:`semver.VersionInfo` + class. + For example, the function :func:`semver.bump_major` is replaced by + :func:`semver.VersionInfo.bump_major` and calling the ``str(versionobject)``: + + .. code-block:: python + + >>> s1 = semver.bump_major("3.4.5") + >>> s2 = str(semver.VersionInfo.parse("3.4.5").bump_major()) + >>> s1 == s2 + True + + Likewise with the other module level functions. + +* :func:`semver.finalize_version` + + Replace it with :func:`semver.VersionInfo.finalize_version`: + + .. code-block:: python + + >>> s1 = semver.finalize_version('1.2.3-rc.5') + >>> s2 = str(semver.VersionInfo.parse('1.2.3-rc.5').finalize_version()) + >>> s1 == s2 + True + +* :func:`semver.format_version` + + Replace it with ``str(versionobject)``: + + .. code-block:: python + + >>> s1 = semver.format_version(5, 4, 3, 'pre.2', 'build.1') + >>> s2 = str(semver.VersionInfo(5, 4, 3, 'pre.2', 'build.1')) + >>> s1 == s2 + True + +* :func:`semver.parse` + + Replace it with :func:`semver.VersionInfo.parse` and + :func:`semver.VersionInfo.to_dict`: + + .. code-block:: python + + >>> v1 = semver.parse("1.2.3") + >>> v2 = semver.VersionInfo.parse("1.2.3").to_dict() + >>> v1 == v2 + True + +* :func:`semver.parse_version_info` + + Replace it with :func:`semver.VersionInfo.parse`: + + .. code-block:: python + + >>> v1 = semver.parse_version_info("3.4.5") + >>> v2 = semver.VersionInfo.parse("3.4.5") + >>> v1 == v2 + True + +* :func:`semver.replace` + + Replace it with :func:`semver.VersionInfo.replace`: + + .. code-block:: python + + >>> s1 = semver.replace("1.2.3", major=2, patch=10) + >>> s2 = str(semver.VersionInfo.parse('1.2.3').replace(major=2, patch=10)) + >>> s1 == s2 + True + + +.. _sec_display_deprecation_warnings: + +Displaying Deprecation Warnings +------------------------------- + +By default, deprecation warnings are `ignored in Python `_. +This also affects semver's own warnings. + +It is recommended that you turn on deprecation warnings in your scripts. Use one of +the following methods: + +* Use the option `-Wd `_ + to enable default warnings: + + * Directly running the Python command:: + + $ python3 -Wd scriptname.py + + * Add the option in the shebang line (something like ``#!/usr/bin/python3``) + after the command:: + + #!/usr/bin/python3 -Wd + +* In your own scripts add a filter to ensure that *all* warnings are displayed: + + .. code-block:: python + + import warnings + warnings.simplefilter("default") + # Call your semver code + + For further details, see the section + `Overriding the default filter `_ + of the Python documentation. diff --git a/semver.py b/semver.py index aca7242e..aec5e9ef 100644 --- a/semver.py +++ b/semver.py @@ -3,9 +3,11 @@ import argparse import collections -from functools import wraps +from functools import wraps, partial +import inspect import re import sys +import warnings __version__ = "2.9.1" @@ -14,28 +16,6 @@ __maintainer__ = ["Sebastien Celles", "Tom Schraitle"] __maintainer_email__ = "s.celles@gmail.com" -_REGEX = re.compile( - r""" - ^ - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - (?:-(?P - (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) - (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* - ))? - (?:\+(?P - [0-9a-zA-Z-]+ - (?:\.[0-9a-zA-Z-]+)* - ))? - $ - """, - re.VERBOSE, -) - -_LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") #: Contains the implemented semver.org version of the spec SEMVER_SPEC_VERSION = "2.0.0" @@ -47,10 +27,66 @@ def cmp(a, b): return (a > b) - (a < b) +def deprecated(func=None, replace=None, version=None, category=DeprecationWarning): + """ + Decorates a function to output a deprecation warning. + + This function will be removed once major version 3 of semver is + released. + + :param str replace: the function to replace (use the full qualified + name like ``semver.VersionInfo.bump_major``. + :param str version: the first version when this function was deprecated. + :param category: allow you to specify the deprecation warning class + of your choice. By default, it's :class:`DeprecationWarning`, but + you can choose :class:`PendingDeprecationWarning``or a custom class. + """ + + if func is None: + return partial(deprecated, replace=replace, version=version, category=category) + + @wraps(func) + def wrapper(*args, **kwargs): + msg = ["Function '{m}.{f}' is deprecated."] + + if version: + msg.append("Deprecated since version {v}. ") + msg.append("This function will be removed in semver 3.") + if replace: + msg.append("Use {r!r} instead.") + else: + msg.append("Use the respective 'semver.VersionInfo.{r}' instead.") + + # hasattr is needed for Python2 compatibility: + f = func.__qualname__ if hasattr(func, "__qualname__") else func.__name__ + r = replace or f + + frame = inspect.currentframe().f_back + + msg = " ".join(msg) + warnings.warn_explicit( + msg.format(m=func.__module__, f=f, r=r, v=version), + category=category, + filename=inspect.getfile(frame.f_code), + lineno=frame.f_lineno, + ) + # As recommended in the Python documentation + # https://docs.python.org/3/library/inspect.html#the-interpreter-stack + # better remove the interpreter stack: + del frame + return func(*args, **kwargs) + + return wrapper + + +@deprecated(version="2.9.2") def parse(version): """ Parse version to major, minor, patch, pre-release, build parts. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.parse` instead. + :param version: version string :return: dictionary with the keys 'build', 'major', 'minor', 'patch', and 'prerelease'. The prerelease or build keys can be None @@ -69,17 +105,7 @@ def parse(version): >>> ver['build'] 'build.4' """ - match = _REGEX.match(version) - if match is None: - raise ValueError("%s is not valid SemVer string" % version) - - version_parts = match.groupdict() - - version_parts["major"] = int(version_parts["major"]) - version_parts["minor"] = int(version_parts["minor"]) - version_parts["patch"] = int(version_parts["patch"]) - - return version_parts + return VersionInfo.parse(version).to_dict() def comparator(operator): @@ -110,6 +136,29 @@ class VersionInfo(object): """ __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") + #: Regex for number in a prerelease + _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") + #: Regex for a semver version + _REGEX = re.compile( + r""" + ^ + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + (?:-(?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* + ))? + (?:\+(?P + [0-9a-zA-Z-]+ + (?:\.[0-9a-zA-Z-]+)* + ))? + $ + """, + re.VERBOSE, + ) def __init__(self, major, minor=0, patch=0, prerelease=None, build=None): self._major = int(major) @@ -163,10 +212,38 @@ def build(self): def build(self, value): raise AttributeError("attribute 'build' is readonly") - def _astuple(self): + def to_tuple(self): + """ + Convert the VersionInfo object to a tuple. + + .. versionadded:: 2.9.2 + Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to + make this function available in the public API. + + :return: a tuple with all the parts + :rtype: tuple + + >>> semver.VersionInfo(5, 3, 1).to_tuple() + (5, 3, 1, None, None) + """ return (self.major, self.minor, self.patch, self.prerelease, self.build) - def _asdict(self): + def to_dict(self): + """ + Convert the VersionInfo object to an OrderedDict. + + .. versionadded:: 2.9.2 + Renamed ``VersionInfo._asdict`` to ``VersionInfo.to_dict`` to + make this function available in the public API. + + :return: an OrderedDict with the keys in the order ``major``, ``minor``, + ``patch``, ``prerelease``, and ``build``. + :rtype: :class:`collections.OrderedDict` + + >>> semver.VersionInfo(3, 2, 1).to_dict() + OrderedDict([('major', 3), ('minor', 2), ('patch', 1), \ +('prerelease', None), ('build', None)]) + """ return collections.OrderedDict( ( ("major", self.major), @@ -177,12 +254,43 @@ def _asdict(self): ) ) + # For compatibility reasons: + @deprecated(replace="semver.VersionInfo.to_tuple", version="2.9.2") + def _astuple(self): + return self.to_tuple() # pragma: no cover + + _astuple.__doc__ = to_tuple.__doc__ + + @deprecated(replace="semver.VersionInfo.to_dict", version="2.9.2") + def _asdict(self): + return self.to_dict() # pragma: no cover + + _asdict.__doc__ = to_dict.__doc__ + def __iter__(self): """Implement iter(self).""" # As long as we support Py2.7, we can't use the "yield from" syntax - for v in self._astuple(): + for v in self.to_tuple(): yield v + @staticmethod + def _increment_string(string): + """ + Look for the last sequence of number(s) in a string and increment. + + :param str string: the string to search for. + :return: the incremented string + + Source: + http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 + """ + match = VersionInfo._LAST_NUMBER.search(string) + if match: + next_ = str(int(match.group(1)) + 1) + start, end = match.span(1) + string = string[: max(end - len(next_), start)] + next_ + string[end:] + return string + def bump_major(self): """ Raise the major part of the version, return a new object but leave self @@ -195,7 +303,8 @@ def bump_major(self): >>> ver.bump_major() VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) """ - return parse_version_info(bump_major(str(self))) + cls = type(self) + return cls(self._major + 1) def bump_minor(self): """ @@ -209,7 +318,8 @@ def bump_minor(self): >>> ver.bump_minor() VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) """ - return parse_version_info(bump_minor(str(self))) + cls = type(self) + return cls(self._major, self._minor + 1) def bump_patch(self): """ @@ -223,7 +333,8 @@ def bump_patch(self): >>> ver.bump_patch() VersionInfo(major=3, minor=4, patch=6, prerelease=None, build=None) """ - return parse_version_info(bump_patch(str(self))) + cls = type(self) + return cls(self._major, self._minor, self._patch + 1) def bump_prerelease(self, token="rc"): """ @@ -239,7 +350,9 @@ def bump_prerelease(self, token="rc"): VersionInfo(major=3, minor=4, patch=5, prerelease='rc.2', \ build=None) """ - return parse_version_info(bump_prerelease(str(self), token)) + cls = type(self) + prerelease = cls._increment_string(self._prerelease or (token or "rc") + ".0") + return cls(self._major, self._minor, self._patch, prerelease) def bump_build(self, token="build"): """ @@ -255,41 +368,62 @@ def bump_build(self, token="build"): VersionInfo(major=3, minor=4, patch=5, prerelease='rc.1', \ build='build.10') """ - return parse_version_info(bump_build(str(self), token)) + cls = type(self) + build = cls._increment_string(self._build or (token or "build") + ".0") + return cls(self._major, self._minor, self._patch, self._prerelease, build) @comparator def __eq__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) == 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) == 0 @comparator def __ne__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) != 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) != 0 @comparator def __lt__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) < 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) < 0 @comparator def __le__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) <= 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) <= 0 @comparator def __gt__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) > 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) > 0 @comparator def __ge__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) >= 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) >= 0 def __repr__(self): - s = ", ".join("%s=%r" % (key, val) for key, val in self._asdict().items()) + s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items()) return "%s(%s)" % (type(self).__name__, s) def __str__(self): - return format_version(*(self._astuple())) + """str(self)""" + version = "%d.%d.%d" % (self.major, self.minor, self.patch) + if self.prerelease: + version += "-%s" % self.prerelease + if self.build: + version += "+%s" % self.build + return version def __hash__(self): - return hash(self._astuple()) + return hash(self.to_tuple()) + + def finalize_version(self): + """ + Remove any prerelease and build metadata from the version. + + :return: a new instance with the finalized version string + :rtype: :class:`VersionInfo` + + >>> str(semver.VersionInfo.parse('1.2.3-rc.5').finalize_version()) + '1.2.3' + """ + cls = type(self) + return cls(self.major, self.minor, self.patch) @staticmethod def parse(version): @@ -304,7 +438,17 @@ def parse(version): VersionInfo(major=3, minor=4, patch=5, \ prerelease='pre.2', build='build.4') """ - return parse_version_info(version) + match = VersionInfo._REGEX.match(version) + if match is None: + raise ValueError("%s is not valid SemVer string" % version) + + version_parts = match.groupdict() + + version_parts["major"] = int(version_parts["major"]) + version_parts["minor"] = int(version_parts["minor"]) + version_parts["patch"] = int(version_parts["patch"]) + + return VersionInfo(**version_parts) def replace(self, **parts): """ @@ -320,12 +464,12 @@ def replace(self, **parts): parts :raises: TypeError, if ``parts`` contains invalid keys """ - version = self._asdict() + version = self.to_dict() version.update(parts) try: return VersionInfo(**version) except TypeError: - unknownkeys = set(parts) - set(self._asdict()) + unknownkeys = set(parts) - set(self.to_dict()) error = "replace() got %d unexpected keyword " "argument(s): %s" % ( len(unknownkeys), ", ".join(unknownkeys), @@ -353,16 +497,23 @@ def isvalid(cls, version): def _to_dict(obj): if isinstance(obj, VersionInfo): - return obj._asdict() + return obj.to_dict() elif isinstance(obj, tuple): - return VersionInfo(*obj)._asdict() + return VersionInfo(*obj).to_dict() return obj +@deprecated(replace="semver.VersionInfo.parse", version="2.9.2") def parse_version_info(version): """ Parse version string to a VersionInfo instance. + .. versionadded:: 2.7.2 + Added :func:`parse_version_info` + + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.parse` instead. + .. versionadded:: 2.7.2 Added :func:`parse_version_info` @@ -382,16 +533,7 @@ def parse_version_info(version): >>> version_info.build 'build.4' """ - parts = parse(version) - version_info = VersionInfo( - parts["major"], - parts["minor"], - parts["patch"], - parts["prerelease"], - parts["build"], - ) - - return version_info + return VersionInfo.parse(version) def _nat_cmp(a, b): @@ -458,7 +600,8 @@ def compare(ver1, ver2): 0 """ - v1, v2 = parse(ver1), parse(ver2) + v1 = VersionInfo.parse(ver1).to_dict() + v2 = VersionInfo.parse(ver2).to_dict() return _compare_by_keys(v1, v2) @@ -550,10 +693,14 @@ def min_ver(ver1, ver2): return ver2 +@deprecated(replace="str(versionobject)", version="2.9.2") def format_version(major, minor, patch, prerelease=None, build=None): """ Format a version string according to the Semantic Versioning specification. + .. deprecated:: 2.9.2 + Use ``str(VersionInfo(VERSION)`` instead. + :param int major: the required major part of a version :param int minor: the required minor part of a version :param int patch: the required patch part of a version @@ -565,34 +712,17 @@ def format_version(major, minor, patch, prerelease=None, build=None): >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') '3.4.5-pre.2+build.4' """ - version = "%d.%d.%d" % (major, minor, patch) - if prerelease is not None: - version = version + "-%s" % prerelease - - if build is not None: - version = version + "+%s" % build - - return version - - -def _increment_string(string): - """ - Look for the last sequence of number(s) in a string and increment, from: - - http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 - """ - match = _LAST_NUMBER.search(string) - if match: - next_ = str(int(match.group(1)) + 1) - start, end = match.span(1) - string = string[: max(end - len(next_), start)] + next_ + string[end:] - return string + return str(VersionInfo(major, minor, patch, prerelease, build)) +@deprecated(version="2.9.2") def bump_major(version): """ Raise the major part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_major` instead. + :param: version string :return: the raised version string :rtype: str @@ -600,14 +730,17 @@ def bump_major(version): >>> semver.bump_major("3.4.5") '4.0.0' """ - verinfo = parse(version) - return format_version(verinfo["major"] + 1, 0, 0) + return str(VersionInfo.parse(version).bump_major()) +@deprecated(version="2.9.2") def bump_minor(version): """ Raise the minor part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_minor` instead. + :param: version string :return: the raised version string :rtype: str @@ -615,14 +748,17 @@ def bump_minor(version): >>> semver.bump_minor("3.4.5") '3.5.0' """ - verinfo = parse(version) - return format_version(verinfo["major"], verinfo["minor"] + 1, 0) + return str(VersionInfo.parse(version).bump_minor()) +@deprecated(version="2.9.2") def bump_patch(version): """ Raise the patch part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_patch` instead. + :param: version string :return: the raised version string :rtype: str @@ -630,14 +766,17 @@ def bump_patch(version): >>> semver.bump_patch("3.4.5") '3.4.6' """ - verinfo = parse(version) - return format_version(verinfo["major"], verinfo["minor"], verinfo["patch"] + 1) + return str(VersionInfo.parse(version).bump_patch()) +@deprecated(version="2.9.2") def bump_prerelease(version, token="rc"): """ Raise the prerelease part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_prerelease` instead. + :param version: version string :param token: defaults to 'rc' :return: the raised version string @@ -646,19 +785,17 @@ def bump_prerelease(version, token="rc"): >>> semver.bump_prerelease('3.4.5', 'dev') '3.4.5-dev.1' """ - verinfo = parse(version) - verinfo["prerelease"] = _increment_string( - verinfo["prerelease"] or (token or "rc") + ".0" - ) - return format_version( - verinfo["major"], verinfo["minor"], verinfo["patch"], verinfo["prerelease"] - ) + return str(VersionInfo.parse(version).bump_prerelease(token)) +@deprecated(version="2.9.2") def bump_build(version, token="build"): """ Raise the build part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_build` instead. + :param version: version string :param token: defaults to 'build' :return: the raised version string @@ -667,17 +804,10 @@ def bump_build(version, token="build"): >>> semver.bump_build('3.4.5-rc.1+build.9') '3.4.5-rc.1+build.10' """ - verinfo = parse(version) - verinfo["build"] = _increment_string(verinfo["build"] or (token or "build") + ".0") - return format_version( - verinfo["major"], - verinfo["minor"], - verinfo["patch"], - verinfo["prerelease"], - verinfo["build"], - ) + return str(VersionInfo.parse(version).bump_build(token)) +@deprecated(version="2.9.2") def finalize_version(version): """ Remove any prerelease and build metadata from the version string. @@ -685,6 +815,9 @@ def finalize_version(version): .. versionadded:: 2.7.9 Added :func:`finalize_version` + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.finalize_version` instead. + :param version: version string :return: the finalized version string :rtype: str @@ -692,8 +825,33 @@ def finalize_version(version): >>> semver.finalize_version('1.2.3-rc.5') '1.2.3' """ - verinfo = parse(version) - return format_version(verinfo["major"], verinfo["minor"], verinfo["patch"]) + verinfo = VersionInfo.parse(version) + return str(verinfo.finalize_version()) + + +@deprecated(version="2.9.2") +def replace(version, **parts): + """ + Replace one or more parts of a version and return the new string. + + .. versionadded:: 2.9.0 + Added :func:`replace` + + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.replace` instead. + + :param str version: the version string to replace + :param dict parts: the parts to be updated. Valid keys are: + ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` + :return: the replaced version string + :raises: TypeError, if ``parts`` contains invalid keys + :rtype: str + + >>> import semver + >>> semver.replace("1.2.3", major=2, patch=10) + '2.2.10' + """ + return str(VersionInfo.parse(version).replace(**parts)) def cmd_bump(args): @@ -719,7 +877,7 @@ def cmd_bump(args): # print the help and exit args.parser.parse_args(["bump", "-h"]) - ver = parse_version_info(args.version) + ver = VersionInfo.parse(args.version) # get the respective method and call it func = getattr(ver, maptable[args.bump]) return str(func()) @@ -838,28 +996,6 @@ def main(cliargs=None): return 2 -def replace(version, **parts): - """ - Replace one or more parts of a version and return the new string. - - .. versionadded:: 2.9.0 - Added :func:`replace` - - :param str version: the version string to replace - :param dict parts: the parts to be updated. Valid keys are: - ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` - :return: the replaced version string - :raises: TypeError, if ``parts`` contains invalid keys - :rtype: str - - >>> import semver - >>> semver.replace("1.2.3", major=2, patch=10) - '2.2.10' - """ - version = parse_version_info(version) - return str(version.replace(**parts)) - - if __name__ == "__main__": import doctest diff --git a/setup.cfg b/setup.cfg index 6ab7e562..1cefc4bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,8 @@ [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ testpaths = . docs +filterwarnings = + ignore:Function 'semver.*:DeprecationWarning addopts = --no-cov-on-fail --cov=semver diff --git a/test_semver.py b/test_semver.py index ed7d80bc..4611d771 100644 --- a/test_semver.py +++ b/test_semver.py @@ -14,6 +14,7 @@ cmd_compare, compare, createparser, + deprecated, finalize_version, format_version, main, @@ -54,9 +55,7 @@ def does_not_raise(item): "string,expected", [("rc", "rc"), ("rc.1", "rc.2"), ("2x", "3x")] ) def test_should_private_increment_string(string, expected): - from semver import _increment_string - - assert _increment_string(string) == expected + assert VersionInfo._increment_string(string) == expected @pytest.fixture @@ -393,6 +392,18 @@ def test_should_versioninfo_bump_multiple(): assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == expected +def test_should_versioninfo_to_dict(version): + resultdict = version.to_dict() + assert isinstance(resultdict, dict), "Got type from to_dict" + assert list(resultdict.keys()) == ["major", "minor", "patch", "prerelease", "build"] + + +def test_should_versioninfo_to_tuple(version): + result = version.to_tuple() + assert isinstance(result, tuple), "Got type from to_dict" + assert len(result) == 5, "Different length from to_tuple()" + + def test_should_ignore_extensions_for_bump(): assert bump_patch("3.4.5-rc1+build4") == "3.4.6" @@ -777,6 +788,13 @@ def test_should_raise_systemexit_when_bump_iscalled_with_empty_arguments(): main(["bump"]) +def test_should_process_check_iscalled_with_valid_version(capsys): + result = main(["check", "1.1.1"]) + assert not result + captured = capsys.readouterr() + assert not captured.out + + @pytest.mark.parametrize( "version,parts,expected", [ @@ -827,3 +845,37 @@ def test_replace_raises_ValueError_for_non_numeric_values(): def test_should_versioninfo_isvalid(): assert VersionInfo.isvalid("1.0.0") is True assert VersionInfo.isvalid("foo") is False + + +@pytest.mark.parametrize( + "func, args, kwargs", + [ + (bump_build, ("1.2.3",), {}), + (bump_major, ("1.2.3",), {}), + (bump_minor, ("1.2.3",), {}), + (bump_patch, ("1.2.3",), {}), + (bump_prerelease, ("1.2.3",), {}), + (format_version, (3, 4, 5), {}), + (finalize_version, ("1.2.3-rc.5",), {}), + (parse, ("1.2.3",), {}), + (parse_version_info, ("1.2.3",), {}), + (replace, ("1.2.3",), dict(major=2, patch=10)), + ], +) +def test_should_raise_deprecation_warnings(func, args, kwargs): + with pytest.warns( + DeprecationWarning, match=r"Function 'semver.[_a-zA-Z]+' is deprecated." + ) as record: + func(*args, **kwargs) + if not record: + pytest.fail("Expected a DeprecationWarning for {}".format(func.__name__)) + assert len(record), "Expected one DeprecationWarning record" + + +def test_deprecated_deco_without_argument(): + @deprecated + def mock_func(): + return True + + with pytest.deprecated_call(): + assert mock_func() From b145931366a03ba8a763dc550dab746a1bbc3594 Mon Sep 17 00:00:00 2001 From: Thomas Schraitle Date: Sat, 18 Apr 2020 00:28:21 +0200 Subject: [PATCH 058/219] Fix #235: Shift focus on semver.VersionInfo.* * Module level functions like `semver.bump_version` are still available in the documentation, but they play a much less important role now. The preferred way is to use semver.Versioninfo instances to use. * Replace 2.9.2 -> 2.10.0 due to #237 * Fix docstring examples --- CHANGELOG.rst | 6 +++- README.rst | 58 ++++++++++++-------------------- docs/pysemver.rst | 5 ++- docs/usage.rst | 66 ++++++++++++++++++------------------ semver.py | 85 +++++++++++++++++++++++------------------------ 5 files changed, 104 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 48e3d82b..ddefadcb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,7 @@ Change Log All notable changes to this code base will be documented in this file, in every released version. -Version 2.9.x (WIP) +Version 2.10.0 (WIP) =================== :Released: 2020-xx-yy @@ -15,6 +15,10 @@ Version 2.9.x (WIP) Features -------- +* :pr:`235`: Improved documentation and shift focus on ``semver.VersionInfo`` instead of advertising + the old and deprecated module-level functions. + + Bug Fixes --------- diff --git a/README.rst b/README.rst index 862fbdc6..265aac46 100644 --- a/README.rst +++ b/README.rst @@ -9,18 +9,23 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. .. teaser-end -.. note:: +.. warning:: - With version 2.9.0 we've moved the GitHub project. The project is now - located under the organization ``python-semver``. - The complete URL is:: + As anything comes to an end, this project will focus on Python 3.x only. + New features and bugfixes will be integrated into the 3.x.y branch only. - https://github.com/python-semver/python-semver + Major version 3 of semver will contain some incompatible changes: - If you still have an old repository, correct your upstream URL to the new URL:: + * removes support for Python 2.7 and 3.3 + * removes deprecated functions (see :ref:`sec_replace_deprecated_functions` for + further information). - $ git remote set-url upstream git@github.com:python-semver/python-semver.git + The last version of semver which supports Python 2.7 and 3.4 will be + 2.10.x. However, keep in mind, version 2.10.x is frozen: no new + features nor backports will be integrated. + We recommend to upgrade your workflow to Python 3.x to gain support, + bugfixes, and new features. The module follows the ``MAJOR.MINOR.PATCH`` style: @@ -30,23 +35,6 @@ The module follows the ``MAJOR.MINOR.PATCH`` style: Additional labels for pre-release and build metadata are supported. - -.. warning:: - - Major version 3.0.0 of semver will remove support for Python 2.7 and 3.4. - - As anything comes to an end, this project will focus on Python 3.x. - New features and bugfixes will be integrated only into the 3.x.y branch - of semver. - - The last version of semver which supports Python 2.7 and 3.4 will be - 2.9.x. However, keep in mind, version 2.9.x is frozen: no new - features nor backports will be integrated. - - We recommend to upgrade your workflow to Python 3.x to gain support, - bugfixes, and new features. - - To import this library, use: .. code-block:: python @@ -54,31 +42,29 @@ To import this library, use: >>> import semver Working with the library is quite straightforward. To turn a version string into the -different parts, use the `semver.parse` function: +different parts, use the ``semver.VersionInfo.parse`` function: .. code-block:: python - >>> ver = semver.parse('1.2.3-pre.2+build.4') - >>> ver['major'] + >>> ver = semver.VersionInfo.parse('1.2.3-pre.2+build.4') + >>> ver.major 1 - >>> ver['minor'] + >>> ver.minor 2 - >>> ver['patch'] + >>> ver.patch 3 - >>> ver['prerelease'] + >>> ver.prerelease 'pre.2' - >>> ver['build'] + >>> ver.build 'build.4' To raise parts of a version, there are a couple of functions available for -you. The `semver.parse_version_info` function converts a version string -into a `semver.VersionInfo` class. The function -`semver.VersionInfo.bump_major` leaves the original object untouched, but -returns a new `semver.VersionInfo` instance with the raised major part: +you. The function :func:`semver.VersionInfo.bump_major` leaves the original object untouched, but +returns a new :class:`semver.VersionInfo` instance with the raised major part: .. code-block:: python - >>> ver = semver.parse_version_info("3.4.5") + >>> ver = semver.VersionInfo.parse("3.4.5") >>> ver.bump_major() VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) diff --git a/docs/pysemver.rst b/docs/pysemver.rst index 3c247b62..b0896eea 100644 --- a/docs/pysemver.rst +++ b/docs/pysemver.rst @@ -10,8 +10,7 @@ Synopsis .. code:: bash - pysemver compare - pysemver bump {major,minor,patch,prerelease,build} + pysemver