Overview

CLIAbstraction offers an abstraction layer for command line programs, so they can be used easily in Python. There is no need for manually assembling parameter lists or considering the order of parameters. All parameters like -v or --value=42 are described as CommandLineArgument instances on a Program class. Each argument class like ShortFlag or PathArgument knows about the correct formatting pattern, character escaping, and if needed about necessary type conversions. A program instance can be converted to an argument list suitable for subprocess.Popen.

While a user-defined command line program abstraction derived from Program only takes care of maintaining and assembling parameter lists, a more advanced base-class, called Executable, is offered with embedded Popen behavior.

Design Goals

The main design goals are:

  • Offer access to CLI programs as Python classes.

  • Abstract CLI arguments (a.k.a. parameter, option, flag, …) as members on such a Python class.

  • Abstract differences in operating systems like argument pattern (POSIX: -h vs. Windows: /h), path delimiter signs (POSIX: / vs. Windows: \) or executable names.

  • Derive program variants from existing programs.

  • Assemble parameters as list for handover to subprocess.Popen with proper escaping and quoting.

  • Launch a program with Popen and hide the complexity of Popen.

  • Get a generator object for line-by-line output reading to enable postprocessing of outputs.

Example

The following example implements a portion of the git program and its commit sub-command.

  1. A new class Git is derived from pyTooling.CLIAbstraction.Executable.

  2. A class variable _executableNames is set, to specify different executable names based on the operating system.

  3. Nested classes are used to describe arguments and flags for the Git program.

  4. These nested classes are annotated with the @CLIArgument attribute, which is used to register the nested classes in an ordered lookup structure. This declaration order is also used to order arguments when converting to a list for Popen.

Usage of Git

# Create a program instance and set common parameters.
git = Git()
git[git.FlagVerbose] = True

# Derive a variant of that pre-configured program.
commit = git.getCommitTool()
commit[commit.ValueCommitMessage] = "Bumped dependencies."

# Launch the program and parse outputs line-by-line.
commit.StartProcess()
for line in commit.GetLineReader():
  print(line)

Declaration of ``Git``

from pyTooling.CLIAbstraction import Executable
from pyTooling.CLIAbstraction.Command import CommandArgument
from pyTooling.CLIAbstraction.Flag import LongFlag
from pyTooling.CLIAbstraction.ValuedTupleFlag import ShortTupleFlag

class Git(Executable):
  _executableNames: ClassVar[Dict[str, str]] = {
    "Darwin":  "git",
    "Linux":   "git",
    "Windows": "git.exe"
  }

  @CLIArgument()
  class FlagVerbose(LongFlag, name="verbose"):
    """Print verbose messages."""

  @CLIArgument()
  class CommandCommit(CommandArgument, name="commit"):
    """Command to commit staged files."""

  @CLIArgument()
  class ValueCommitMessage(ShortTupleFlag, name="m"):
    """Specify the commit message."""

  def GetCommitTool(self):
    """Derive a new program from a configured program."""
    tool = self.__class__(executablePath=self._executablePath)
    tool[tool.CommandCommit] = True
    self._CopyParameters(tool)

    return tool

Programm API

Condensed definition of class Program:

class Program(metaclass=ExtendedType, slots=True):
   # Register @CLIArgument marked nested classes in `__cliOptions__
   def __init_subclass__(cls, *args: Tuple[Any, ...], **kwargs: Dict[str, Any]):
     ...

   def __init__(self, executablePath: Path = None, binaryDirectoryPath: Path = None, dryRun: bool = False) -> None:
     ...

   @staticmethod
   def _NeedsParameterInitialization(key):
      ...

   # Implement indexed access operators: prog[...]
   def __getitem__(self, key):
      ...
   def __setitem__(self, key, value):
      ...

   @readonly
   def Path(self) -> Path:
      ...

   def ToArgumentList(self) -> List[str]:
      ...

   def __repr__(self):
      ...

   def __str__(self):
      ...

Executable API

Condensed definition of class Executable:

class Executable(Program):
   def __init__( self, executablePath: Path = None, binaryDirectoryPath: Path = None, workingDirectory: Path = None, # environment: Environment = None, dryRun: bool = False):
      ...

   def StartProcess(self):
      ...

   def Send(self, line: str, end: str="\n") -> None:
      ...

   def GetLineReader(self) -> Generator[str, None, None]:
      ...

   @readonly
   def ExitCode(self) -> int:
      ...

Consumers

This abstraction layer is used by:

  • ✅ Wrap command line interfaces of EDA tools (Electronic Design Automation) in Python classes.
    pyEDAA.CLITool