Package: yaclipy
yaclipyYet another python command-line interface that has a consistent way to call any kind of function/method from the command line.FeaturesSub commands are known deterministically.  They are parsed before any commands are executed.Easy-to-read documentation automatically shown from the docstring with=hor–help.The function’s annotations and default values are used to coerce the command line arguments to the correct type.A consistent way to call all kinds of function signatures (using inspect.signature to the fullest extent).Ability to accept multiple list-typed parameters.*args and **kwargs have useful abilities.Getting StartedWhile not the recommended way of using yaclipy, this is the simplest yet meaningful example.Given the following file namedcli.py:#!/usr/bin/env pythonimportsysfromprint_extimportprint,PrettyExceptionimportyaclipyasCLIdefmain(say,times__t=1)->str:''' Say something multiple times

    Parameters:
        <message>, --say <message>
            What you want to say
        <int>, --times <int>, -t <int>
            How many times you want to say it.
    '''return' '.join([say]*times__t)if__name__=='__main__':try:CLI.Command(main)(sys.argv[1:]).run()exceptPrettyExceptionase:print.pretty(e)Given that the file is executablechmod +x cli.py, you can use it as follows.$./cli.py-h<doc string>Using-hor--helpwill show the docstring documentation, along with a list of possible sub-commands.
Sub-commands are determined explicitly with thesub_cmdsdecorator, or by the return type annotation.$./cli.py-t3--sayHoHo Ho HoYou may use a single dash for single character names, otherwise use double-dashes.
Names defined with double underscores separate the aliases that can be used on the command line (--timesor-t).Thesayparameter has no type information so it will be a string.
Thetimes__tparameter’s default value is an int, so you can only pass integers.-thiwill fail with an error.$./cli.py"Hello World"2Hello World Hello WorldSince the parameters are defined as keywordorpositional, you can pass them positionally.$./cli.pygo--times3upperGO GO GOWhen positional and keyword parameters are used simultaneously, positional arguments always come first.upperis the start of a new command.  In this case it is executed on the return valuestrof the previous command.If the value returned from a command is notNonethen the value is pretty printed.$./cli.py\\--times--timesKeyword arguments are identified with dashes.
If you want to use a value that starts with a dash then it must be escaped with a backslash.
The shell eats one backslash if you don’t surround the argument in quotes.Only the leading backslash is removed.  If you specify only a backslash./cli.py \\then an empty string will be consumed as the first argument.Negative numbers such as-.3,-0.5e33don’t need to be escaped.The following examples introduce more complicated examples.
They just show the function declaration for brevity.Positional vs. Keyworddeffoo(a=3,/,banana__b='hi',*,carrot__c:int=None):''' Foo

    Parameters:
        <int>
            Positional only
        <str>, --banana <str>, -b <str>
            Positional or keyword
        --carrot <int>, -c <int>
            Keyword only
    '''# foo 4 bye --carrot 42# foo 4 -c 42 -b byea==4banana__b=='bye'carrot__c==42The distinction between position-only, positional or keyword and keyword-only parameters is important.
Parameters before the/cannot be specified by name.  Parameters after the*mustbe given by name.
Other parameters may be given either way.Notice how the docstring documentation indicates the positionally.Flagsdeffoo(*,verbose__v=False,times__t:int):''' Flags example

    Parameters:
        --verbose, -v
            More verbose
        --times <int>, -t <int>
            How many times
    '''# foo -vt 3 --verbose# foo -vv --times 3verbose__v==2times__t==3Flags are specified by a default value ofFalse.
You can’t useboolas a type in any other way such asx:boolory:[bool].Flags can be specified multiple times in which case its value won’t beTrue, but an integer specifying how many times it was given.
Sinceint(True) == 1you can useint(verbose__v)to get the number of times it was specified.Since flag arguments don’t take value, single letter flags can be combined together in the usual way.
The last letter of the group may be a non-flag type that consumes the succeeding value.Special Namesdeffoo(*,if_=1,happy_days=2,lots__of__aliases__t__q=3,_hidden=4):# foo --if 10 --happy-days 20 --happy_days 200 --lots 30 --of 40 --aliases 50 -t 60 -q 70if_==10happy_days==200lots__of__aliases__t__q==70_hidden===4This shows the various naming schemes that exist.A trailing underscore is ignored and used to alias keywords.Single underscores may be given as dashes insteadDouble dashes separate aliases.  There can be multiple.Leading underscores indicate private variables that cannot be set from the command line.
They must have a default value or be set from the previous call in the call chain (described below).Sub-CommandsimportyaclipyasCLIdeffoo(*,name,_value):passdefbar(*,name,_value):pass@CLI.sub_cmds(foo,baz=bar)defroot(*,verbose__v=False):returndict(name='jim',_value='hi'*int(verbose__v))# root -v foo -h# root -v baz --name bobCommands can be chained together.
The sub-commands available are known deterministically, either explicitly with thesub_cmdsdecorator, or implicitly from the return type annotation.The complete chain of commands is fully parsed before any commands are actually executed.
By making the sub-command lookup deterministic we can provide better help and documentation support.
Also, any command-line syntax errors in sub-commands are caught before anything is executed.The return value of the previous command is passed to the next command.
If the return value is a dictionary then its values will be initially applied to the function’s keyword parameters.The values override the parameter’s default value, but a matching command line argument has highest priority.
In the second example above, the name argumentboboverwritesjimthat was provided in the return value.If the function defines a special_inputparameter then the return value of the parent will be applied to it directly.Generatorsdefshow(*,_input):x,xxx=_inputprint(f'3^{x}=={xxx}')@CLI.sub_cmds(show)deffoo(*,times__t=3):foriinrange(times__t):yieldi,pow(3,i)# foo -t 4 showIf a generator is used then it can yield a value to the sub-command and then continue with cleanup-code after the sub-command completes.By returning or yielding a dictionary you can set keyword parameters of the sub-command.
If the function defines a special_inputparameter then the return value of the parent will be applied to it directly.Listsdeffoo(a:int,b:[float],c=[]):# foo 3 1.1 -.1 1e3 - 66 \\-apples# foo -c 66 -c \\-apples -b#3 1.1 -0.1 1e3 -a 3# 3 1.1 - -c# 66 \\-apples - -b#2 -.1 1e3a==3b==[1.1,-0.1,1e3]c==['66','-apples']In this example type annotations are used for the first two parameters.
Since the inside of the third list is unknown,stris assumed.The two examples above are equivalent ways of setting the parameters.There are three ways to set lists.For positional parameter lists, values are taken until a value that starts with a dash is encountered.
A single dash-may be used to to indicate that we are done with this positional parameter.
To include a value that starts with a dash (such as a single dash) the leading dash needs to be escaped\\-.
Negative numbers don’t need to be escaped.For keyword parameters you can use repeated application of the argument-c66-c\\-apples.
If the argument’s value starts with a dash then it needs to be escaped or it will be treated as the next keyword argument.For keyword parameters you can use the--arg#Nsyntax to specify that the followingNvalues are in the list.  If you don’t specify N,--arg#, then values are taken just like a positional parameter until a single dash, or another keyword argument, is encountered.The three ways can be mixed and matched, but positional arguments must always precede keyword arguments.JSONA parameter of typedictis parsed as json.  It may, or may not parse to a dict.*argsThelistssection above discussed how to get lists of values.
But that way has a couple of limitations.
Keyword arguments must follow the position arguments which is unnatural for commands that deal with file globs.
Also, values starting with a dash must be escaped.By specifying*argsyou can get around these limitations because it just captures all un-processed trailing arguments.
This comes with its own limitations.  Obviously, it can’t have any sub-commands.deffoo(first=None,*files,verbose__v=False):# foo *# foo - *# foo - - *# foo -- *In the first example, the first file name is captured byfirstand the remaining files would go tofiles.
In the second example,firstis skipped so all files go tofiles.Both the first and second examples have a tricky corner-case.
If you have a file named-v(Why!?)then it would try to set the verbose flag and (hopefully) generate an error.By explicitly ending the positional and keyword sections with-you can safely capture all of the files.  The two separate dashes in the third example can be combined together for aesthetics.
If you know that there are no crazy files starting with a dash then the first two ways are fine.**kwargsdeffoo(a=False,**kwargs)->str:# foo -axd 33 -d 44 --apple x --banana - uppera==Truekwargs=={'x':True,'d':['33','44'],'apple':'x','banana':True}returnstr(kwargs)The rules for capturing arbitrary key-values are as follows.If it must be a flag, either because it is at the end or in the middle of a flag group, then assume the type is a flag.Otherwise, assume astrif the argument appears once, otherwise[str]A single dash can be used to stop taking keyword arguments and go to the next command.ConfigOften programs need configuration values that can be set for a specific user’s environment, or to configure environments such production or test.The Config system of yaclipy uses the standard python import system for namespacing.ConfigVarobjects are declared withConfig.var().answer=Config.var("The answer to everything.",42)speech=Config.var("What does the leader say?","I declare that...")a,b=Config.var(),Config.var()# These names will not be 'a' and 'b'.  They will be 'unk'.The variable name is grepped from the stacktrace, so anything other thanvar = Config.var()will result in a name being “unk”.When a variable needs be used, or set it can be brought into scope in the usual way and then reada()or seta("HelloWorld").Config vars should only be set from special@Config.option()decorated functions.  This creates aConfigOptionclass that can configure all of the necessary variables for a specific application environment or purpose.@Config.option()defone():answer(1)speech("I am the one")@Config.option()deftwo():answer(2)speech("I am not the one")This creates two different configuration options.
UseConfig.configure('one')to execute theone()option and set the corresponding config vars.Additional config files can be imported in the usual way from your mainconfig.pyroot file.with Config.include: import local.configcan be used for optional, possibly non-existent modules.In these local config modules it is common to want to override and existing option.# local/config.pyimportconfig@config.two.override()deftwo(super):super()# This is the old two() functionanswer(-answer())cli.pyInstead of installing yaclipy into the system, it is better to manage python packages on a per-project basis with virtual environments.To easily facilitate this style, copy the contents ofexamples/venvto your project directory and then run./cli.py.Thecli.pyfile simply bootstraps a project-local virtual environmentVENV_DIR, installs yaclipy into it, and then turns control over to yaclipy.Therequirements.txtfile holds the package dependencies that need to be installed into the virtual environment.  When changing dependencies make sure to delete the corresponding lock file so that the changes are picked-up.InstallationInstead of installing this manually, use the bootstrapping method shown above inexamples/venv.$pipinstallyaclipyPluginsOther libraries may be imported and used as sub-commands.Test$hatchshell$pytestLicenseyaclipyis distributed under the terms of theMITlicense.