From 91cbce94879cd04de6d4ea3ab11929cf13742a39 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 20 Nov 2019 21:32:02 +0100 Subject: [PATCH 001/174] 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 002/174] 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 003/174] 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 004/174] 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 005/174] 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 006/174] 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 007/174] 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 008/174] 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 009/174] 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 010/174] 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 011/174] 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 012/174] 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 013/174] 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 014/174] 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 015/174] 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 016/174] 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 017/174] 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 018/174] 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