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

Git program defining –version argument.
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

Usage of the abstracted Git program.
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

executablePath

Exactly that path. It is used as given, and has to exist.

binaryDirectoryPath

That directory, joined with the platform’s name from _executableNames.

neither

The platform’s name, resolved through PATH with shutil.which().

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.