From 59dfe13f5b31a4160fb0365dfab0c74d674b1e3b Mon Sep 17 00:00:00 2001 From: Anthon van der Neut Date: Wed, 15 Oct 2014 22:27:57 +0200 Subject: added documentation and example programs --- Makefile | 8 +- README.rst | 304 +++++++++++++++++++++++++++++++++++++++++++ __init__.py | 2 +- _action/checksinglestore.py | 6 +- example/aliases.py | 13 ++ example/checksingleaction.py | 9 ++ example/countaction.py | 11 ++ example/smartformatter.py | 28 ++++ example/splitaction.py | 11 ++ example/testcmd.py | 44 +++++++ test/test_argparse.py | 2 +- 11 files changed, 430 insertions(+), 8 deletions(-) create mode 100644 example/aliases.py create mode 100644 example/checksingleaction.py create mode 100644 example/countaction.py create mode 100644 example/smartformatter.py create mode 100644 example/splitaction.py create mode 100644 example/testcmd.py diff --git a/Makefile b/Makefile index a44c161..81e8fb1 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,8 @@ clean: rm -rf build .tox $(PKGNAME).egg-info/ README.pdf find . -name "*.pyc" -exec rm {} + -BBASE:=ssh://hg@bitbucket.org/ruamel -BB:=$(BBASE)/std.$$(/usr/bin/basename $$PWD) +updatereadme: + updatereadme -bitbucket: - ssh ruamel@localhost "cd $$PWD; hg push $(BB)" +layout: pdf + cp README.pdf /data0/tmp/pdf diff --git a/README.rst b/README.rst index 393ee03..ee25009 100644 --- a/README.rst +++ b/README.rst @@ -1,2 +1,306 @@ argparse extensions =================== + +This package provides extensions to argparse on two levels: + +- basic argparse extensions: default subparser, subparser aliases in + 2.X +- additional actions that can be specified for add_argument +- smart formatter that allows combination of defaults help formatting + **and** raw desciptions +- wrapper for argparse using decorators + +Extensions to basic argparse +---------------------------- + +Insert the following to be able to specify `aliases +`_ in +subparser definitions in 2.6 and 2.7:: + + from __future__ import print_function + + import sys + from ruamel.std.argparse import ArgumentParser, SubParsersAction + + parser = ArgumentParser() + if sys.version_info < (3,): # add aliases support + parser.register('action', 'parsers', SubParsersAction) + subparsers = parser.add_subparsers() + checkout = subparsers.add_parser('checkout', aliases=['co']) + checkout.add_argument('foo') + args = parser.parse_args(['co', 'bar']) + print(args) + +.. example code aliases.py + +Resulting in:: + + Namespace(foo='bar') + + +.. example output aliases.py + +Additional actions +------------------ + +CountAction ++++++++++++ + +Count up and down:: + + from __future__ import print_function + + from ruamel.std.argparse import CountAction + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('--verbose', '-v', action=CountAction, const=1, nargs=0) + parser.add_argument('--quiet', '-q', action=CountAction, dest='verbose', + const=-1, nargs=0) + + print(parser.parse_args("--verbose -v -q".split())) + +.. example code countaction.py + +results in:: + + Namespace(verbose=1) + + +.. example output countaction.py + + +SplitAppend ++++++++++++ + +Append after splitting on "``,``". Running:: + + from __future__ import print_function + + from ruamel.std.argparse import SplitAppendAction + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('-d', action=SplitAppendAction) + + print(parser.parse_args("-d ab -d cd -d kl -d mn".split())) + print(parser.parse_args("-d ab,cd,kl,mn".split())) + print(parser.parse_args("-d ab,cd -d kl,mn".split())) + +.. example code splitaction.py + +results in:: + + Namespace(d=['ab', 'cd', 'kl', 'mn']) + Namespace(d=['ab', 'cd', 'kl', 'mn']) + Namespace(d=['ab', 'cd', 'kl', 'mn']) + + +.. example output splitaction.py + +CheckSingleStoreAction +++++++++++++++++++++++ + +Complain if the same option is called multiple times:: + + from __future__ import print_function + + from ruamel.std.argparse import CheckSingleStoreAction + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('--check', '-c', action=CheckSingleStoreAction, const=1, nargs=0) + + print(parser.parse_args("--check -c".split())) + +.. example code checksingleaction.py + +results in:: + + WARNING: previous optional argument "-c []" overwritten by "-c []" + Namespace(check=[]) + + +.. example output checksingleaction.py + +Smart formatting +---------------- + +You can only specify one formatter in standard argparse, so you cannot +both have pre-formatted description. using +RawDescriptionHelpFormatter,as well as default arguments with +ArgumentDefaultsHelpFormatter. + +The ``SmartFormatter`` is a subclass of ``argparse.HelpFormatter`` and +has the normal formatter as default. Help text can be marked at the +beginning for variations in formatting: + +- ``R|..`` format raw, i.e. don't wrap and fill out, observer newline +- ``*|..`` format a password help, never echo password defaults +- ``D|..`` add defaults to **all** entries (that is why having ``*|`` + is important) + +The version string is formatted using _split_lines and preserves any +line breaks in the version string. + +:: + + from __future__ import print_function + + from ruamel.std.argparse import SmartFormatter + import argparse + + def exit(self, *args, **kw): + pass + + argparse.ArgumentParser.exit = exit + + # the 'D|....' in the second pass triggers generating defaults for all entries, + # while being smart about which one already have a %(default)s + + for index, log_s in enumerate(['log to file', 'D|log to file']): + parser = argparse.ArgumentParser(formatter_class=SmartFormatter) + + parser.add_argument('--log', default='abc.log', help=log_s) + parser.add_argument('--username', + help='username to login with (default: %(default)s)') + parser.add_argument('--password', help='*|password to use for login') + parser.add_argument('--recursive', '-r', action='store_true', + help="R|recurse into subdirectories \nto find files") + parser.set_defaults(username='anthon', password="test123") + + if index > 0: + print('--------------------------------------\n') + parser.parse_args(["--help"]) + + +.. example code smartformatter.py + +results in:: + + usage: smartformatter.py [-h] [--log LOG] [--username USERNAME] + [--password PASSWORD] [--recursive] + + optional arguments: + -h, --help show this help message and exit + --log LOG log to file + --username USERNAME username to login with (default: anthon) + --password PASSWORD password to use for login + --recursive, -r recurse into subdirectories + to find files + -------------------------------------- + + usage: smartformatter.py [-h] [--log LOG] [--username USERNAME] + [--password PASSWORD] [--recursive] + + optional arguments: + -h, --help show this help message and exit + --log LOG log to file (default: abc.log) + --username USERNAME username to login with (default: anthon) + --password PASSWORD password to use for login (default: *******) + --recursive, -r recurse into subdirectories + to find files (default: False) + + +.. example output smartformatter.py + + +Wrapping argparse +----------------- + +When using argparse with subparser, each of which have their own +function ( using ``.set_defaults(func=function``) that can be called, +there is a lot of repetitive code. + +An alternative is provided by the ``ProgramBase`` class that should be +subclassed and the ``sub_parser``, ``option`` and ``version`` +decorators that can be applied to methods of that subclass. + +A typical use case is:: + + from __future__ import print_function + + import sys + import os + + from ruamel.std.argparse import ProgramBase, option, sub_parser, version, \ + SmartFormatter + + class TestCmd(ProgramBase): + def __init__(self): + super(TestCmd, self).__init__( + formatter_class=SmartFormatter + ) + + # you can put these on __init__, but subclassing TestCmd + # will cause that to break + @option('--quiet', '-q', help='suppress verbosity', action='store_true', + global_option=True) + @version('version: 1.2.3') + def _pb_init(self): + # special name for which attribs are included in help + pass + + def run(self): + if self._args.func: + return self._args.func() + + def parse_args(self, *args): + self._parse_args(*args) + + @sub_parser(help='specific help for readit') + @option('--name', default='abc') + def readit(self): + print('calling readit') + + @sub_parser('writeit', help='help for writeit') + @option('--target') + def other_name(self): + print('calling writeit') + + + n = TestCmd() + n.parse_args(['--help']) + n.run() + +.. example code testcmd.py + +and output:: + + usage: testcmd.py [-h] [--quiet] [--version] {readit,writeit} ... + + positional arguments: + {readit,writeit} + readit specific help for readit + writeit help for writeit + + optional arguments: + -h, --help show this help message and exit + --quiet, -q suppress verbosity + --version show program's version number and exit + + +.. example output testcmd.py + + + +The method name is by default the name of the sub_parser. This can be +overriden by providing a non-keyword argument to ``sub_parser``. The +keyword arguments are passed to the ``add_parser`` method. + +The ``option`` functions as ``add_argument``. If ``option`` is put on +a method that is not a sub_parser, such an option will be a global +option. These have to be specified before any sub_parser argument when +invoking the script. Often it is handy to specify such an option with +an ``global_option=True`` keyword argument. This makes sure that +option is added to all the sub_parsers as well. This allows you to +invoke both ``prog --quiet writeit`` and ``prog writeit --quiet``). +You can assing these options to ``__init__``, but when sub classing +``TestCmd`` this will lead to problems. It is therefore better to pu +them on the special handled method ``_pb_init`` if subclassing might +happen. + +Care should be taken that all attributes on ``TestCmd`` are accessed +during scanning for sub parsers. In particular any ``@property`` +decorated method will be accessed. + diff --git a/__init__.py b/__init__.py index 4d551d3..da35031 100644 --- a/__init__.py +++ b/__init__.py @@ -32,7 +32,7 @@ def _convert_version(tup): # < -version_info = (0, 2, "alpha", 2) +version_info = (0, 3, 1) __version__ = _convert_version(version_info) del _convert_version diff --git a/_action/checksinglestore.py b/_action/checksinglestore.py index 27d5cc8..11fd80c 100644 --- a/_action/checksinglestore.py +++ b/_action/checksinglestore.py @@ -11,6 +11,8 @@ class CheckSingleStoreAction(argparse.Action): if getattr(namespace, self.dest, None) is not None: print( 'WARNING: previous optional argument "' + option_string + " " + - getattr(namespace, self.dest) + '" overwritten by "' + - option_string + " " + values + '"') + str(getattr(namespace, self.dest)) + '" overwritten by "' + + str(option_string) + + " " + str(values) + + '"') setattr(namespace, self.dest, values) diff --git a/example/aliases.py b/example/aliases.py new file mode 100644 index 0000000..f2d237e --- /dev/null +++ b/example/aliases.py @@ -0,0 +1,13 @@ +from __future__ import print_function + +import sys +from ruamel.std.argparse import ArgumentParser, SubParsersAction + +parser = ArgumentParser() +if sys.version_info < (3,): # add aliases support + parser.register('action', 'parsers', SubParsersAction) +subparsers = parser.add_subparsers() +checkout = subparsers.add_parser('checkout', aliases=['co']) +checkout.add_argument('foo') +args = parser.parse_args(['co', 'bar']) +print(args) diff --git a/example/checksingleaction.py b/example/checksingleaction.py new file mode 100644 index 0000000..cad05ea --- /dev/null +++ b/example/checksingleaction.py @@ -0,0 +1,9 @@ +from __future__ import print_function + +from ruamel.std.argparse import CheckSingleStoreAction +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--check', '-c', action=CheckSingleStoreAction, const=1, nargs=0) + +print(parser.parse_args("--check -c".split())) \ No newline at end of file diff --git a/example/countaction.py b/example/countaction.py new file mode 100644 index 0000000..b9ee104 --- /dev/null +++ b/example/countaction.py @@ -0,0 +1,11 @@ +from __future__ import print_function + +from ruamel.std.argparse import CountAction +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--verbose', '-v', action=CountAction, const=1, nargs=0) +parser.add_argument('--quiet', '-q', action=CountAction, dest='verbose', + const=-1, nargs=0) + +print(parser.parse_args("--verbose -v -q".split())) diff --git a/example/smartformatter.py b/example/smartformatter.py new file mode 100644 index 0000000..cb19f09 --- /dev/null +++ b/example/smartformatter.py @@ -0,0 +1,28 @@ +from __future__ import print_function + +from ruamel.std.argparse import SmartFormatter +import argparse + +def exit(self, *args, **kw): + pass + +argparse.ArgumentParser.exit = exit + +# the 'D|....' in the second pass triggers generating defaults for all entries, +# while being smart about which one already have a %(default)s + +for index, log_s in enumerate(['log to file', 'D|log to file']): + parser = argparse.ArgumentParser(formatter_class=SmartFormatter) + + parser.add_argument('--log', default='abc.log', help=log_s) + parser.add_argument('--username', + help='username to login with (default: %(default)s)') + parser.add_argument('--password', help='*|password to use for login') + parser.add_argument('--recursive', '-r', action='store_true', + help="R|recurse into subdirectories \nto find files") + parser.set_defaults(username='anthon', password="test123") + + if index > 0: + print('--------------------------------------\n') + parser.parse_args(["--help"]) + diff --git a/example/splitaction.py b/example/splitaction.py new file mode 100644 index 0000000..11198bd --- /dev/null +++ b/example/splitaction.py @@ -0,0 +1,11 @@ +from __future__ import print_function + +from ruamel.std.argparse import SplitAppendAction +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('-d', action=SplitAppendAction) + +print(parser.parse_args("-d ab -d cd -d kl -d mn".split())) +print(parser.parse_args("-d ab,cd,kl,mn".split())) +print(parser.parse_args("-d ab,cd -d kl,mn".split())) diff --git a/example/testcmd.py b/example/testcmd.py new file mode 100644 index 0000000..a319306 --- /dev/null +++ b/example/testcmd.py @@ -0,0 +1,44 @@ +from __future__ import print_function + +import sys +import os + +from ruamel.std.argparse import ProgramBase, option, sub_parser, version, \ + SmartFormatter + +class TestCmd(ProgramBase): + def __init__(self): + super(TestCmd, self).__init__( + formatter_class=SmartFormatter + ) + + # you can put these on __init__, but subclassing TestCmd + # will cause that to break + @option('--quiet', '-q', help='suppress verbosity', action='store_true', + global_option=True) + @version('version: 1.2.3') + def _pb_init(self): + # special name for which attribs are included in help + pass + + def run(self): + if self._args.func: + return self._args.func() + + def parse_args(self, *args): + self._parse_args(*args) + + @sub_parser(help='specific help for readit') + @option('--name', default='abc') + def readit(self): + print('calling readit') + + @sub_parser('writeit', help='help for writeit') + @option('--target') + def other_name(self): + print('calling writeit') + + +n = TestCmd() +n.parse_args(['--help']) +n.run() diff --git a/test/test_argparse.py b/test/test_argparse.py index 926b685..d50b46b 100644 --- a/test/test_argparse.py +++ b/test/test_argparse.py @@ -1,3 +1,4 @@ +# coding: utf-8 import pytest @@ -7,7 +8,6 @@ from textwrap import dedent def exit(self=None, status=None, message=None): pass -# default for tox stub is to Fail def test_argparse(capsys): desc = dedent("""\ Please do not mess up this text! -- cgit v1.2.1