Package: autocommand
autocommandA library to automatically generate and run simple argparse parsers from function signatures.InstallationAutocommand is installed via pip:$ pip install autocommandUsageAutocommand turns a function into a command-line program. It converts the function's parameter signature into command-line arguments, and automatically runs the function if the module was called as__main__. In effect, it lets your create a smart main function.fromautocommandimportautocommand# This program takes exactly one argument and echos it.@autocommand(__name__)defecho(thing):print(thing)$ python echo.py hello
hello
$ python echo.py -h
usage: echo [-h] thing

positional arguments:
  thing

optional arguments:
  -h, --help  show this help message and exit
$ python echo.py hello world  # too many arguments
usage: echo.py [-h] thing
echo.py: error: unrecognized arguments: worldAs you can see, autocommand converts the signature of the function into an argument spec. When you run the file as a program, autocommand collects the command-line arguments and turns them into function arguments. The function is executed with these arguments, and then the program exits with the return value of the function, viasys.exit. Autocommand also automatically creates a usage message, which can be invoked with-hor--help, and automatically prints an error message when provided with invalid arguments.TypesYou can use a type annotation to give an argument a type. Any type (or in fact any callable) that returns an object when given a string argument can be used, though there are a few special cases that are described later.@autocommand(__name__)defnet_client(host,port:int):...Autocommand will catchTypeErrorsraised by the type during argument parsing, so you can supply a callable and do some basic argument validation as well.Trailing ArgumentsYou can add a*argsparameter to your function to give it trailing arguments. The command will collect 0 or more trailing arguments and supply them toargsas a tuple. If a type annotation is supplied, the type is applied to each argument.# Write the contents of each file, one by one@autocommand(__name__)defcat(*files):forfilenameinfiles:withopen(filename)asfile:forlineinfile:print(line.rstrip())$ python cat.py -h
usage: ipython [-h] [file [file ...]]

positional arguments:
  file

optional arguments:
  -h, --help  show this help message and exitOptionsTo create--optionswitches, just assign a default. Autocommand will automatically create--longand-short switches.@autocommand(__name__)defdo_with_config(argument,config='~/foo.conf'):pass$ python example.py -h
usage: example.py [-h] [-c CONFIG] argument

positional arguments:
  argument

optional arguments:
  -h, --help            show this help message and exit
  -c CONFIG, --config CONFIGThe option's type is automatically deduced from the default, unless one is explicitly given in an annotation:@autocommand(__name__)defhttp_connect(host,port=80):print('{}:{}'.format(host,port))$ python http.py -h
usage: http.py [-h] [-p PORT] host

positional arguments:
  host

optional arguments:
  -h, --help            show this help message and exit
  -p PORT, --port PORT
$ python http.py localhost
localhost:80
$ python http.py localhost -p 8080
localhost:8080
$ python http.py localhost -p blah
usage: http.py [-h] [-p PORT] host
http.py: error: argument -p/--port: invalid int value: 'blah'NoneIf an option is given a default value ofNone, it reads in a value as normal, but suppliesNoneif the option isn't provided.SwitchesIf an argument is given a default value ofTrueorFalse, or
given an explicitbooltype, it becomes an option switch.@autocommand(__name__)defexample(verbose=False,quiet=False):pass$ python example.py -h
usage: example.py [-h] [-v] [-q]

optional arguments:
  -h, --help     show this help message and exit
  -v, --verbose
  -q, --quietAutocommand attempts to do the "correct thing" in these cases- if the default isTrue, then supplying the switch makes the argumentFalse; if the type isbooland the default is some otherTruevalue, then supplying the switch makes the argumentFalse, while not supplying the switch makes the argument the default value.Autocommand also supports the creation of switch inverters. Passadd_nos=Truetoautocommandto enable this.@autocommand(__name__, add_nos=True)
    def example(verbose=False):
        pass$ python example.py -h
usage: ipython [-h] [-v] [--no-verbose]

optional arguments:
  -h, --help     show this help message and exit
  -v, --verbose
  --no-verboseUsing the--no-version of a switch will pass the opposite value in as a function argument. If multiple switches are present, the last one takes precedence.FilesIf the default value is a file object, such assys.stdout, then autocommand just looks for a string, for a file path. It doesn't do any special checking on the string, though (such as checking if the file exists); it's better to let the client decide how to handle errors in this case. Instead, it provides a special context manager calledsmart_open, which behaves exactly likeopenif a filename or other openable type is provided, but also lets you use already open files:fromautocommandimportautocommand,smart_openimportsys# Write the contents of stdin, or a file, to stdout@autocommand(__name__)defwrite_out(infile=sys.stdin):withsmart_open(infile)asf:forlineinf:print(line.rstrip())# If a file was opened, it is closed here. If it was just stdin, it is untouched.$ echo "Hello World!" | python write_out.py | tee hello.txt
Hello World!
$ python write_out.py --infile hello.txt
Hello World!Descriptions and docstringsTheautocommanddecorator acceptsdescriptionandepilogkwargs, corresponding to thedescription <https://docs.python.org/3/library/argparse.html#description>_ andepilog <https://docs.python.org/3/library/argparse.html#epilog>_ of theArgumentParser. If no description is given, but the decorated function has a docstring, then it is taken as thedescriptionfor theArgumentParser. You can also provide both the description and epilog in the docstring by splitting it into two sections with 4 or more - characters.@autocommand(__name__)defcopy(infile=sys.stdin,outfile=sys.stdout):'''Copy an the contents of a file (or stdin) to another file (or stdout)----------Some extra documentation in the epilog'''withsmart_open(infile)asistr:withsmart_open(outfile,'w')asostr:forlineinistr:ostr.write(line)$ python copy.py -h
usage: copy.py [-h] [-i INFILE] [-o OUTFILE]

Copy an the contents of a file (or stdin) to another file (or stdout)

optional arguments:
  -h, --help            show this help message and exit
  -i INFILE, --infile INFILE
  -o OUTFILE, --outfile OUTFILE

Some extra documentation in the epilog
$ echo "Hello World" | python copy.py --outfile hello.txt
$ python copy.py --infile hello.txt --outfile hello2.txt
$ python copy.py --infile hello2.txt
Hello WorldParameter descriptionsYou can also attach description text to individual parameters in the annotation. To attach both a type and a description, supply them both in any order in a tuple@autocommand(__name__)defcopy_net(infile:'The name of the file to send',host:'The host to send the file to',port:(int,'The port to connect to')):'''Copy a file over raw TCP to a remote destination.'''# Left as an exercise to the readerDecorators and wrappersAutocommand automatically follows wrapper chains created by@functools.wraps. This means that you can apply other wrapping decorators to your main function, and autocommand will still correctly detect the signature.fromfunctoolsimportwrapsfromautocommandimportautocommanddefprint_yielded(func):'''Convert a generator into a function that prints all yielded elements'''@wraps(func)defwrapper(*args,**kwargs):forthinginfunc(*args,**kwargs):print(thing)returnwrapper@autocommand(__name__,description='Print all the values from START to STOP, inclusive, in steps of STEP',epilog='STOP and STEP default to 1')@print_yieldeddefseq(stop,start=1,step=1):foriinrange(start,stop+1,step):yieldi$ seq.py -h
usage: seq.py [-h] [-s START] [-S STEP] stop

Print all the values from START to STOP, inclusive, in steps of STEP

positional arguments:
  stop

optional arguments:
  -h, --help            show this help message and exit
  -s START, --start START
  -S STEP, --step STEP

STOP and STEP default to 1Even though autocommand is being applied to thewrapperreturned byprint_yielded, it still retreives the signature of the underlyingseqfunction to create the argument parsing.Custom ParserWhile autocommand's automatic parser generator is a powerful convenience, it doesn't cover all of the different features that argparse provides. If you need these features, you can provide your own parser as a kwarg toautocommand:fromargparseimportArgumentParserfromautocommandimportautocommandparser=ArgumentParser()# autocommand can't do optional positonal parametersparser.add_argument('arg',nargs='?')# or mutually exclusive optionsgroup=parser.add_mutually_exclusive_group()group.add_argument('-v','--verbose',action='store_true')group.add_argument('-q','--quiet',action='store_true')@autocommand(__name__,parser=parser)defmain(arg,verbose,quiet):print(arg,verbose,quiet)$ python parser.py -h
usage: write_file.py [-h] [-v | -q] [arg]

positional arguments:
  arg

optional arguments:
  -h, --help     show this help message and exit
  -v, --verbose
  -q, --quiet
$ python parser.py
None False False
$ python parser.py hello
hello False False
$ python parser.py -v
None True False
$ python parser.py -q
None False True
$ python parser.py -vq
usage: parser.py [-h] [-v | -q] [arg]
parser.py: error: argument -q/--quiet: not allowed with argument -v/--verboseAny parser should work fine, so long as each of the parser's arguments has a corresponding parameter in the decorated main function. The order of parameters doesn't matter, as long as they are all present. Note that when using a custom parser, autocommand doesn't modify the parser or the retrieved arguments. This means that no description/epilog will be added, and the function's type annotations and defaults (if present) will be ignored.Testing and Library useThe decorated function is only called and exited from if the first argument toautocommandis'__main__'orTrue. If it is neither of these values, or no argument is given, then a new main function is created by the decorator. This function has the signaturemain(argv=None), and is intended to be called with arguments as if viamain(sys.argv[1:]). The function has the attributesparserandmain, which are the generatedArgumentParserand the original main function that was decorated. This is to facilitate testing and library use of your main. Calling the function triggers aparse_args()with the supplied arguments, and returns the result of the main function. Note that, while it returns instead of callingsys.exit, theparse_args()function will raise aSystemExitin the event of a parsing error or-h/--helpargument.@autocommand()deftest_prog(arg1,arg2:int,quiet=False,verbose=False):ifnotquiet:print(arg1,arg2)ifverbose:print("LOUD NOISES")return0print(test_prog(['-v','hello','80']))$ python test_prog.py
hello 80
LOUD NOISES
0If the function is called with no arguments,sys.argv[1:]is used. This is to allow the autocommand function to be used as a setuptools entry point.Exceptions and limitationsThere are a few possible exceptions thatautocommandcan raise. All of them derive fromautocommand.AutocommandError.If an invalid annotation is given (that is, it isn't atype,str,(type, str), or(str, type), anAnnotationErroris raised. Thetypemay be any callable, as described in theTypes_ section.If the function has a**kwargsparameter, aKWargErroris raised.If, somehow, the function has a positional-only parameter, aPositionalArgErroris raised. This means that the argument doesn't have a name, which is currently not possible with a plaindeforlambda, though many built-in functions have this kind of parameter.There are a few argparse features that are not supported by autocommand.It isn't possible to have an optional positional argument (as opposed to a--option). POSIX thinks this is bad form anyway.It isn't possible to have mutually exclusive arguments or optionsIt isn't possible to have subcommands or subparsers, though I'm working on a few solutions involving classes or nested function definitions to allow this.DevelopmentAutocommand cannot be important from the project root; this is to enforce separation of concerns and prevent accidental importing ofsetup.pyor tests. To develop, install the project in editable mode:$ python setup.py developThis will create a link to the source files in the deployment directory, so that any source changes are reflected when it is imported.