Skip to content

Commit

Permalink
Fix python-semver#59: Implement command line interface
Browse files Browse the repository at this point in the history
* Extend setup.py with entry_point key and point to semver.main.
  The script is named "semver"

* Introduce 3 new functions:
  * createparser: creates and returns an argparse.ArgumentParser
    instance
  * process: process the CLI arguments and call the requested actions
  * main: entry point for the application script

* Add test cases
  * sort import lines of semver functions/class with isort tool
  * sort list of SEMVERFUNCS variable

* Extend documentation
  * Add sphinx-argparse as a doc requirement
  * Include new cli.rst file which (self)documents the
    arguments of the semver script with the help of sphinx-argparse
  * Extend extensions variable in conf.py to be able to use the
    sphinx-argparse module
  • Loading branch information
tomschr committed Oct 13, 2019
1 parent c585f5c commit 81d2012
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 20 deletions.
46 changes: 46 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
CLI
===

The library provides also a command line interface. This allows to include
the functionality of semver into shell scripts.

Using the semver Script
-----------------------

The script name is :command:`semver` and provides the subcommands ``bump``
and ``compare``.

To bump a version, you pass the name of the part (major, minor, patch, prerelease, or
build) and the version string, for example::

$ semver bump major 1.2.3
2.0.0
$ semver bump minor 1.2.3
1.3.0

If you pass a version string which is not a valid semantical version, you get
an error message::

$ semver bump build 1.5
ERROR 1.5 is not valid SemVer string

To compare two versions, use the ``compare`` subcommand. The result is

* ``-1`` if first version is smaller than the second version,
* ``0`` if both are the same,
* ``1`` if the first version is greater than the second version.

For example::

$ semver compare 1.2.3 2.4.0
.. _interface:

Interface
---------

.. argparse::
:ref: semver.createparser
:prog: semver
.. :path: compare
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.napoleon',
'sphinxarg.ext',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Semver |version| -- Semantic Versioning
readme
install
usage
cli
development
api

Expand Down
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# requirements file for documentation
sphinx
sphinx_rtd_theme
sphinx-argparse
100 changes: 98 additions & 2 deletions semver.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""
Python helper for Semantic Versioning (http://semver.org/)
"""
from __future__ import print_function

import argparse
import collections
import re

from functools import wraps
import re
import sys


__version__ = '2.8.2'
Expand Down Expand Up @@ -604,6 +606,100 @@ def finalize_version(version):
return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'])


def createparser():
"""Create an :class:`argparse.ArgumentParser` instance
:return: parser instance
:rtype: :class:`argparse.ArgumentParser`
"""
parser = argparse.ArgumentParser(prog=__package__,
description=__doc__)
s = parser.add_subparsers()

# create compare subcommand
parser_compare = s.add_parser("compare",
help="Compare two versions"
)
parser_compare.set_defaults(which="compare")
parser_compare.add_argument("version1",
help="First version"
)
parser_compare.add_argument("version2",
help="Second version"
)

# create bump subcommand
parser_bump = s.add_parser("bump",
help="Bumps a version"
)
parser_bump.set_defaults(which="bump")
sb = parser_bump.add_subparsers(title="Bump commands",
dest="bump")

# Create subparsers for the bump subparser:
for p in (sb.add_parser("major",
help="Bump the major part of the version"),
sb.add_parser("minor",
help="Bump the minor part of the version"),
sb.add_parser("patch",
help="Bump the patch part of the version"),
sb.add_parser("prerelease",
help="Bump the prerelease part of the version"),
sb.add_parser("build",
help="Bump the build part of the version")):
p.add_argument("version",
help="Version to raise"
)

return parser


def process(args):
"""Process the input from the CLI
:param args: The parsed arguments
:type args: :class:`argparse.Namespace`
:param parser: the parser instance
:type parser: :class:`argparse.ArgumentParser`
:return: result of the selected action
:rtype: str
"""
if args.which == "bump":
maptable = {'major': 'bump_major',
'minor': 'bump_minor',
'patch': 'bump_patch',
'prerelease': 'bump_prerelease',
'build': 'bump_build',
}
ver = parse_version_info(args.version)
# get the respective method and call it
func = getattr(ver, maptable[args.bump])
return str(func())

elif args.which == "compare":
return str(compare(args.version1, args.version2))


def main(cliargs=None):
"""Entry point for the application script
:param list cliargs: Arguments to parse or None (=use :class:`sys.argv`)
:return: error code
:rtype: int
"""
try:
parser = createparser()
args = parser.parse_args(args=cliargs)
# args.parser = parser
result = process(args)
print(result)
return 0

except (ValueError, TypeError) as err:
print("ERROR", err, file=sys.stderr)
return 2


if __name__ == "__main__":
import doctest
doctest.testmod()
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,7 @@ def read_file(filename):
'clean': Clean,
'test': Tox,
},
entry_points={
'console_scripts': ['semver = semver:main'],
}
)
101 changes: 83 additions & 18 deletions test_semver.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
from argparse import Namespace
import pytest # noqa

from semver import compare
from semver import match
from semver import parse
from semver import format_version
from semver import bump_major
from semver import bump_minor
from semver import bump_patch
from semver import bump_prerelease
from semver import bump_build
from semver import finalize_version
from semver import min_ver
from semver import max_ver
from semver import VersionInfo
from semver import parse_version_info

from semver import (VersionInfo,
bump_build,
bump_major,
bump_minor,
bump_patch,
bump_prerelease,
compare,
createparser,
finalize_version,
format_version,
main,
match,
max_ver,
min_ver,
parse,
parse_version_info,
process,
)

SEMVERFUNCS = [
compare, match, parse, format_version,
bump_major, bump_minor, bump_patch, bump_prerelease, bump_build,
max_ver, min_ver, finalize_version
compare, createparser,
bump_build, bump_major, bump_minor, bump_patch, bump_prerelease,
finalize_version, format_version,
match, max_ver, min_ver, parse, process,
]


Expand Down Expand Up @@ -580,3 +585,63 @@ def test_should_be_able_to_use_integers_as_prerelease_build():
assert isinstance(v.prerelease, str)
assert isinstance(v.build, str)
assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, '4', '5')


@pytest.mark.parametrize("cli,expected", [
(["bump", "major", "1.2.3"],
Namespace(which='bump', bump='major', version='1.2.3')),
(["bump", "minor", "1.2.3"],
Namespace(which='bump', bump='minor', version='1.2.3')),
(["bump", "patch", "1.2.3"],
Namespace(which='bump', bump='patch', version='1.2.3')),
(["bump", "prerelease", "1.2.3"],
Namespace(which='bump', bump='prerelease', version='1.2.3')),
(["bump", "build", "1.2.3"],
Namespace(which='bump', bump='build', version='1.2.3')),
# ---
(["compare", "1.2.3", "2.1.3"],
Namespace(which='compare', version1='1.2.3', version2='2.1.3')),
])
def test_should_parse_cli_arguments(cli, expected):
parser = createparser()
assert parser
result = parser.parse_args(cli)
assert result == expected


@pytest.mark.parametrize("args,expected", [
# bump subcommand
(Namespace(which='bump', bump='major', version='1.2.3'),
"2.0.0"),
(Namespace(which='bump', bump='minor', version='1.2.3'),
"1.3.0"),
(Namespace(which='bump', bump='patch', version='1.2.3'),
"1.2.4"),
(Namespace(which='bump', bump='prerelease', version='1.2.3-rc1'),
"1.2.3-rc2"),
(Namespace(which='bump', bump='build', version='1.2.3+build.13'),
"1.2.3+build.14"),
# compare subcommand
(Namespace(which='compare', version1='1.2.3', version2='2.1.3'),
"-1"),
(Namespace(which='compare', version1='1.2.3', version2='1.2.3'),
"0"),
(Namespace(which='compare', version1='2.4.0', version2='2.1.3'),
"1"),
])
def test_should_process_parsed_cli_arguments(args, expected):
assert process(args) == expected


def test_should_process_print(capsys):
rc = main(["bump", "major", "1.2.3"])
assert rc == 0
captured = capsys.readouterr()
assert captured.out.rstrip() == "2.0.0"


def test_should_process_raise_error(capsys):
rc = main(["bump", "major", "1.2"])
assert rc != 0
captured = capsys.readouterr()
assert captured.err.startswith("ERROR")

0 comments on commit 81d2012

Please sign in to comment.