Package: lazycli
lazycli is a module which provides a decorator which will generate cli
scripts from function signatures. The intention is to allow the creation
of cli-scripts with as little extra work as possible. It was originally
going to be calledsig2cli, but someone else alreadyhad the same
ideaand got the name on PyPI in ten months before I did.The one and only goal of lazycli is to facilitate the creation of CLI
interfaces withminimum effort.lazycli wrapsargparsefrom the Python standard library and exposes
some parts of theargparse api. The abstraction it provides is a
little leaky, but it’s not too bad, because it’s relatively simple and
is not intended to provide the full range functionality. If you need
flexibility, useargparsedirectly or something more powerful likeclick.ContentsBasicsTypesOutputSubcommandsBasicsConsider this simple clone of thecpcommand,cp.py:#!/usr/bin/env python3importlazycliimportshutilimportsys@lazycli.scriptdefcp(*src,dst,recursive=False):"""copy around files"""forpathinsrc:try:shutil.copy2(path,dst)exceptIsADirectoryErroraserr:ifrecursive:shutil.copytree(path,dst)else:print(err,file=sys.stderr)if__name__=='__main__':cp.run()$./cp.py-husage:cp.py[-h][-r][src[src...]]dstcopyaroundfilespositionalarguments:srcdstoptionalarguments:-h,--helpshowthishelpmessageandexit-r,--recursiveIt works like you’d expect. I chosecpbecauseshutilcan do all
the heavy lifting, and the body of the function isn’t important. The
important thing in this script are these three lines:@lazycli.scriptdefcp(*src,dst,recursive=False):# ... and ...cp.run()All parameters without defaults become positional arguments.All parameters with defaults become optional arguments.*argsarguments will translate into variadic arguments at the
command line as well.There can always be zero of them.Parameters with boolean default values are treated as boolean flags
and don’t accept arguments.Short versions of flags are generated automatically from the first
letter of the parameter.A.runfunction is tacked on to thecpfunction which
triggers argument parsing and applies the results tocp. Thecpfunction itself is unaltered and can be called elsewhere if
desired.I’m not entirely sure how useful this last point is, since script
entry-point functions tend not to be very general-purpose, but, eh, who
knows.Be aware that, presently,**kwargs-style parameters are ignored
by lazycli.Note on short flags:Short flags are generated for optional arguments based on the first
letter of parameter names. If that flag has been used by a previous
parameter, the flag will be uppercased. If that has already been used,
no short flag is generated. Because of this, changing the order of
arguments can potentially break the backward compatibility of your
CLI.Note on boolean defaults:A boolean default set toFalseproduces the output seen above. If
we change the parameter default torecursive=True, the name of the
flag is inverted:optional arguments:
  -h, --help          show this help message and exit
  -r, --no-recursiveTypeslazycli attempts to determine argument types based first on type
annotations in the function signature and then based on the type of the
default argument.If the type of parameter is an iterable (besides mappings, strings and
files), it will become a variadic when interpreted. If it’s a
subscripted type from thetypingmodule, liketyping.Iterable[int], the subscript will be used as the type.If the type is determined to be a mapping or is annotated asobject, the argument should be a json literal (though it could
theoretically be a string, number, array or object).The inferred type is then used as a constructor to parse the argument
string. This means only constructors that can take strings as input may
be used.#!/usr/bin/env python3importtypingastimportlazycli@lazycli.scriptdefmysum(numbers:t.Iterable[float]):returnsum(numbers)if__name__=='__main__':mysum.run()$./sum.py-husage:sum.py[-h][numbers[numbers...]]positionalarguments:numberstype:floatoptionalarguments:-h,--helpshowthishelpmessageandexit$./sum.py5813.0Though the style is questionable, this means you can use arbitrary
callables as type annotations:#!/usr/bin/env python3importsysimportlazycli@lazycli.scriptdefupcat(infile:open=sys.stdin,outfile:lambdaf:open(f,'w')=sys.stdout):"""cat, but upper-cases everything."""forlineininfile:outfile.write(line.upper())if__name__=='__main__':upcat.run()usage:upcat.py[-h][-iINFILE][-oOUTFILE]cat,butupper-caseseverything.optionalarguments:-h,--helpshowthishelpmessageandexit-iINFILE,--infileINFILEtype:open;default:<stdin>-oOUTFILE,--outfileOUTFILEtype:<lambda>;default:<stdout>This looks pretty bad, andmypyis going to hate it. A better way to
do this is probably just parsing the string inside the script.However, because the pattern of having an optional file argument and
falling back to standard streams is so common,lazycliprovides
special classes for making this less ugly:#!/usr/bin/env python3importsysfromlazycliimportscript,ReadFile,WriteFile@scriptdefupcat2(infile:ReadFile=sys.stdin,outfile:WriteFile=sys.stdout):"""cat, but upper-cases everything."""forlineininfile:outfile.write(line.upper())if__name__=='__main__':upcat2.run()These classes will provide users more helpful type information and will
return true if used in instance checks of text file types (includingsys.{stdin,stdout,stderr}and non-bytes output of theopenbuiltin function). These classes don’t create instances of themselves,
but rather instances ofio.TextIOWrapper. However, they still break
mypy. Funny how metaclasses will do that.In addition to ReadFile and WriteFile, there is also an AppendFile
constructor.OutputSo far, output has simply been printed. However, If the function has a
return value, that will also be printed. If it is an iterable (besides a
string or mapping), each item will be printed on a new line.SubcommandsI’ll expand this section of the documentation later, but here’s a sample
script, modeled on info in thisblog post#!/usr/bin/env python3importlazycli@lazycli.scriptdefmain(version=False):ifversion:return1.0@main.subcommanddefhello(name,greeting="Hello",caps=False):returngreet(name,greeting,caps)@main.subcommanddefgoodbye(name,greeting="Goodbye",caps=False):returngreet(name,greeting,caps)defgreet(name,greeting,caps):ifcaps:returnf'{greeting},{name}!'.upper()returnf'{greeting},{name}!'if__name__=='__main__':script.run()Any parameters definied on the script function (main, in this case)
will be passed to the script function, and parameters definied on the
subcommands are parsed to them. If one wishes to pass values from the
top-level function to subcommands, and external mechanism must be used.
(e.g. a global variable)$./test_sub.py-husage:test_sub.py[-h][-v]{hello,goodbye}...positionalarguments:{hello,goodbye}optionalarguments:-h,--helpshowthishelpmessageandexit-v,--version$$$./test_sub.pyhello-husage:test_sub.pyhello[-h][-c][-gGREETING]namepositionalarguments:nameoptionalarguments:-h,--helpshowthishelpmessageandexit-c,--caps-gGREETING,--greetingGREETINGdefault:Hello