Program
The Program represents an executable command line program. It offers an interface to
define and enable command line arguments.
Features:
Abstract a command line program as a Python class.
Abstract arguments of that program as nested classes derived from pre-defined Argument classes.
See Arguments.Construct a list of arguments in correct order, ready to be used with e.g.
subprocess.
Simple Example
The following example implements a portion of the git program and its --version argument.
Program Definition
class Git(Program):
_executableNames: ClassVar[Dict[str, str]] = {
"Darwin": "git",
"FreeBSD": "git",
"Linux": "git",
"Windows": "git.exe"
}
@CLIArgument()
class FlagVersion(LongFlag, name="version"):
"""Print the version information."""
Program Usage
git = Git()
git[git.FlagVersion] = True
print(git.ToArgumentList()) # ['/usr/bin/git', '--version']
print(git) # "/usr/bin/git" "--version"
Setting Program Names based on OS
The same program is spelled differently per operating system - git and git.exe - so the name isn’t a single
string but a mapping from platform to name, declared as the class variable _executableNames. The keys are the
values platform.system() returns, and the constructor looks the current one up.
class Git(Program):
_executableNames: ClassVar[Dict[str, str]] = {
"Darwin": "git",
"FreeBSD": "git",
"Linux": "git",
"Windows": "git.exe"
}
A platform the mapping doesn’t name raises CLIAbstractionError, chained from a
PlatformNotSupportedError - so this program doesn’t run here is reported when the
object is constructed rather than when it is started.
Where the executable is looked for depends on what the constructor was given:
Constructor parameter |
Where the executable comes from |
|---|---|
|
Exactly that path. It is used as given, and has to exist. |
|
That directory, joined with the platform’s name from |
neither |
The platform’s name, resolved through |
A missing executable raises CLIAbstractionError chained from
FileNotFoundError. With dryRun=True the check is logged and skipped instead, so a command line can be
assembled and printed on a machine that doesn’t have the program installed.
Defining Arguments on a Program
An argument is declared as a nested class of the program, derived from one of the argument classes in
Arguments, and marked with the CLIArgument attribute.
class Git(Program):
@CLIArgument()
class FlagVersion(LongFlag, name="version"):
"""Print the version information."""
Three things happen there, and each is deliberate:
The nesting is the scope. The class is defined inside the program it belongs to, so an argument cannot be set on a program that doesn’t declare it, and reading the class body is reading that program’s command line interface.
The decorator makes it findable. CLIArgument is a
pyTooling attribute. When a subclass of Program is created, its
__init_subclass__ asks the attribute for every marked class in this class’ scope and records them in
__cliOptions__. A nested class without the decorator is an ordinary nested class and no argument.
The class-argument carries the spelling. name="version" is a class keyword argument, read by the argument
base-class’ own __init_subclass__ and combined with that class’ pattern - --{0} for a
LongFlag - so the class knows how to render itself as --version.
Hint
__cliOptions__ maps each argument class to the position it was declared at, and that number is the sort
key ToArgumentList() uses. The order arguments appear on the command line
is therefore the order they are declared in the class body, not the order they were set in.
CLIArgument
CLIArgument is an Attribute and carries no data of
its own - it marks a nested class as this program’s argument. Marking is all it does; the collection happens once,
when the program class is created, and costs nothing per instance.
Setting Arguments on a Program
A program instance behaves like a dictionary whose keys are the argument classes:
git = Git()
git[git.FlagVersion] = True # a flag: the value is ignored
git[git.ValuedOption] = "some value" # an argument carrying a value
The key is the class, not a string, so a misspelled option is caught by the same tools that catch a misspelled attribute. Two mistakes raise instead of being accepted:
an argument class the program doesn’t declare -
KeyError, naming the program;an argument that was already set -
KeyError, because setting it twice is a bug rather than an override.
Whether the assigned value is used depends on the argument class.
_NeedsParameterInitialization() decides it: a valued argument is constructed
with the value, a flag is constructed without one. That is why True above is not a value but a way of saying
present.
Reading the item back returns the argument object, whose
Value can be changed afterwards, so a program can be built
once and re-run with a different value:
git[git.ValuedOption].Value = "another value"
ToArgumentList() renders the whole thing - the executable’s path first, then
every set argument in declaration order - as the list subprocess expects. repr() and str() give the
same list quoted for reading.
Derive Program Variants
A program class can be derived like any other class, and the derived class inherits nothing of the arguments:
__init_subclass__ collects the nested classes in its own scope, so a variant declares the arguments it
supports, including re-declaring the ones it shares.
A configured variant of a configured instance is derived by a method of the program: it creates a new instance, copies
every argument set on this one with _CopyParameters(), then sets the arguments
it is given explicitly - see GetCommitTool in the overview’s example. An argument the new
instance’s class doesn’t declare raises KeyError.