Package: appeal
Copyright 2021-2023 by Larry HastingsQuickstartimportappealimportsysapp=appeal.Appeal()@app.command()defhello(name):print(f"Hello,{name}!")app.main()Here's a simplefgreputility:importappealimportsysapp=appeal.Appeal()@app.command()deffgrep(pattern,*files,ignore_case=False):ifnotfiles:files=['-']print_file=len(files)>1ifignore_case:pattern=pattern.lower()forfileinfiles:iffile=="-":f=sys.stdinelse:f=open(file,"rt")forlineinf:ifignore_case:match=patterninline.lower()else:match=patterninlineifmatch:ifprint_file:print(file+": ",end="")print(line.rstrip())iffile!="-":f.close()if__name__=="__main__":app.main()OverviewAppeal is a command-line argument processing library for
Python, likeargparse,optparse,getopt,docopt,Typer, andclick.  But Appeal takes a
refreshing new approach.Other libraries have complicated, cumbersome interfaces
that force you to repeat yourself over and over.
Appeal leverages Python's rich function call interface,
making it effortless to define your command-line interface.
You write Python functions, and Appeal translates them into
command-line options and arguments.Appeal provides amazing power and flexibility--but it's
also intuitive, because it mirrors Python itself.
If you understand how to write Python functions,
you're already halfway to understanding Appeal!Appeal has only one dependency,mybiglibrary.Appeal is currently only supported for POSIX platforms
(UNIX, Linux, BSD, OS X, etc).  It might work on Windows
but this has not yet been tested.A New And Appealing ApproachAppeal isn't like other command-line parsing libraries.
In fact, you really shouldn't think of Appeal as a
"command-line parsing library" per se. And, although you
work with Appeal by passing in functions for Appeal to call,
you shouldn't think of these functions as "callbacks".Appeal lets you designAPIscallable from the command-line.
It's just like any other Python library API--except that
the caller calls you from the command-line instead of from
Python.  Appeal is the mechanism converting between these two
domains: it translates your API into command-line semantics,
then translates the user's command-line back into calls to your API.This raises another good point: the API you build using Appeal
also often makes for a very niceautomation API,allowing
your program to also be used as a library by other programs
with minimal effort.BasicsTaxonomyLet's start by establishing the terminology we'll use
for command-lines, based on command-line idioms established
by POSIX and by popular programs.  Here's a sample
command-line, illustrating all the various types of things
you might ever see:% ./script.py --debug add --flag ro -v -xz myfile.txt
  ^           ^       ^   ^      ^  ^  ^   ^
  |           |       |   |      |  |  |   |
  |           |       |   |      |  |  |   argument
  |           |       |   |      |  |  |
  |           |       |   |      |  |  multiple short options
  |           |       |   |      |  |
  |           |       |   |      |  short option
  |           |       |   |      |
  |           |       |   |      oparg
  |           |       |   |
  |           |       |   long option
  |           |       |
  |           |       command
  |           |
  |           global long option
  |
  program nameCommand-lines are a sequence of strings separated by
whitespace.  The meaning of each string can depend
both on the position of the string and the characters
in the string itself.Anargumentis any whitespace-delimited string on the
command-line that doesn't start with a-(minus sign).
Unless it's anoparg--which we'll talk about in a minute--the
meaning of an argument is defined by its position.  For example,
if you ran:fgrep WM_CREATE window.cWM_CREATEandwindow.cwould bearguments;the first argument,WM_CREATE,
would be the string you wanted to search for, andwindow.cwould be the name of the file you wanted to search.Acommandis a special kind of argument some programs
use to specify what function you want the program to perform.
A good example of a program that uses commands isgit;
when you rungit addorgit commit,addandcommitare bothcommands.The command is always the first
argument to a program that uses them.If a string on the command-line starts with a-(minus
sign), that's anoption.  There are two styles of
option:short optionsandlong options.Short optionsstart with a single dash,-.  This is
followed by one or more individual characters, which
are the short option strings.  In the above example,
we specify two sets of short options: the first is-v,
the second is-xz.  You can combine options togther,
and it's the same as specifying them separately.  We
could have said-vxz, or-v -x -z; these both do
the same thing.  When we talk about short options, we
say the word "dash" followed by the letter.  For example,-vwould be pronounced "dash v".Long optionsstart with two dashes,--.  Everything
after the two dashes is the name of the option.  In the
above example, we can see one long option,--flag.
Again, when we talk about long options, we say the
dashes out loud, followed by the words from the option.
For example,--flagwould be pronounced "dash dash flag".Both types of options can optionally take one (or more)
arguments of their own.  An argument to an option is
called anoparg.In the above example, the long option--flagtakes the opargro.Finally, there areglobal optionsandcommand
options.Global options apply to the entire
program, are always available, and are specifiedbeforethe command.  Command options are
command-specific, and appearafterthe command.
Global options can be long options or short options;
command options can be long options or short options, too.Remapping Python To The Command-LineNow let's consider a Python function call:deffgrep(pattern,filename,*,ignore_case=False):...We can draw some similarities between Python
function calls and command-lines.For example, they both support arguments where
position is significant.  A command-lineargumentis similar to a Python functionpositionalparameter, in that they're both identified by
position.Python function calls and command-lines also
both support arguments identified by name.
A command-lineoptionis similar to a Pythonkeyword-onlyargument.This leads us to the fundamental concept behind Appeal.
With Appeal, you write a Python function, and tell
Appeal that it represents acommand.Appeal
examines the function, translating its parameters into
command-line features.  Positional parameters become
command-line arguments, and keyword-only parameters
become options.(Technically, Appeal translates bothpositional parametersandpositional-or-keyword parametersinto arguments.
For the sake of clarity and consiseness, I'll always
refer to these collectively aspositional parameters.)Hello, World!Let's see Appeal in action, with our first example.
In all our examples we're going to assume your program
is calledscript.py.  Let's sayscript.pylooked like
this:importappealapp=appeal.Appeal()@app.command()defhello(name):print(f"Hello,{name}!")app.main()If you now ranpython3 script.py help hello, you'd
see usage information for yourhellocommand.
It'd start like this:usage: script.py hello nameAlready, a lot has happened!  Let's go over it piece by piece:We created anAppealobject calledapp.
This object will handle processing the command-line
and calling the appropriate command function.We decorated the functionhello()with@app.command(),
a method call on our Appeal object.
This tells Appeal thathello()should be acommand, using the name of the function as the
command string, and translating the function's
parameters into the command-line parameters.
So our command-line command is calledhello.
We call a function decorated with@app.command()acommand function.Ourhello()command function takes one positional
parameters,name.  Therefore, ourhellocommand
on the command-line takes one positional argument,
which we identify asnamein the usage string.Appeal also automatically created simple help for our
program, displayingusageinformation.  Usage shows
you what command-line options and arguments the command
will accept.So!  If you ran this command at the command-line:% python3 script.py hello worldAppeal would call yourhello()function like this:hello('world')and you'd be rewarded with:Hello, world!The return value from your command function is the return
code for your program.  If you returnNoneor0, that's
considered success; returning a non-zero integer indicates
failure.  (And if your function exits without a return
statement, Python behaves as if your function ended withreturn None.)Default Values And*argsLet's change up our example, and add an optional parameter:importappealapp=appeal.Appeal()@app.command()deffgrep(pattern,filename=None):print(f"fgrep{pattern}{filename}")app.main()Now our command is calledfgrep, and it takes two parameters.
The second one,filename, is optional, with a default value ofNone.You can of course specify both parameters yourself.  Running this:% python3 script.py fgrep WM_CREATE window.cresults in Appeal calling yourfgrep()function like this:fgrep('WM_CREATE','window.c')But you can also omit thefilenameparameter.
If you run this command at the command-line:% python3 script.py fgrep WM_CREATEAppeal would callfgrep()like this:fgrep('WM_CREATE',None)Actually that's not 100% accurate.  When Appeal
builds the arguments to call yourfgrep()function,
it only passes in the arguments you passed in on the
command-line.  So actually Appeal calls yourfgrep()function like this:fgrep('WM_CREATE')And it's Python that sets thefilenameparameter toNone.What else can Appeal command functions do?  Well, they can
have a*argsparameter. Naturally, a command function that
takes*args(internally called avar_positionalparameter) can accept as many positional arguments as the
user wants to supply.  Here's a demonstration:importappealapp=appeal.Appeal()@app.command()deffgrep(pattern,*filenames):print(f"fgrep{pattern}{filenames}")app.main()Now the user could pass in no filenames, one filename,
fifty filenames--as many as they want!  They'd all be
collected in a tuple and passed in tofgrep()in thefilenamesparameter.Options, Opargs, And Keyword-Only ParametersNow let's examine what Appeal does with keyword-only
parameters.  Let's add three keyword-only parameters
to our example:importappealapp=appeal.Appeal()@app.command()deffgrep(pattern,*filenames,color="",number=0,ignore_case=False):print(f"fgrep{pattern}{filenames}{color!r}{number}{ignore_case}")app.main()Now thefgrepcommand-line usage looks like this:usage: script.py fgrep [-c|--color] [-n|--number int] [-i|--ignore-case] pattern [filenames]...Again, a lot just happened.First, I'll remind you, keyword-only parameters
are presented as options on the command-line.
Appeal automatically took each keyword-only parameter,
added'--'to the front of the parameter name,
and turned that into an option.  (Also, if the parameter
name has any underscores, Appeal turns those into dashes.)Second, Appeal also automatically uses the first letter of a
keyword-only argument as a short option.  So thecolorkeyword-only parameter becomes both the--colorand-coptions.  When running your program, the user
can use-cor--colorinterchangably.  The same goes
for-iand--ignore_case, and for-nand--number.(What if you have two keyword-only parameters that start
with the same letter?  The first one gets the short option.
If we added a keyword-only parameter namedcreditto the
end offgrep()'s parameter list, Appeal would mapcolorto--colorand-c, but only mapcreditto--credit.)Third, options arealways optional.(As a pedantic wag might put it--"the clue's right there in the name.")
Therefore, in Appeal, keyword-only
parameters to command functions mustalwayshave a
default value.  (Python programmers usually have default
values for their keyword-only parameters anyway, so this
requirement isn't a big deal.)Fourth, notice that--colortakes an argument, oroparg.Appeal noticed that thecolorparameter had a default
value of""--its default value is astr.
So Appeal infers that you want the user to supply an oparg
to--color.  If the user specifies--coloron the
command-line, it must be followed by an oparg, and Appeal
will take the string off the command-line and pass it
straight into thecolorparameter.Fifth,--numberalso takes an oparg, but it has a default of0.
Appeal infers from that that--numbershould be anint.
Appeal automatically converts the string from the command-line
into a Python object for you, using the type of the default value.
(Appeal did that for--colortoo--except--colortakes a str,
so no conversion is necessary.)  When the user provides an oparg
to--numberon the command-line, it must be followed by an
oparg; Appeal will take that oparg, pass it in toint, then take
the return value fromintand pass it in to thenumberparameter.Finally,ignore_casehas a default value ofFalse.
Boolean values for options are a special case: they don't
take an oparg. All they do is negate the default value.
So if the user specifies-ionce on the command-line,
Appeal would passTruein to theignore_caseparameter.(By the way, a default value ofNoneis a second
special case.  If a positional or keyword-only parameter
has a default value ofNone, Appeal behaves as if the
type of the default isstr.  It consumes an argument
or oparg from the command-line and passes it in unchanged
to that parameter.)Let's put it all together!  If you ran this command at the command-line:% python3 script.py fgrep -i --number 3 --color blue WM_CREATE window.cAppeal would callfgrep()like this:fgrep('WM_CREATE','window.c',color='blue',number=3,ignore_case=True)And if you ran this command at the command-line:% python3 script.py fgrep --color green boogalooAppeal would callfgrep()like this:fgrep('boogaloo',color='green')The Global Command, Subcommands, And The Default CommandMany programs that support "commands" also have
"global options".  Global options are options
specified on the command-linebeforethe command.
For example, in the example command-line at the top
of this document,script.pytakes a--debugoption specified before the command--which makes it
a "global option".Appeal supports global options, too.  It's simple:
write your command function like normal, but
instead of decorating it withAppeal.command(), decorate
it withAppeal.global_command().  Appeal will process all
those options before the command, and call your global
command function.Appeal.global_command()also gets used for programs that
don't use "commands".  Although the "command" command-line
paradigm is popular these days, most programs don't bother
with them.  For example,ls,grep, and... hey!pythonitself!  None of these programs support commands, but they
all support command-line arguments and options.Naturally, Appeal supports this behavior.  Simply decorate
one function withAppeal.global_command()and don't add
any command functions.On the flip side of this coin, Appeal also supportssubcommands.  This is a common feature of command-line
parsing libraries, though it's rarely-used in practice.
The idea is, your command canitselfbe followed by
another command.To add a subcommand to your Appeal instance, just
decorate your command function with two chained
command calls, specifying the name of the existing
command in the first call, like so:@app.command()defdb(...):...@app.command("db").command()defdeploy(...):...This adds adeploysubcommand under thedbcommand.
So now the whole command-line looks something like this:script.py [global arguments and options] db [db arguments and options] deploy [deploy arguments and options]Finally, what should Appeal do if your program
takes commands, but the user doesn't supply one?
That's what thedefault commandis for.  The
default command is a command function Appeal will
run for you if your Appeal instance has commands,
and the user doesn't supply one.  For example,
ifscript.pyhas ten different commands, but the
user just runsscript.pywithout any arguments, Appeal would run the default
command.If you don't specify a default command, Appeal has
a built-in defaultdefault command.  The defaultdefault
commandraises a usage error, which means it prints basic help
information and exits.To specify your own default command, just decorate a
command function with theAppeal.default_command()decorator.
For example, if you wanted your program to run thestatuscommand when the user didn't specify a command, you could
do this:@app.default_command()defdefault():returnstatus()Notice that the default command doesn't take any arguments
or options.  It simply can't accept any, by definition.(If the user specified options
without a command, they'd be considered "global options"
and would be processed by the global command.  And if the
user specified an argument, that would automatically be the
name of the command to run.)And yes, subcommands can have a default command too:@app.command('db').default_command()defdb_default():returndb_status()Annotations And IntrospectionPython 3 supports annotations for function parameters, meant
to conceptually represent types.  Appeal supports annotations
too; they explicitly tell Appeal what type of object a parameter
requires.  For example:importappealapp=appeal.Appeal()@app.command()deffgrep(pattern,*filenames,id:float=None):print(f"fgrep{pattern}{filenames}{id}")app.main()Hereidhas a default value ofNone, but it also has
an explicit annotation offloat.   If the user uses--idon the command-line, it must be followed by an oparg,
which Appeal will convert to a Python object by callingfloat.
(And, as you can see, the annotation and the type of the default
don'tnecessarilyhave to agree... although it's usually a
good idea.)Although annotations aremeantto represent types, Appeal
actually accepts any callable--it can be a type, or a
user-defined class, or just a regular function.  Appeal
calls these annotationsconverters.Here's how Appeal decides on the converter for a parameter,
from highest-priority to lowest-priority:If the signature for that parameter has an annotation,
Appeal uses the annotation as the converter.If the signature for that parameterdoesn'thave an
annotation, butdoeshave a default value, Appeal
will usetype(default)as the converter in most cases.
The exceptions:Iftype(default)isNoneType, Appeal will usestrinstead.Iftype(default)isbool, and the parameter is a
keyword-only parameter, Appeal will use a special internal
class that implements the special-case "negate the default"
behavior for options with boolean default values.If the signature for that parameter lacks both an annotationanda default value, Appeal usesstras the converter.Converters are surprisingly flexible.
For example, Appeal will introspect the converter for a
keyword-only parameter and map all its positional arguments
into opargs.  That's how Appeal supports options that takemultiple opargs:you simply annotate the keyword-only
parameter with a converter that takesmultiple arguments.Appeal will also pay attention to the annotations for the
converter's own arguments, and use those to convert the
strings from the command-line into Python objects.Let's tie it all together with another example:importappealapp=appeal.Appeal()defint_and_float(integer:int,real:float):return[integer*3,real*5]@app.command()deffgrep(pattern,*filenames,position:int_and_float=(0,0.0)):print(f"fgrep{pattern}{filenames}{position}")app.main()Here, Appeal would introspectfgrep(), then also
introspectint_and_float().  The resulting usage
string would now look like this:usage: script.py fgrep [-p|--position integer real] pattern [filenames]...--positiontakestwoopargs.  Appeal would
callinton the first one andfloaton the second
one.  It would then callint_and_float()with those
values, and the return value ofint_and_float()would
be passed in to thepositionparameter onfgrep().So now if you ran:% python3 script.py fgrep -p 2 13 funkyfreshAppeal would call:fgrep('funkyfresh',position=[6,65.0])Finally, let's change the example to demonstrate something
else: although converters can be any callable, user-defined
classes work fine too.  And Appeal can correctly infer the
type based on the default value for any type.  So consider
this example:importappealapp=appeal.Appeal()classIntAndFloat:def__init__(self,integer:int,real:float):self.integer=integer*3self.real=real*5def__repr__(self):returnf"<IntAndFloat{self.integer}{self.real}>"@app.command()deffgrep(pattern,*filenames,position=IntAndFloat(0,0.0)):print(f"fgrep{pattern}{filenames}{position}")app.main()This example behaves essentially the same as the previous example
in this section, except the formatting ofpositionis slightly
different.  But the command-line usage is exactly the same!
Appeal inferred the converter forpositionbased on the type
of its default value, then introspected that type to determine
how many opargs it should consume from the command-line and how
to convert them.An important note about annotationsIf you use static type analysis in your project,
your static type analyzer may not enjoy analyzing Python
code using Appeal.  Static type analyzers are designed
to understand "type hints", a means of specifying static
type information introduced in Python 3.5 with thetypingmodule.  But Appeal doesn't use type hints,
and there are some ways Appeal uses annotations that
static type analyzers may not like.Fortunately, there are ways to get static type analyzers
to work alongside Appeal.First, you can decorate your Appeal command functions
and converters with@typing.no_type_check().  This should
only be necessary if you use functions as annotations;
if you only ever use types and classes, this shouldn't be
necessary.Second, if you're using Python 3.9 or newer, you can usetyping.Annotatedwith your annotations.typing.Annotatedallows you to specify an ordered list of values, and static
type hints only ever use thefirstvalue.  Appeal also
handlestyping.Annotated, but Appeal only ever uses thelastvalue.  This makes it easy--you can have both types
of annotations, side by side, and both static type checkers
and Appeal are perfectly happy.Converter FlexibilityYou can use almost any function as an annotation...
within reason.  Appeal will introspect your annotation,
determine its input parameters, and call it to convert
the command-line argument into the argument it passes
in to your command function.For example, what if you wanted an option that accepted
a string which gets broken up based on a delimiter substring?
This is a common idiom forconfigurescripts on UNIX-like
platforms; for example,Python's ownconfigurescriptsupports this option:--with-dbmliborder=db1:db2:...Happily that's easy to do in Appeal.  Just write a converter
function that accepts a string, breaks it into substrings
however you like, and returns the list.Although... you don't need to bother!  Appeal also provides
a converter that does it for you, calledappeal.split().
You pass in as many delimiter strings as you want, andappeal.split()will split the command-line across all of
them.  (If you don't specify any delimiters,appeal.split()will split at every whitespace character.)Specifying An Option More Than OnceOne thing you might have noticed by now: the interfaces
you've seen only allow Appeal to handle command-lines
where an option can be specified either zero times or
one time.  What if you want the user to be able to
specify an option three times?  Or ten?  That's what theMultiOptionclass is for.MultiOptionobjects
are converters that allow options to be specified
multiple times.MultiOptionisn't useful by itself; it's only an
abstract base class.  To make use of it you'll
need to use a subclass--or create your own.This time, let's start with some examples.  Appeal
provides three useful subclasses ofMultiOption:counter,accumulator, andmapping.First, let's look atcounter.countersimply counts the number of times an option is
specified on the command-line.  This is a somewhat
common idiom for "verbose" options; a program
that supports-vto meanverbosemay allow
you to specify-vmore than once to make
itmoreverbose.  Here's how you'd do that
with Appeal:importappealapp=appeal.Appeal()@app.command()deffgrep(*,verbose:appeal.counter()=0):print(f"fgrep verbose={verbose!r}")app.main()If the user ran% python3 script.py fgrepAppeal would callfgrep()allowing Python to pass in the default value of0toverbose.
And if the user ran% python3 script.py fgrep -v --verbose -vAppeal would callfgrep(verbose=3)accumulatorhandles options that take a single oparg.
It remembers them all and returns them in a single array.
Like so:importappealapp=appeal.Appeal()@app.command()deffgrep(*,pattern:appeal.accumulator=[]):print(f"fgrep pattern={pattern!r}")app.main()If the user ran% python3 script.py fgrep --pattern three -p four --pattern fiv5Appeal would callfgrep(pattern=['three','four','fiv5'])What if you don't want strings, but another type?  Using crazy
science magic from the future,accumulatoris actually
parameterized.  You can say:importappealapp=appeal.Appeal()@app.command()deffgrep(*,pattern:appeal.accumulator[int]=[]):print(f"fgrep pattern={pattern!r}")app.main()and now the opargs to--patternwill all be converted using int.You can even specify multiple types as arguments to the
parameterized version ofaccumulator, separated by commas.
The option will then require multiple opargs and convert
them to the types specified.mappingis likeaccumulatorexcept it returns adictinstead of alist.  An option annotated withmapping()consumestwopositional arguments from the command-line;
the first one is the key, the second one is the value.
(You can also parameterizemappingthe same way you parameterizeaccumulator, though you can only specify exactly two types.)Of course, you can also subclassMultiOptionto make your own
converter classes with custom behavior.MultiOptionsubclasses
can override these three methods:classMultiOption:definit(self,default):...defoption(self,...):...defrender(self):...Well, actually, subclasses arerequiredto overrideoption()andrender().  Butinit()is optional.If you then specify a subclass ofMultiOptionas an
annotation on a keyword-only parameter of an
Appeal command function, several things happen:If that option is specified one or more times on
the command-line, Appeal will instantiate exactly
one of these objects and call itsinit()method.Every time the user specifies that option on
the command-line, Appeal will call theoption()method on the object.After finishing processing the command-line,
Appeal will call therender()method on the
object, and pass the value it returns as the
argument to that keyword-only parameter.The most powerful part of this interface: you can
redefineoption()to suit your needs--it supports
the same sort of polymorphism as annotations do.
Appeal will introspect youroption()method to
determine how many opargs to consume from the
command-line, and how to convert them.Let's demonstrate all this with another example.
If you want your option to take two opargs,
with one being anintand the other being
afloat, you would defineoption()in your
subclass as:classMyMultiOption(appeal.MultiOption):defoption(self,a:int,b:float):....Every time the user specified your option,
it would take two opargs, and they would be
converted into anintand afloatbefore
calling youroption()method.  It's up to
you to decide how to store them, and how to
render them into a single value returned
by yourrender()method.MultiOptionis a subclass of a generalOptionclass.Optionbehaves identically
toMultiOption, except it only permits
specifying the option once on the command-line,
which means it will only youroption()method once.
You usually don't need to bother with making subclasses
ofOption--it's usually better to just use a class
directly, like ourclass IntAndFloatexample.
The only feature you get by subclassingOptionis,
you get the default value for the parameter passed in
to your constructor.(The downside of subclassingOptionandMultiOptionis that it makes exporting your Appeal API as an automation
API a little less convenient for the user, because your
users will have to construct these objects and feed
values into them by calling theoptionmethod.)Data ValidationWhat if you want to restrict the data the user provides
on the command-line?  That's simple, just use a converter!
Appeal provides a couple sample converters for data validation,
but it's easy to write your own.The classic example is a parameter where you can only use one
of a list of values.  For that, you can use Appeal'svalidate()converter.  For example, this command restricts thedirectionparameter to one of six canonical directions:importappealapp=appeal.Appeal()@app.command()defgo(direction:appeal.validate('up','down','left','right','forward','back')):print(f"go direction={direction!r}")app.main()You can pass in an explicit type using atype=named argument tovalidate(); if you omit it,
it uses the type of the first argument.Appeal also has a built-in range validator
calledvalidate_range().  It takesstartandstoparguments the same way Python'srange()function does. Note thatvalidate_range()differs from Python'srange()in one subtle way:
valuesequaltostopare allowed.If you prefer, you can "clamp"
the value the user passed in to the range,
by supplying the argumentclamp=Truetovalidate_range(). In that case, if the value
the user specifies is outside the range,validate_range()will return the closest value of eitherstartorstop.(That's whyvalidate_range()allows the
value to beequal tostop.clampwould
be annoying to use ifstopitself was an
illegal value--particularly if the types
were floats.)Appeal validation functions are straightforward to write.
So, if these are insufficient to your needs,
you can easily write your own.  Take a look
at the implementations ofvalidate()andvalidate_range()inside Appeal to see one way to do it!Multiple Options For The Same ParameterSome programs have a set of options on their
command-line that are mutually exclusive.  Consider
this simple-minded command-line:go [--north|--south|--east|--west]That is, you want the user to be able to "go" in
one of those four directions, butonlyone.
How would you do that in Appeal?Easy.  You simply define multiple options that
write to the same parameter.  All the behavior
you've seen so far is using thedefaultway of
mapping keyword-only parameters to options.  But
actually Appeal allows you to make your own mappings.
You can map a parameter as many ways as you want,
even using different converters!To manually define your own options, use theAppeal.option()method on your Appeal instance.  It's a decorator you
apply to your command function.  The first argument is
the name of the parameter you want the option to write
to.  After that is one or more options you want to
map to this parameter.Appeal.option()also takesdefaultandannotationkeyword-only parameters,
allowing you to specify respectively the default value or
annotation for this option.Here's a simple example of how to implement the abovegocommand with Appeal:importappealapp=appeal.Appeal()@app.command()@app.option("direction","--north",annotation=lambda:"north")@app.option("direction","--south",annotation=lambda:"south")@app.option("direction","--east",annotation=lambda:"east")@app.option("direction","--west",annotation=lambda:"west")defgo(*,direction='north'):print(f"go direction={direction!r}")app.main()All these annotations return a string.  But actually you can
return any type you want--and you can even map multiple
annotations that return different types to the same parameter.
You can even annotate with aMultiOptionto allow specifying
that option multiple times!Note that, whenever you use theoption()decorator
to map your own options onto a parameter, Appeal won't add
its default options for that parameter.  It'll only have
the options you explicitly set.  Which means, for example,
that in the sample code above, there aren't any short options
for the options we created.-nwon't work, only--north.One final thing.  Your command function can accept**kwargstoo.  The only things that will go into it are options you
create withAppeal.option(), which map to parameters that
don't otherwise exist.Recursive ConvertersYou already know that you can pass in a converter that takes
multiple arguments, and Appeal will consume multiple arguments
from the command-line to fill it.  And if the arguments to that
converter have annotations, Appeal will call those functions to
convert the command-line argument into the type your converter
wants.But what if you did...this?importappealapp=appeal.Appeal()defint_float(i:int,f:float):return(i,f)defmy_converter(i_f:int_float,s:str):return[i_f,s]@app.command()defrecurse(a:str,b:my_converter=[(0,0),'']):print(f"recurse a={a!r}b={b!r}")app.main()Themy_converter()parameteri_fis a positional
parameter with an annotation that, itself,takes two positional parameters.Would it surprise you to know--yes, it actually works!Converters have been fully recursive thiswhole time.Actually this fact has been hiding in plain sight
all along--all the examples usingint_and_float()are recursive
too, becauseint_and_float()has parameters annotated withintandfloat.  Of course, those functions only take
a single string argument;my_converter()takes two
annotated positional parameters.
But the principles remain the same.Still, this is a more complex situation than we've seen before.recursetakes a positional parameterbthat has a default
value, but its converter takes multiple positional parameters,
and one of thosealsohas a converter that takes multiple
positional parameters.  How does Appeal map this to the
command-line?Appeal "flattens" the tree of converter functions into a linear
series of arguments and options.  In this case the usage string
would look like this:recurse a [i f s]This tells you therecursecommand takes either one or four command-line
arguments.  That optional group of three command-line arguments
has a special name in Appeal: it's an "argument group".
Technically, Appeal views this command-line as taking two
"argument groups": the first group is required, and consumes
one command-line argument; the second group is optional, and
consumes three command-line arguments.(We actually saw our first "argument group" in the
second example in theAnnotations And Introspectionsection above, but
that time the argument group was an oparg.)Now let's add an option and see what changes:importappealapp=appeal.Appeal()defint_float(i:int,f:float):return(i,f)defmy_converter(i_f:int_float,s:str,*,verbose=False):return[i_f,s,verbose]@app.command()defrecurse2(a:str,b:my_converter=[(0,0),'',False]):print(f"recurse2 a={a!r}b={b!r}")app.main()Now the usage looks like this:recurse2 a [i [-v|--verbose] f s]Notice the way Appeal renders it in the usage
string--the options aren't created untilafterthe first
argument in the optional argument group.  This may seem
strange but that's how it works.  That's how ithasto work.Why?  From a high conceptual level, Appeal doesn't know that
you've "entered" the optional argument group until it
sees the user supply the first argument for that group.
So it doesn't create the options defined in that group
until after the first argument.This high conceptual level corresponds exactly to how Appeal
calls your function.  Consider, if the user runs this command:recurse2 xyzAppeal calls your function like so:recurse2('xyz')Since Appeal never calledmy_converter(), it can't
map--verbose.  It can only map--verboseonce it
knows it's going to callmy_converter(), and that
only becomes true the moment you supply that second
command-line argument.Once youdosupply that second command-line argument,
you have to supply two more, for a total of four.recurse2 pdq 1 2 xyzAppeal calls your function like so:recurse2('pdq', my_converter(int('1'), float('2'), xyz))

recurse2 pdq 1 2 xyzAnd in this example, you can supply the-vor--verboseanywhereafterthe second parameter.  So if your command-line looks like this:recurse2 pdq 1 2 xyz -vAppeal callsrecurse()like this:recurse2('pdq',my_converter(int('1'),float('2'),xyz,verbose=True))Take a look back at all the examples in this document, and consider
that anywhere you specify a function or type, you can pass in nearly
any callable you like.For example, the parameterized version ofmappingisn't limited just to simple types.
If you usedmapping[str, int_float]as the annotation
for a keyword-only parameter, that option would consume
three arguments on the command line: astr, anint, and
afloat, and the dictionary would map strings to 2-tuples
of ints and floats.Now you're starting to see how powerful Appeal's converters
really are!Now Witness The Power Of This Fully Armed And Operational Battle StationBut recursive converters are just the beginning!Buckle your seatbelt, Dorothy--because Kansas is going bye-bye.--Cypher, "The Matrix" (1999)Options that map other optionsWhat if you did...this?importappealapp=appeal.Appeal()defmy_converter(a:int,*,verbose=False):return[a,verbose]@app.command()definception(*,option:my_converter=[0,False]):print(f"inception option={option!r}")app.main()Woah, that works too!  We've created an option thatitselftakes an option.  If you runfgrep --option,
you can now also specify-vor--verbose--but onlyafteryou've specified--option.In case you're wondering:Appeal.option()must
decorate the function that takes the parameter you're
mapping an optionto.So if you want to define
explicit options for theverboseparameter tomy_converterin the above example, you'd
decoratemy_converterwithAppeal.option()calls,
notinception.  (This also means, everywhere you
usemy_converteras a converter, it will behave
the same, including taking the same options.)Multiple options that aren't MultiOptionsWe're just getting started!  How about this:importappealapp=appeal.Appeal()defmy_converter(a:int,*,verbose=False):return[a,verbose]@app.command()defrepetition(*args:my_converter):print(f"repetition args={args!r}")app.main()That works too, and I bet you're already guessing what it
does.  This version ofweirdaccepts as manyintarguments
as the user wants to specify on the command-line, andeach onecan optionally take its own-vor--verboseflag.Positional parameters that only consume optionsI'll give you one more crazy example:importappealapp=appeal.Appeal()classLogging:def__init__(self,*,verbose=False,log_level='info'):self.verbose=verboseself.log_level=log_leveldef__repr__(self):returnf"<Logging verbose={self.verbose!r}log_level={self.log_level}>"@app.command()defmixin(log:Logging):print(f"mixin log={log!r}")app.main()Can you guess what usage formixinlooks like?  (Probably!)
It looks like this:mixin [-v|--verbose] [-l|--log-level str]Even thoughlogis a positional parameter, it doesn't consume
any positional arguments on the command-line.  TheLoggingconverter only adds options!  This is what object-oriented
programmers might call a "mix-in".  With theLoggingconverter,
you can add logging options to every one of your commands, without
having to re-implement it each time.  (Though in most cases it's
probably better to add such options to a global command function.)Internally this works exactly like you'd expect.  Since thelogparameter consumes no command-line arguments, Appeal will
always call its converter.  Specifying any of the options will
set arguments for that call.  And the resultingLoggingobject
will be passed in as the argument tolog.What's really going on here is that, from Appeal's perspective,there's no difference between a "command function" and a
"converter".A command function is just a converter that
happens to be mapped to a command.  So anything you can do
with a command function, you can do with a converter too.
A converter can define options, it can be decorated withapp.option()(orapp.parameter()which we haven't
discussed yet), it can have accept any kind of parameter defined
by Python, and any parameter can use (almost) any converter.
And those converters can recursively use other converters.Realy, anything can be used with anything:Converters for positional parameters
can take positional parameters, or keyword-only parameters, or*args, or**kwargs.Converters for keyword-only parameters
can take positional parameters, or keyword-only parameters, or*args, or**kwargs.Converters for*argscan take positional parameters, or keyword-only parameters, or*args, or**kwargs.Command functions can use any converter.The global command function can use any converter.Bynowyou can see the expressive power Appeal gives you.
Of course, you'll rarely use only a fraction of that power.
But it's reassuring to know that, whatever command-line API
metaphor you want to express, it's not justpossiblein
Appeal--it'seasy.Classes, Instances, And PreparersMaybe you've noticed--all the examples so far have used
standard Python functions as Appeal commands.  What about
method calls?  Can you use those for commands?  The answer
is, yes of course!  But it's slightly more complicated.Appeal's whole purpose in life is to call functions by
pulling data from the command-line.  Whenever it sees a
positional parameter on a function, it thinks "okay, I'm
gonna have to supply an argument to that".  So if you map
anunboundmethod call to a command:classMyApp:@app.command()defsum(self,*operands:int):returnsum(*operands)Appeal would see theselfparameter and think "aha! I
need to pass a string in there!"  We need to prevent
Appeal from seeing that parameter in the first place.There are two major techniques to handle this.  The first
is straightforward, if a bit inflexible: create the instance
of your class first, then callapp.command()()on the
bound instances.  Like this:app=appeal.Appeal()classMyApp:defsum(self,*operands:int):returnsum(*operands)o=MyApp()app.command()(o.sum)app.main()Since you pass in the already-bound method to Appeal, it doesn't
evenseetheselfparameter in the signature.  (The signature
of a bound method doesn't include theselfparameter.)This works fine... but maybe it looks a little weird.  We're no
longer decorating functions (or methods), instead we're calling
the decorator function directly and passing in the bound method.
It also restricts us to one instance ofMyAppper Appeal
instance, which might be restrictive.The other technique uses a little magic to provide a convenient
and familiar-looking interface.Appeal.app_class()gives you
two decorators; you use one to decorate your class, and the
other to decorate methods in the class.  Appeal will instantiate
your class for you, and use your__init__method as your app's
"global command" to handle global options!importappealapp=appeal.Appeal()app_class,command_method=app.app_class()@app_class()classMyApp:def__init__(self,*,verbose=False):print(f"MyApp init verbose={verbose!r}")self.verbose=verbosedef__repr__(self):return"<MyApp>"@command_method()defadd(self,a,b,c):print(f"MyApp add self={self!r}a={a!r}b={b!r}c={c!r}self.verbose={self.verbose!r}")app.main()Behind the scenes, this uses aCommandMethodPreparerobject
to handle late-binding the method to the object.  SinceAppeal.app_class()is a little inflexible, you may want
to use these objects directly.  You can create one manually
by callingAppeal.command_method().  Here's an example showing
how to use one:importappealapp=appeal.Appeal()command_method=app.command_method()classMyApp:def__init__(self,id):self.id=iddef__repr__(self):returnf"<MyApp id={self.id!r}>"@command_method()defadd(self,a,b,c):print(f"MyApp add self={self!r}a={a!r}b={b!r}c={c!r}")my_app=MyApp("dingus")p=app.processor()p.preparer(command_method.bind(my_app))p.main()This is the first time you're seeing theProcessorobject.  All the runtime information for processing
a command-line lives in theProcessorobject; in
fact,Appeal.mainandAppeal.processare both
thin wrappers over their equivalent methods on theProcessorobject.  Moving all the runtime information
into theProcessorobject lets you process multiple
command-lines with the same Appeal object, even
simultaneously!TheCommandMethodPreparerobject is at the core of how Appeal
handles late-binding of methods to objects.  First,
you decorate the method calls of your class with this object.
You then call thebindmethod on that object to pass in the
instance of that class you want to bind those methods to--thoughapp_class()takes care of that for you.bind()returns a callable
you pass in toProcessor.preparer, which binds the method to that
instance before Appeal calls it.Under the covers,CommandMethodPreparerwraps the method
with afunctools.partialobject, passing in a placeholder
object for theselfparameter.  Thencommand_method.bind()replaces the placeholder for the real instance.  For maximum
compatibility, it actually usesgetattr()to bind the
instance to the method.Writing HelpAppeal automatically generatesusagefor your command functions.
But it's up to you to write the documentation explaining what those
commands and arguments and options actuallydo.There's very complete notes on how to write documentation in Appeal,
seeappeal/notes/writing.documentation.txtin the Appeal source
distribution.  In a nutshell, you write docstring in a particular way,
and Appeal can mechanically parse them and combine them together.
So you document each converter separately, and Appeal smooshes all
these bits of documentation together to produce the help for your
command function.(One note: the main help for your program should be the docstring
for your Appeal instance's global command.)Appeal's latest superpower: reading config filesAppeal allows for friction-free command-line APIs.  You write your
command function, point Appeal at it, and whoosh! now you've got a
command-line interface.  But there are other interfaces users may
want to use to configure your program.  Now Appeal can work with
those too.For example, your program may read configuration from environment
variables.  Some programs launch an editor; for examplegitwill
open an editor when committing a revision.  Traditionally on
UNIX-based platforms this is configurable using two environment
variables,VISUALandEDITORin that order of preference.Appeal doesn't need to add explicit support for environment variables,
as Python already has an easy-to-use interface.  For example, here's
how to support the environment variables configuring your editor:However, many programs also support a configuration file, also called
an "rc file"  on UNIX.  By convention settings in such a config file
usually take precedence over environment variables.  For example, you
can configure what editorgituses for commits with a value calledcore.editorstored in a config file.As of 0.6, Appeal has support for reading data from configuration
files.  Note that Appeal doesn't read the data files itself; you
already have a library for that.  Instead, Appeal has a generic
mechanism for reading data from either an iterable or a mapping--
either a list or a dict.The first step is to read in the values from the configuration file,
and produce a dict or dict-like object.  You can use any library
you like.  For example, thetomlilibrary
works well forTOML files.JSON and YAML parsers also work nicely.  And this facility worksespeciallywell with myPerkyfile format.  Though that's just a coincidence, as they were designed
separately, years apart.  Honest!(You can also useconfigparserto read your INI config file,
but this doesn't mesh well with Appeal's model.  Better support
for reading INI files is a possible future direction for Appeal.)Once you've got a dictionary containing your configuration information,
you can get Appeal to read from it using a single method call:Appeal.read_mapping(self,callable,mapping)Simply pass in the callable you want called, and the mapping--the dict--you
read from your config file.  Appeal will read the names of the callable's
parameters, pull values out of the mapping using those names, and pass those
values in to a call to the callable.Of course, any mapping will work.  But this method works particularly
well with classes decorated withdataclasses.dataclass.  In just a few
lines, you can define a class to contain your configuration information,
read it out of a file, and populate the class with values of all the correct
types!In a lot of ways, this works very similarly to Appeal when it's processing
a command-line.  For example:Appeal will use the annotations and default values to convert
the values from the dictionary into the correct types.Parameters with default values are optional; parameters without
default values are required.But there are differences too:You can use positional-only, positional-or-keyword,
or keyword-only arguments.  However, var-positional
(*args) and var-keyword (**kwargs) are unsupported.Let's bring all of this together with an example.  Let's say we're
writing a hypothetical program that may launch an editor.  Our
sophisticated program hasfiveways to decide what program
to run for the editor.  In decreasing order of importance:Command-line options '-e' and '--editor' specify the
editor to use for this instnace.The config file~/.myprogramrcis a Perky file, and it
can contain aneditorvalue.If the user has set anVISUALenvironment variable, use that.If the user has set anEDITORenvironment variable, use that.The default value is/usr/bin/vi.Here's sample Python code implementing those semantics:default_editor=os.environ.get("VISUAL",os.environ.get("EDITOR","/usr/bin/vi"))@dataclasses.dataclassclassConfigFile:editor:str=default_editord=perky.load(os.path.expanduser("~/.myprogramrc"))app=appeal.Appeal()config_file=app.read_mapping(ConfigFile,d)@app.global_command()defglobal_command(*,editor=config_file.editor):print(f"editor ={editor}")app.main()Note: usingos.path.expanduserand a hard-coded filename
like this is no longer considered best practice.  You should useplatformdirsto
define the paths to your config files.NestingAppeal's config file reader supports reading values from nested dicts.
This maps directly onto nested function calls in annotations.  If an
annotation takes two or more parameters, the name of the parameter
with that annotation will be used as the name of the nested dict.Since that probably wasn't clear--sorry!--an example would probably help.
Consider this example dictionary:d={'a':33,'b':{'verbose':True,'color':'blue',},}Here the value of'b'is a nested dict.  If we want Appeal to read
a dict with this shape, it will have to descend into that nested dict.
Appeal does that by default when a parameter has an annotation, and
the annotation takes two or more parameters.  Here's sample Python
showing how to read this dict using Appeal:defread_b(verbose=False,color='black'):return(verbose,color)defconfig_file(a:int,b:read_b):return(a,b)Becauseread_bis an annotation taking multiple parameters,
Appeal will assume the value of'b'is a nested dict,
and will get the values of'verbose'and'color'from
that dict.If you don't want this behavior, you can disable it by
decorating the annotation function with theunnestedmethod on the Appeal object.  If we change the code to
the following:@app.unnested()defread_b(verbose=False,color='black'):return(verbose,color)defconfig_file(a:int,b:read_b):return(a,b)Appealwon'tdescend into a nested dict named'b'.
In this case the dictionary would have to be be completely
flat, like this:d={'a':33,'verbose':True,'color':'blue'}IterablesAppeal can also read from iterables inside the dictionary.
The parameter that accepts an iterable should be annotated
with a subclass ofMultiOption.  Appeal will instantiate
theMultiOptionand use the MultiOption protocol to
fill the object.For example, if your config file dict looked like this:d={'color':'blue','lines':['line 1','here is line 2','and finally, line 3',]}Appeal could map it to this callable:@dataclasses.dataclassclassConfigFile:lines:appeal.accumulatorcolor:str=''If theMultiOptionoption takes multiple parameters,
then the list must contain dictionaries.  For example,
this Python code:classResolutions(appeal.Multioption):definit(self,default=None):self.default=defaultself.values=Nonedefoption(self,width:int,height:int,depth:int):ifself.valuesisNone:self.values=[]self.values.append((width,height,depth))defrender(self):ifself.valuesisNone:returnself.defaultreturnself.values@dataclasses.dataclassclassConfigFile:resolutions:Resolutionscolor:str=''Would be able to read this mapping:d={'color':'orange','resolutions':[{'width':1280,'height':1024,'depth':24},{'width':1600,'height':1200,'depth':16},],}read_iterableIn addition to Appeal'sread_mappingmethod, Appeal
also supports aread_iterablemethod.  The API is
almost identical:Appeal.read_iterable(callable,iterable)However this function is much simpler.  The iterable
should itself be an iterable of iterables.  Appeal will
call the callable you specify once for every nested
iterable.  The result will be appended to a list, andread_iterablewill return that list.As always, Appeal will handle converting values using
the annotations you specify.  Unlikeread_mapping,
here you may pass in a function accepting*args,
in which case Appeal will process an arbitrary number
of trailing arguments.  Also unlikeread_mapping,
there's no support for testing--neither nested dictionaties
nor (further) nested lists.  (Like when processing the
command-line, when reading values from the iterable,
nested annotations get flattened.)CSV filesFinally, Appeal has special support for reading CSV files.  This may
seem like an odd thing to support--nobody uses CSV files as config
files.  But CSV files were the proof-of-concept for Appeal's config
file support, and it's proved useful in another project, so for
now it's staying in.  There's a special method for reading CSV
files:Appeal.read_csv(self,callable,csv_reader,*,first_row_map=None)You pass in your callable, and a freshcsv.readerobject.  Appeal
will read the rows out of theCSVobject, passing in the strings into
thecallable, and append the result to a list.  The return value is
that list.Iffirst_row_mapis false,read_csvwill ignore the first line of
the CSV file (the "column names" line) and pass in the values from the
CSV file by position.  Iffirst_row_mapis true,read_csvwill use
the rows from the first line of the CSV file as keys in a dictionary,
populate the values with each subsequent row, and will pass the
arguments by name.In other words, iffirst_row_mapis false, Appeal callscallable(*row)for every line after the first line in the CSV file.  And iffirst_row_mapis true, Appeal callsd={key:valueforkey,valueinzip(column_headers,row)}callable(**d)for every line after the first line in the CSV file.API ReferenceAppeal(help=True, version=None, positional_argument_usage_format="{name}", default_options=default_options)Creates a new Appeal instance.Ifhelpis true, Appeal automatically adds help support to
your program:Adds hard-coded-hand--helpoptions that print basic help.If your Appeal instance has any commands, and you haven't defined
ahelpcommand, automatically adds ahelpcommand.Ifversionis true, it should be a string denoting the version
of your program.  Appeal will automatically add version support
to your program:Adds hard-coded-vand--versionoptions that print
the version string.If your Appeal instance has any commands, and you haven't defined
aversioncommand, automatically adds aversioncommand
which prints the version string.positional_argument_usage_formatis the format string used
to format positional arguments for usage.  The only valid
interpolations inside this string are{name}, which evaluates
to the name of the parameter, and{name.upper()}, which evaluates
to the upper-cased name of the parameter.  So if you want your usage
string to show arguments or opargs as<name>orNAME, you can
achieve that by settingpositional_argument_usage_formatto<{name}>or{name.upper()}respectively.default_optionsis a callable, called when a keyword-only parameter
for a command function or a converter doesn't have any options
explicitly mapped to it.  The purpose ofdefault_optionsis to
callAppeal.option()one or more times to create some default options
for that keyword-only parameter.The API for adefault_optionscallable should be:default_options(appeal, callable, parameter_name, annotation, default)appealis the Appeal instance.callableis the command function or converter the parameter is defined on.parameter_nameis the name of the keyword-only parameter that does
not have any explicitly defined options.annotationis the annotation for this parameter.  This may
be explicitly set on the function, or it may be inferred from the
default parameter.defaultis the default value for this parameter.  Since Appeal
requires that keyword-only parameters must always have default values,
this may never beinspect.Parameter.empty.The return value ofdefault_optionsis ignored.The default value ofdefault_optionsisAppeal.default_options(),
documented below.Appeal.command(name=None)Used as a decorator.  Returns a callable that accepts a single
parametercallable, which must be a callable.Adds the callable as a command
for the current Appeal instance.  IfnameisNone, the name of
the command will becallable.__name__.(Doesn't modifycallablein any way.)Appeal.global_command()Used as a decorator.  Returns a callable that accepts a single
parametercallable, which must be a callable.Sets theglobal commandfor this Appeal object.  This is
the command that processes global options before the first
command function.Can only be set on the topmost Appeal object.  (You can't
callapp.command('foo').global_command().)(Doesn't modifycallablein any way.)Appeal.default_command()Used as a decorator.  Returns a callable that accepts a single
parametercallable, which must be a callable.Sets thedefault commandfor this Appeal object.  The default
command is run when your Appeal instance has subcommands,
but the user doesn't supply the name of a command on the command-line.Your default command function must not take any parameters.(Doesn't modifycallablein any way.)Appeal.option(parameter_name, *options, annotation=empty, default=empty)Used as a decorator.  Returns a callable that accepts a single
parametercallable, which must be a callable.Maps an option on the command-line to the parameterparameter_nameon the decorated function.  All subsequent positional parameters
are options, like--verboseor-v.  (Thus, they must be strings,
either exactly two characters long, or four or more characters long.)annotationis the converter that will be used if this
option is invoked.  If no explicitannotationis supplied,Appeal.option()will default totype(default).defaultis the default value for this option.  Since this parameter
only comes into play if the user specifies this option, adefaultvalue
here is nearly useless.  But it does have two uses:If the type of the annotation is a subclass ofOption, this default
value will be passed in toOption.init().If noannotationis specified, the annotation defaults totype(default).It's illegal to callAppeal.option()without specifying a value
for eitherannotationordefault.RaisesAppealConfigurationErrorif anyoptionhas already been
mapped inside thisAppealinstancewith a different signature.(Doesn't modifycallablein any way.)Appeal.parameter(self, parameter_name, *, usage=None)Used as a decorator.  Returns a callable that accepts a single
parametercallable, which must be a callable.Allows for configuration of a positional (or positional-or-keyword)
parameter on a command function or converter.parameter_nameis the
name of the parameter; it must be a parameter of the decoratedcallable.Currently the only supported configuration isusage, which specifies
the string that will represent this parameter in usage information.(Doesn't modifycallablein any way.)Appeal.main(args=None)Processes a command-line and calls your command functions.
Stops at the first failure result and passes it in tosys.exit().
Catches usage errors; if it catches one, displays usage information.
The implementation callsAppeal.process().Appeal.process(args=None)Processes a command-line and calls your command functions.
Stops at the first failure result and returns that result.
Doesn't catch any errors.  Useful mainly for automation,
particularly for testing, and as the main driver underlyingAppeal.main().Appeal.default_options()Appeal.default_long_option()Appeal.default_short_option()These functions create the default options for a keyword-only
parameter.  They're all valid callbacks for thedefault_optionsparameter for theAppeal()constructor.Appeal.default_options()is the default value for that parameter.Appeal.default_long_option()creates the option--{modified_parameter_name}with the default annotation and default value.modified_parameter_nameisparameter_name.lower().replace('_', '-').Appeal.default_short_option()creates the option-{parameter_name[0]}with the default annotation and default value.Appeal.default_options()creates both.In all three cases, if the function isn't able to map at least one option,
it raises anAppealConfigurationError.Notes on the default option semantics:WhenAppeal.default_option()converts a keyword-only parameter
into a long option and a short option, Appeal copies off the first
character as the short option, andthenruns a conversion function
on the string.  The conversion function lowercases the string and
converts underscores into dashes.  So for the the keyword-only
parameterDefine,Appeal.default_option()would (attempt to) create the two options-Dand--define.
For the keyword-only parameterblock_type, it would attempt to
create-band--block-type.What if you have multiple keyword-only parameters that have
the same first letter?  Only the first mapping succeeds.
So if you usedef myoptions(*, block_type=None, bad_block=None)as an Appeal command,-bwill map toblock_type.  If you
want it to map tobad_block, just swap the two keyword-only
parameters sobad_blockis first, or explicitly define your
options by decorating your function withAppeal.option().
(As of some recent version, Python guarantees it will maintain
the order of keyword-only parameters when introspecting a
function--and it was accidentally true in every version of
Python before that explicit guarantee anyway.)AppealConfigurationErrorAn exception.
Raised when the Appeal API is used improperly.AppealUsageErrorAn exception.
Raised when Appeal processes an invalid command-line.
Caught byAppeal.main(), which uses it to print usage
information and return an error.AppealCommandErrorAn exception.
Raised when an Appeal command function returns a
result indicating an error.  (Equivalent toSystemExit.)
Caught byAppeal.main(), which uses it to print usage
information and return an error.ReferenceThe library inspects the parameters of your function and uses
those for the arguments, options, and opargs of your subcommand:Positional-only and positional-or-keyword parameters
(parameters before*,or*args,) map to positional
arguments.  This:@app.command()deffgrep(pattern,file,file2=None):...would take two required command-line arguments, "pattern"
and "file", and an optional third command-line argument "file2".Keyword-only parameters map to options.  They must have a default
value.  The name of the
parameter is the name of the option, e.g. this subcommand
accepts a--verboseargument:@app.command()deffoo(*,verbose=False):...If an argument to your function has an annotation, that
value is called to convert the string from the command-line
before passing in to your function.  e.g.@app.command()deffoo(level:int):...would callinton the string from the command-line before
passing it in to level.If a parameter to your function doesn't have an annotation,
butdoeshave a default value, it behaves as if you added
an annotation oftype(default_value).  e.g.@app.command()deffoo(level=0):...would also callinton the string from the command-line before
passing it in tolevel.Keyword-only parameters with aboolannotation or a boolean
default value are special: they don't take an argument.  Instead,
they toggle the default value.Parameters with a default value ofNoneand no annotation
are also slightly special, in that they take astrargument
(as taking aNoneTypeargument doesn't make sense).Appeal automatically adds single-letter options for keyword-only
parameters when possible.  Since keyword-only parameters maintain
their order in Python*++*, Appeal gives the single-letter shortcut to
the first parameter that starts with that letter.  e.g.@app.command()deffoo(*,verbose=False,varigated=0):...-vwould map to--verbose, not--varigated.Putting it all together: if you wanted to write an "fgrep" subcommand
with a "usage" string like this:fgrep [-v|--verbose] [--level <int>] pattern [ file1 [ file2 ... ] ]you'd write it as follows:@app.command()deffgrep(pattern,*file,verbose=False,level=0):...++This is now guaranteed behavior in current Python, and even
in the Python 3 series before that, it was always true anyway.Appeal And POSIX Utility SemanticsThe POSIX standard defines command-line behavior for all POSIX
utility commands, in 1003.1, Chapter 12, currently at revision POSIX.1-2017:https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.htmlAppeal isn't a perfect match for POSIX semantics; it disallows some
things POSIX allows, and allows some things POSIX disallows.As per required POSIX semantics (1003.1-2017, Chapter 12),
options can never be required.  It therefore follows that
in Appeal, keyword arguments to command functions must
always have a default.The POSIX standard makes no mention of "long options",
so it's not clear whether or not the standard permits them.
(Presumably they will be permitted in a future standard.)POSIX requires that options that accept/require multiple opargs
should accept them as a single string with either spaces
or commas separating the opargs.  Appeal supports this behavior
withappeal.split.  But it also permits options that consume
multiple separate opargs from the command-line.POSIX requires that all options be specified before any positional
arguments.  Appeal doesn't enforce this, and will happily consume
options and positional arguments in any order.  In fact,
"subcommands" require permitting options after positional arguments
for anything beyond the simplest possible subcommand support.POSIX requires that, if an option (short option) has a singleoptionalargument (oparg), its argument must be concatenated
to the option.  For example, if-ftakes an optional argument,
and you want to specify the argumentguava, youmustwrite
this as-fguava, no other spelling is permissible.  While Appeal
supports this spelling, it also supports-f=guavaand-f guava.
More importantly, if you specify-fon your command-line (and
not-f=<something>or-f<something>), Appealwillconsume the
next argument on the command-line as an oparg, which is what POSIX
definitely doesnotwant.  I feel Appeal's consistency is
more important than supporting this syntactic hack.  Note that
the oparg is still optional, so if-fis the last thing
on your command-line, that will achieve this "option with
default value" behavior.Additional Subtle Features And BehaviorsYou can specify options and arguments in any order on a
command-line, Appeal doesn't care.  If you want Appeal to
stop recognizing arguments starting with dashes as options,
specify--(two dashes with nothing else).  All subsequent
strings on the command-line will be used as arguments, even
if they start with a-.Many built-in types are not introspectable.  If you callinspect.signature(int)it throws aValueError.  Appeal
has special-cased exactly five built-in types:bool,int,str,complex, andfloat.Accumulatoractually allows parameterizing multiple types,
separated by commas.Accumulator[int, float]will take
two opargs each time the option is specified, and the first
will be anintand the second will be afloat.  The
list returned will contain tuples of ints and floats.You can't callmain()on an Appeal object more than once.
TheAppeal()instance you use has internal state that changes
when you execute itsmain()method.Information about a particular converter is localized to
a particularAppeal()instance.  If you decorate a converter
with@app.option(), every place inside thatAppeal()instance
that you use that converter will also pick up the changes you
made with@app.option().You shouldn't callusage()until you've added all the
commands, options, and parameters information into your
Appeal object.  Why?  Because, for example,usage()computes the default options for keyword-only parameters
that haven't gotten any explicitly defined options.
But if you then define one of those options, Appeal will
throw an error at you.Almost any callable can be a converter--but noteveryfunction.  There are two limitations.  First, as already
mentioned, in order for a function to be a legal converter,
every keyword-only parameter must have a default value.
The second requirement is more specific: in order to use
a function as a converter for a*args*parameter,somewherein the annotations tree under that function,
some function must take a required positional parameter.Finally, the UNIXmakecommand has an interesting
and subtle behavior.  The--jobsand-joptions tomakespecify how many jobs to run in parallel.  If you runmakewithout any parameters, it runs one job at a time.
If you runmake -j 5, it runs five jobs at a time.  But!
If you specifymake -j, where-jis the last thing on the
command-line it runsas many jobs at a time as it wants.
In a way, the-joption hastwo default values.Can you do this with Appeal?  Naturally!  Simply specify
your keyword-only parameter with both an annotation and
a default value, then design the annotation function
to take one argument thatalsohas a default value.
Like so:defjobs(jobs:int=math.inf):returnjobs@app.command()defmake(*targets,jobs:jobs=1):...Restrictions on Appeal command functions:You may not useinspect.Parameter.emptyas a default value
for any keyword-only parameter to a converter or command function.The converter for avar_positional(*args) parametermustrequire at least one positional argument.Changelog0.6.22023/10/12Presentation change: if you run a program without arguments,
runs no-argumenthelpinstead ofusage.  This prints out
both usage information and a list of commands, which seems more
useful.  That's how most modern programs do it (e.g.git,hg).Minor API change: renamed Appeal's custom exceptions, to
remove the wordAppeal.  So, for example,AppealUsageErroris now simplyUsageError.  I added aliases so the old names
still work; I'll remove them eventually, but I promise to
keep the old names around for at least one year.Fixed usage generation, added tests.Fixed a bug where using a "simple type" (e.g. bool, float)
as an annotation for an option would cause Appeal help to
raise an exception.  Fixes #15.0.6.12023/07/22Fixed 3.6 and 3.7 support--some equals-sign-in-f-strings
uses crept in.Added GitHub Actions integration.  Tests and
coverage are run in the cloud after every checkin.
Thanks toDan Popefor gently walking me through this!Fixed metadata in thepyproject.tomlfile.Added badges for testing and supported Python
versions.  (Didn't add the coverage badge yet...
it's too embarassing!)0.62023/07/20A huge upgrade!A new feature: Appeal can now read configuration files!
Check out the new APIsAppeal.read_mapping,Appeal.read_iterable, and evenAppeal.read_csv.
This was a massive undertaking and involved a big
overhaul of the compiler.The biggest change to existing behavior: Appeal now
early-maps options.  (See issue #3.)
In short: when options are only defined in an optional group,
they get provisionally mapped (made available)beforethe first
argument in that group.  Using that option enters the group just
like specifying the first argument in that group.  You'll see
the difference in usage; an optional group that mapped an option
used to look like[a [-v|--verbose] b c]but now looks like[[-v|--verbose] a b c].Appeal now handles multiple short options smashed together
(e.g.-ace)identicallyto them being specified separately
(e.g.-a -c -e).  This caused an observable change in behavior
regarding when child options get unmapped.Appeal only permits using child options in a limited context:
it must be after the parent option is executed, it
must be after the parent option has consumed all its requiredor optionalopargs, and it must be before any top-level
positional argument or option mapped before the parent option
was executed.  But Appeal was lax about enforcing these rules
when using multiple short options smashed together (e.g.-ace);
it would handle all the options andthenunmap child options
as needed.  The good news: Appeal now enforces these rules here
too.  (The old behavior seems to have beenintentionalon my
part--what was Ithinking?!)The usage message raised for an unknown option is now much
better.  If the option is defined anywhere in the program
being run, it prints a different message telling you it
can't be used here, but also tells you where it can be used.
For example, if you use option-x, but that's a child
option mapped by--parent, the message would say-x can't be used here, it must be used immediately after --parent.RenamedAppeal.argumenttoAppeal.parameter.
This was one of those "what was Ithinking?"moments.
The function affects the parameter, not the argument.
The old name still works but will be removed before 1.0.short_option_concatenated_opargis now more strictly
enforced: it's only permitted for short options that haveexactly oneoptionaloparg, as specified by POSIX.0.5.9Improved the error message generated when you have a
required parameter after aVAR_POSITIONALparameter.
(This command-line can never succeed, because theVAR_POSITIONALconsumes all remaining arguments on
the command-line, which means the subsequent required
parameter can never be satisfied.)  Fixes #6.Changed README to use absolute instead of relative links
for images, which means images should now render properly
on the Appeal page at PyPI.  Thanks for the PR, Hugo!Switched a bunch of internal classes to use Python "slots".
Hopefully a tiny memory and speed optimization.Remove unused / unneeded stuff:unusedpartial_replace*functionsunused Charm bytecode instructionsjump_relativeandload_o_optionunneeded CharmInterpreter registeroption(only used in one error
message, obviated by instead using the program name directly)Now that Appeal depends onbiganyway, switch tobig'sPushbackIterator..Add peephole optimizer step for jump-to-jump optimization.
Honestly this was probably unnecessary, as I don't think Appeal's
compiler can evengeneratecode with jumps-to-jumps (yet).0.5.8Fixed up the "name" of the program for options.  We used to take
the name of the command, add all the option strings, and join it
together with commas, as in'command, -o, --option'.  Now it
looks like'command -o | --option'.Fixed presentation bug: if you didn't have enough positional
arguments for your command function, but you ever invoked an
option on the command-line, the usage text would contain the
name of the last option invoked (aka the last Charm program run).
Added a regression test for this.Cleaned up implementation a little: instead of using mystery
lists on theCharmInterpreterstack and context_stack, I now
use instances of bespokeCharmStackEntryandCharmContextStackEntryclasses.0.5.7Rewrite the technology behindaccumulator[...]andmapping[...].  It previously usedexec(), which was
limiting; for example, you couldn't use your own types
or converters.  The new implementation should be much
more robust; it now manually defines an explicit signature
for theoption()method of the subclass it creates.This fixed a regression, where you couldn't use a locally-defined
class (e.g.IntFloat) as one of the types in the square
brackets foraccumulatorandmapping.  Added a test
for this.0.5.6Fix formatting for usage when you have a
global commandandsubcommands.0.5.5Add support fortyping.Annotated, new in Python 3.9.Add dependency tomybiglibrary.This gives Appeal a much better implementation ofmultisplit,
and I plan to switch to thebigword wrapper and columnizer
functions, which are a... "big" improvement over what's in
Appeal right now.RenameSingleOptionto justOption.  (The nameSingleOptionis now deprecated, but I'll leave it
as a redundant name forOption... for now.)0.5.3Fix compatibility back to Python 3.6.0.5.2Fix compatibility with Python 3.11.  Python'sinspect.Parameterobject
no longer allows anamethat happens to be a keyword, which was a minor
inconvenience (Appeal used to uselambdahere sometimes).0.5.1Fixed regression, issue #5.  If you didn't supply enough required
parameters, you'd get aTypeErrorinstead of a proper usage error.0.5Initial release!