Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command Line Interface #59

Closed
mattrohland opened this issue Jul 3, 2017 · 20 comments · Fixed by #164
Closed

Command Line Interface #59

mattrohland opened this issue Jul 3, 2017 · 20 comments · Fixed by #164

Comments

@mattrohland
Copy link

I've been working on a continuous integration pipeline that needs to perform very basic operations on an applications current version. The application's pipeline is currently a series of Shell commands strung together and while it would be possible to rework much of the pipeline as a Python script that uses python-semver as-is, extending python-semver with a command line interface ended up being a much lower level of effort.

While the interface I wrote is far from comprehensive, I was wondering if a CLI was something you'd be interested in having as part of the python-semver core package.

Here is a Gist of what I'd written for my immediate needs: https://gist.github.com/mattrohland/43b47bd079bd3d47adf674cbdbd970c3

@k-bx
Copy link
Contributor

k-bx commented Jul 5, 2017

@mattrohland yes, I'd definitely be interested in something like this and would be happy to get a PR!

One thing I should mention – I didn't quite follow Python's state-of-the-art on providing executables with packages, and not sure how to do it best to not irritate our users, not force them running the command in "sudo" etc. In Haskell packages, you can provide an executable description, and upon install it gets installed into the ~/.local/bin, then a user is free to add that to their PATH. I like the approach.

Anyways, if there's a similar popular way to handle things in Python and community is generally happy with it, I'd be glad to receive a PR adding the command-line functionality. Thanks!

@weakcamel
Copy link

Hello,
I also could do with CLI for semver and although the npm semver is there, I'd much more appreciate a Python script to avoid dependency on Javascript in my CI.

I'm happy to give a hand if need be?

@k-bx
Copy link
Contributor

k-bx commented Aug 26, 2017

@weakcamel if @mattrohland doesn't give his progress update in a day – feel free to take over the work, I'm sure it's not that big task to implement

@scls19fr
Copy link
Member

scls19fr commented May 12, 2018

What requirements do you accept?

Click http://click.pocoo.org/5/
Docopt http://docopt.org

Or just want us to use standard argument parser libray argparse https://docs.python.org/3/library/argparse.html#module-argparse

@weakcamel
Copy link

Oops. I missed the previous update and then went AWOL myself - sorry about that.

Either is good really, as long as the requirements are defined in setup.py
Plain argparse sounds good as it would keep this module dependency-free, but in my personal opinion: if only dependencies are available on PyPI then whatever works.

@scls19fr
Copy link
Member

scls19fr commented May 15, 2018

I personally like "click" http://click.pocoo.org/5/why/
Having tests for CLI http://click.pocoo.org/6/setuptools/#testing-the-script is something which should also be considered
http://click.pocoo.org/6/testing/

@lorengordon
Copy link

upon install it gets installed into the ~/.local/bin, then a user is free to add that to their PATH. I like the approach

It's pretty easy with setuptools and the entry_points option to create console scripts automatically:

When installing, use pip install --user ... and the script goes in the user space rather than the system space, so no sudo required.

Just need a python script that wraps the main library functions and exposes arguments using something like argparse or click. I've used both, but have mostly gone back to argparse since it's in the standard library, and now that py2.6 is mostly out the door.

@weakcamel
Copy link

Okey-dokey. So, is anyone else taking a stab at this or is it free for taking?

+1 to the setuptools entry point in any case.

@lorengordon
Copy link

Almost a year later, I'd say it's still free for the taking! 😆

@scls19fr
Copy link
Member

@weakcamel , contributions are highly appreciated!

@scls19fr
Copy link
Member

scls19fr commented May 27, 2018

Here is a possible CLI implementation with argparse

"""python-semver CLI
Semantic Versioning Command Line Interface"""

import argparse
import semver


def create_parser():
    parser = argparse.ArgumentParser(description='Semantic Versioning CLI.')
    parser.add_argument('--compare', nargs=2, metavar=('a', 'b'),
                        help='Compare version `a` with version `b`')
    parser.add_argument('--bump_major', nargs=1, metavar='a',
                        help='Bump major version `a`')
    parser.add_argument('--bump_minor', nargs=1, metavar='a',
                        help='Bump minor version `a`')
    parser.add_argument('--bump_patch', nargs=1, metavar='a',
                        help='Bump patch version `a`')
    parser.add_argument('-v', '--version', action='store_true',
                        help='Print python-semver version')
    return parser


def parse_args(args):
    if args.compare is not None:
        s = semver.compare(*args.compare)
    elif args.bump_major is not None:
        s = semver.bump_major(*args.bump_major)
    elif args.bump_minor is not None:
        s = semver.bump_minor(*args.bump_minor)
    elif args.bump_patch is not None:
        s = semver.bump_patch(*args.bump_patch)
    elif args.version:
        s = semver.__version__
    else:
        return
    return s


def main():
    parser = create_parser()
    args = parser.parse_args()
    s = parse_args(args)
    if s is None:
        parser.print_help()
    else:
        print(s)


if __name__ == '__main__':
    main()

and tests

def test_semver_cli():
    parser = create_parser()

    args = parser.parse_args(['-v'])
    v = parse_args(args)
    assert v == __version__

    args = parser.parse_args(['--compare', '1.0.0', '2.0.0'])
    v = parse_args(args)
    assert v == -1

    args = parser.parse_args(['--bump_major', '3.4.5'])
    v = parse_args(args)
    assert v == '4.0.0'

    args = parser.parse_args(['--bump_minor', '3.4.5'])
    v = parse_args(args)
    assert v == '3.5.0'

    args = parser.parse_args(['--bump_patch', '3.4.5'])
    v = parse_args(args)
    assert v == '3.4.6'

@weakcamel
Copy link

Oki-doki :-) I'm happy to do the mechanics (update setup.py and create the PR) .

@scls19fr
Copy link
Member

That's nice @weakcamel !

@weakcamel
Copy link

weakcamel commented May 30, 2018

@scls19fr a few questions (I'll try to keep them to a few) as this would be my first contribution here:

  • is it ok if I run tests (with tox) on my machine only with py27 / py35 - or should I try and work out my environment to handle all of them?
    • if it's the lattter, some tips would be appreciated, my experience with tox is very limited - I'm happy to read the docs on my own though, if only I'm sure what to stick to
  • should I keep all of the code in this 1 code + 1 test layout?

@scls19fr
Copy link
Member

You can test locally on every environnement using simply

python setup.py test

or simply run manually tests and PEP8 checks using

py.test
flake8

Anyway CI will catch problem for you for others Python versions.

Not sure I understand correctly your last question.

You need to put cli in a separate file and tests in an other file test_semver.py.

@weakcamel
Copy link

Anyway CI will catch problem for you for others Python versions.

Great, thanks!

Not sure I understand correctly your last question.
You need to put cli in a separate file and tests in an other file test_semver.py.

Sorry for an unclear question yet your answer was spot on anyway :-)

To clarify, I've been wondering whether I should put the main inside semver.py (it surely would work too) or keep the CLI code separate. That answered it, thanks!

@scls19fr
Copy link
Member

the reason why I prefer a separate file for CLI is that we have now doctests in semver.py

@tomschr
Copy link
Member

tomschr commented Oct 3, 2019

I would prefer to use the argparse module instead of the others. Reasons:

  • semver should NOT depend on external libraries. It should be a "standalone" library. We all know the issues when we depend on external libraries, don't we?
  • argparse is already included in the standard library; although it might be not perfect, it's there.
  • the CLI interface to create with argparse is not that difficult to use. Just a simple interface doesn't qualify it to depend on external libraries just for the sake of "being simpler" (but that's just my opinion, others my disagree).

You brought up different solutions where to put the CLI interface:

  • into the semver.py file
  • into another, separate file

However, we didn't discuss a third solution:

  • turn semver from a package into a module (meaning replace semver.py into a semver/ directory with __init__.py and other files).

With the third solution, you could add a cli.py file, adapt the setup.py to create a semver.py automatically through entry points and you're done. Would be probably a bit too much, wouldn't it? 😉

Nevertheless, I would propose this CLI API (assuming we have a CLI script semver in the path):

$ semver -h
usage: semver [-h] {compare,bump} ...

Semantic Versioning Command Line Interface

positional arguments:
  {compare,bump}  commands
    compare       Compares two versions
    bump          Bumps a version

optional arguments:
  -h, --help      show this help message and exit
$ semver bump major "1.2.3"
2.0.0
$ semver bump minor "1.2.3"
1.3.0
$ semver bump patch "1.2.3"
1.2.4
$ semver bump patch "x"
ERROR: x is not valid SemVer string
# => return value would be != 0
$ semver compare "1.3.0" "1.1.0"
1

It would be easy to extend, for example, adding a "match" subcommand or doing other fancy stuff.

My implementation is inspired by @scls19fr and would use the above lines:

"""
Semantic Versioning Command Line Interface
"""

import argparse
import sys

from semver import compare, parse_version_info


def parsecli(cliargs=None):
    """Parse CLI with :class:`argparse.ArgumentParser` and return parsed result

    :param list cliargs: Arguments to parse or None (=use sys.argv)
    :return: parsed CLI result and parser instance
    :rtype: tuple(:class:`argparse.Namespace`, :class:`argparse.ArgumentParser`)
    """
    parser = argparse.ArgumentParser(prog=__package__,
                                     description=__doc__)
    s = parser.add_subparsers(help="commands")
    
    # create compare subcommand
    parser_compare = s.add_parser("compare",
                                  help="Compares two versions"
                                  )
    parser_compare.set_defaults(which="compare")
    parser_compare.add_argument("version1",
                                help="First version"
                                )
    parser_compare.add_argument("version2",
                                help="First 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"
                       )

    args = parser.parse_args(args=cliargs)
    return args, parser
    
    
def process(args, parser):
    """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`
    """
    print("args:", args)
    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])
        print(func())
    
    elif args.which == "compare":
        res = compare(args.version1, args.version2)
        print(res)
    
    return 0


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:
        return process(*parsecli(cliargs))
    
    except (ValueError, TypeError) as err:
        print("ERROR", err, file=sys.stderr)
        return 2

tomschr added a commit to tomschr/python-semver that referenced this issue Oct 13, 2019
* 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; combines
    parsecli() and process()

* 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
tomschr added a commit to tomschr/python-semver that referenced this issue Oct 13, 2019
* 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; combines
    parsecli() and process()

* 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
tomschr added a commit to tomschr/python-semver that referenced this issue Oct 13, 2019
* 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
tomschr added a commit to tomschr/python-semver that referenced this issue Oct 13, 2019
* 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
tomschr added a commit to tomschr/python-semver that referenced this issue Oct 13, 2019
* 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
@tomschr
Copy link
Member

tomschr commented Oct 13, 2019

@scls19fr, @k-bx, @mattrohland @weakcamel
I've played with the argparse module a bit and implemented a "proof-of-concept". It's an idea. For details, see the respective pull request.

Feel free to discuss it. 😉

(Sorry for the noise. But for some reasons, there seems to be a conflict in the file docs/conf.py. I've tried to fix it, but maybe others have a better idea than me.)

tomschr added a commit to tomschr/python-semver that referenced this issue Oct 13, 2019
* 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
@tomschr
Copy link
Member

tomschr commented Oct 13, 2019

Ok, found it. I forgot to pull from the original, upstream repo, argh, my fault. The conflict is fixed now.

tomschr added a commit to tomschr/python-semver that referenced this issue Oct 13, 2019
* 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

* Update CHANGELOG.rst
tomschr added a commit to tomschr/python-semver that referenced this issue Oct 13, 2019
* Extend setup.py with entry_point key and point to semver.main.
  The script is named "pysemver"

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

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

* Extend documentation
  * Add sphinx-argparse as a doc requirement
  * Include new cli.rst file which (self)documents the
    arguments of the semver script with the help of sphinx-argparse
  * Extend extensions variable in conf.py to be able to use the
    sphinx-argparse module

* Update CHANGELOG.rst
scls19fr pushed a commit that referenced this issue Oct 13, 2019
* Extend setup.py with entry_point key and point to semver.main.
  The script is named "pysemver"

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

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

* Extend documentation
  * Add sphinx-argparse as a doc requirement
  * Include new cli.rst file which (self)documents the
    arguments of the semver script with the help of sphinx-argparse
  * Extend extensions variable in conf.py to be able to use the
    sphinx-argparse module

* Update CHANGELOG.rst
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants