Command-Line Interface (CLI)

In Overall Simulation Control we talked about the overall Simulation class – the component that ran the entire application, creating objects, running the simulation, gathering the data, writing a log, and saving files with the interesting, useful results.

We suggested a class definition, but never followed through to the OS-level interface for the application as a whole. There are several ways an application like this can be packaged, we’ll look at how to make a final command-line interface (CLI) for this simulation application.

In CLI Analysis, we’ll look at the general problem of the CLI, to decide what it should do and how it supports the rest of the application.

There will be a few design decisions. In Design Decision – CLI Library we’ll address the choice of libraries. In Design Decision – Class vs. Function we’ll decide if this should be a kind of :strong:Singleton class or a simpler collection of functions.

In CLI Design we’ll describe the final design of the CLI. In CLI Deliverables we’ll enumerate the components to be built and tested.

CLI Analysis

The CLI will be part of the overall user story described in Foundations. In Our Simulation Application we described a hypothetical command for running the simulation:

python3 -m casino.craps --Dplayer.name="Player1326" >details.log

This shows a number of features that drive the design:

  • The app runs from the OS terminal prompt. It uses POSIX command-line arguments. (It’s possible to use the Python CLI libraries to create Windows-like commands, also.)

  • The app is a package, casino, with a sub-package, craps, that contains a main function that does the real work.

  • The detailed output was written to standard output. A shell redirect (>details.log) was used to capture the output into a file for further analysis.

This concept has some room for improvement, but it points to a number of detailed responsibilities we can use to drive the design.

We can pull out three diffient kinds of responsibilities, shown below:

  • Parse command line options and arguments. This is something we’ll delegate to a separate library. We have two choices and Design Decision – CLI Library will address the library choice.

  • Write to standard output and standard error. This is a built-in feature of the print() function. In the long run, however, we need to move away from the simple use of print() and separate output into two parts: a log to summarize what work is being done, and the final output files in CSV format, created as part of the statistical summary of the Simulator class.

  • Have a structure that permits easy access via the python -m command.

Design Decision – CLI Library

There are several popular packages for building CLI’s.

Problem. Which CLI library should we use? Two of the popular packages are the argparse package, which is part of the Python standard library, and the click library, which must be installed separately.

Forces. Both packages solve the core problems of parsing command-line options.

When we look at simplicity or convenience, the argparse package is part of the Python standard library. This makes it very handy.

The click library must be installed separately. It offers a number of handy features, and seems slightly easier to work with than argparse.

In particular, the click library works as a collection of decorators, slightly reducing the overall complexity of the main application.

Solution. We’ll recommend using click to build CLI’s for command-line applications.

Consequences. We’ll need to install click and add it to the project’s requirements.txt.

conda install click

The core use case for click looks a little bit like this example:

Example CLI module

"""
Mastering Object Oriented Design, 4ed.

A demo of the CLI.
"""

import click


@click.command()
@click.option("-p", "--player", default="Simple", help="Player Class")
def main(player):
    print(f"Player set to {player}")

if __name__ == "__main__":
    main()

The main() function here needs to build the instance of the Simulator with the proper game, players, and casino-specific table subclass.

Because each game is quite different, it’s sensible to create three separate packages, one for each game.

Design Decision – Class vs. Function

In Python, the top level of an application doesn’t have to stick closely to object-oriented programming techniques. The top-level features are often better described by separate functions.

Problem. How do we implement the OS interface? A main() function, or a class that must be instantiated, or a class with a @staticmethod that can be used to do the work?

Forces. There are a number of alternatives for structuring the top-level main program.

  • A simple script of statements. This is very, very difficult to unit test. We discourage the use of flat script-like Python modules.

  • A main() function. Often called main(). In order to be part of a top-level program, there must also be a script present in the module. This leads to a module with an overall layout like the following:

    imports
    class definitions
    
    def main():
        the real work
    
    if __name__ == "__main__":
        main()  # pragma: no cover
    
  • A class with that must be instantiated. This is nearly identical to the function example shown above. The following example shows this variation. It’s not clear that this is advantageous.

    imports
    class definitions
    
    class Main:
        def main(self):
            the real work
    
    if __name__ == "__main__":
        Main().main()  # pragma: no cover
    
  • A class with a @staticmethod. This paralles the Java concept of a static main function that’s required by the JVM. This would lead to a module with the following kind of organization.

    imports
    class definitions
    
    class Main:
        @staticmethod
        def main():
            the real work
    
    if __name__ == "__main__":
        Main.main()  # pragma: no cover
    

The two class-based alternatives don’t seem to offer material advantages. The script only creates a single instance of the class, or uses the class directly. Any class with only a single instance can be viewed as a Big Hammer solution.

Solution. The conventional approach in most Python code is a top-level function, often with a name like main(). We can apply the click decorations to this function easily and use it to define the main application.

Consequences. We’ll need to create proper unit tests for the top-level main() function. This will require using the click unit testing features to invoke the command with appropriate arguments.

CLI Design

We’ve (intentionally) provied little guidance on the structure of the modules and packages in this application. The following organization may require rework to restructure the classes into the four modules within the casino package.

The casino package is the top-level package for the entire suite of simulation components. The top-level package has no components, since its purposes is to act as a namespace for the various simulators. The package is implemented as a directory named casino with an empty __init__.py file.

A casino.common module will have the abstract superclasses for the other modules. This module is implemented as the common.py file in the casino directory. The remaining modules are similarly a simple .py file filled with class definitions, and a top-level main() function.

The casino.craps module with the complete definition for Craps. The module will import common definitions from the casino.common module.

The casino.roulette module with the complete definition for Roulette. The module will import common definitions from the casino.common module.

Similar to the above modules, a casino.blackjack module will have the complete Blackjack simulation.

casino.craps.main()

The main function for the Craps simulation. This will have @click.command() and @click.option decorators to define the various options

casino.roulette.main()

The main function for the Roulette simulation. This will have @click.command() and @click.option decorators to define the various options

casino.blackjack.main()

The main function for the Blackjack simulation. This will have @click.command() and @click.option decorators to define the various options

CLI Deliverables

The following components will be built:

  • The top-level casino module.

  • The lower-level modules:

  • Any revisions to the unit tests required to reflect the new organization.

  • Unit tests for the three main() functions.