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 amain
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 ofprint()
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 theSimulator
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 calledmain()
. 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 staticmain
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:
casino.common
Any revisions to the unit tests required to reflect the new organization.
Unit tests for the three
main()
functions.