face¶
face is a Pythonic microframework for building command-line applications:
- First-class subcommand support
- Powerful middleware architecture
- Separate Parser layer
- Built-in flagfile support
- Handy testing utilities
- Themeable help display
Installation¶
face is pure Python, and tested on Python 3.7+, as well as PyPy. Installation is easy:
pip install face
Then get to building your first application!
from face import Command, echo
def hello_world():
"A simple greeting command."
echo('Hello, world!')
cmd = Command(hello_world)
cmd.run()
"""
# Here's what the default help looks like at the command-line:
$ cmd --help
Usage: cmd [FLAGS]
A simple greeting command.
Flags:
--help / -h show this help message and exit
"""
Getting Started¶
Check out our Tutorial for more.
Tutorial¶
Contents
Part I: Say¶
The field of overdone versions of echo
has been too long dominated
by Big GNU.
Today, we start taking back the power.
We will implement the say
command.
Positional arguments¶
While face offers a Parser interface underneath, the canonical way to create even the simplest CLI is with the Command object.
To demonstrate, we’ll start with the basics, positional arguments.
say hello world
should print hello world
:
from face import Command, echo
def main():
cmd = Command(say, posargs=True) # posargs=True means we accept positional arguments
cmd.run()
def say(posargs_): # positional arguments are passed through the posargs_ parameter
echo(' '.join(posargs_)) # our business logic
if __name__ == '__main__': # standard fare Python: https://stackoverflow.com/questions/419163
main()
A basic Command takes a single function entrypoint, in our case, the
say
function.
Note
Face’s echo()
function is a version of print()
with
improved options and handling of console states, ideal for CLIs.
Flags¶
Let’s give say
some options:
say --upper hello world
or
say -U hello world
should print
HELLO WORLD
.
...
def main():
cmd = Command(say, posargs=True)
cmd.add('--upper', char='-U', parse_as=True, doc='uppercase all output')
cmd.run()
def say(posargs_, upper): # our --upper flag is bound to the upper parameter
args = posargs_
if upper:
args = [a.upper() for a in args]
echo(' '.join(args))
...
The parse_as
keyword argument being set to True
means that the
presence of the flag results in the True
value itself. As we’ll
see below, flags can take arguments, too.
Flags with values¶
Let’s add more flags, this time ones that take values.
say --separator . hello world
will print hello.world
.
Likewise,
say --count 2 hello world
will repeat it twice:
hello world hello world
...
def main():
cmd = Command(say, posargs=True)
cmd.add('--upper', char='-U', parse_as=True, doc='uppercase all output')
cmd.add('--separator', missing=' ', doc='text to put between arguments')
cmd.add('--count', parse_as=int, missing=1, doc='how many times to repeat')
cmd.run()
def say(posargs_, upper, separator, count):
args = posargs_ * count
if upper:
args = [a.upper() for a in args]
echo(separator.join(args))
...
Now we can see that parse_as
:
- Can take a value (e.g.,
True
), which make the flag no-argument- Can take a callable (e.g.,
int
), which is used to convert the single argument- Defaults to
str
(as used byseparator
)
We can also see the missing
keyword argument, which specifies the
value to be passed to the Command’s handler function if the flag is
absent. Without this, None
is passed.
Note
Face also supports required flags, though they are not an ideal CLI
UX best practice. Simply set missing
to face.ERROR
.
More Interesting Flag Types¶
say --multi-separator=@,# hello wonderful world
prints
hello@wonderful#world
(The separators repeat)
say --from-file=fname
reads the file and adds all words from it to its
output
say --animal=dog|cat|cow
will prepend “woof”, “meow”, or “moo” respectively.
Part II: Calc¶
(Details TBD!)
With echo
having met its match,
we are on to bigger and better:
this time,
with math
$ num
<Big help text>
Precision support¶
$ num add 0.1 0.2
0.30000000000000004
$ num add --precision=3 0.1 0.2
0.3
Oh, now let’s add it to all subcommands.
Part III: Middleware¶
(Details TBD!)
Doing math locally is all well and good, but sometimes we need to use the web.
We will add an “expression” sub-command
to num that uses https://api.mathjs.org/v4/
.
But since we want to unit test it,
we will create the httpx.Client
in a middleware.
$ num expression "1 + (2 * 3)"
7
But we can also write a unit test that does not touch the web:
$ pytest test_num.py
Part IV: Examples¶
There are more realistic examples of face usage out there, that can serve as a reference.
Cut MP4¶
The script
cut_mp4
is a quick but useful tool to cut recordings using
ffmpeg
.
I use it to slice and dice the Python meetup recordings.
It does not have subcommands or middleware,
just a few flags.
Glom¶
Glom
is a command-line interface front end for the glom
library.
It does not have any subcommands,
but does have some middleware usage.
Pocket Protector¶
Pocket Protector is a secrets management tool. It is a medium-sized application with quite a few subcommands for manipulating a YAML file.
Montage Admin Tools¶
Montage Admin Tools is a larger application. It has nested subcommands and a database connection. It is used to administer a web application.
Command¶
-
class
face.
Command
(func, name=None, doc=None, **kwargs)[source]¶ The central type in the face framework. Instantiate a Command, populate it with flags and subcommands, and then call command.run() to execute your CLI.
Note that only the first three constructor arguments are positional, the rest are keyword-only.
Parameters: - func (callable) – The function called when this command is run with an argv that contains no subcommands.
- name (str) – The name of this command, used when this command is included as a subcommand. (Defaults to name of function)
- doc (str) – A description or message that appears in various help outputs.
- flags (list) – A list of Flag instances to initialize the Command with. Flags can always be added later with the .add() method.
- posargs (bool) – Pass True if the command takes positional arguments. Defaults to False. Can also pass a PosArgSpec instance.
- post_posargs (bool) – Pass True if the command takes additional positional arguments after a conventional ‘–’ specifier.
- help (bool) – Pass False to disable the automatically added –help flag. Defaults to True. Also accepts a HelpHandler instance, see those docs for more details.
- middlewares (list) – A list of @face_middleware decorated callables which participate in dispatch. Also addable via the .add() method. See Middleware docs for more details.
-
add
(*a, **kw)[source]¶ Add a flag, subcommand, or middleware to this Command.
If the first argument is a callable, this method contructs a Command from it and the remaining arguments, all of which are optional. See the Command docs for for full details on names and defaults.
If the first argument is a string, this method constructs a Flag from that flag string and the rest of the method arguments, all of which are optional. See the Flag docs for more options.
If the argument is already an instance of Flag or Command, an exception is only raised on conflicting subcommands and flags. See add_command for details.
Middleware is only added if it is already decorated with @face_middleware. Use .add_middleware() for automatic wrapping of callables.
-
add_command
(subcmd)[source]¶ Add a Command, and all of its subcommands, as a subcommand of this Command.
Middleware from the current command is layered on top of the subcommand’s. An exception may be raised if there are conflicting middlewares or subcommand names.
-
add_middleware
(mw)[source]¶ Add a single middleware to this command. Outermost middleware should be added first. Remember: first added, first called.
-
get_dep_names
(path=())[source]¶ Get a list of the names of all required arguments of a command (and any associated middleware).
By specifying path, the same can be done for any subcommand.
-
get_flag_map
(path=(), with_hidden=True)[source]¶ Command’s get_flag_map differs from Parser’s in that it filters the flag map to just the flags used by the endpoint at the associated subcommand path.
-
prepare
(paths=None)[source]¶ Compile and validate one or more subcommands to ensure all dependencies are met. Call this once all flags, subcommands, and middlewares have been added (using .add()).
This method is automatically called by .run() method, but it only does so for the specific subcommand being invoked. More conscientious users may want to call this method with no arguments to validate that all subcommands are ready for execution.
-
run
(argv=None, extras=None, print_error=None)[source]¶ Parses arguments and dispatches to the appropriate subcommand handler. If there is a parse error due to invalid user input, an error is printed and a CommandLineError is raised. If not caught, a CommandLineError will exit the process, typically with status code 1. Also handles dispatching to the appropriate HelpHandler, if configured.
Defaults to handling the arguments on the command line (
sys.argv
), but can also be explicitly passed arguments via the argv parameter.Parameters: - argv (list) – A sequence of strings representing the
command-line arguments. Defaults to
sys.argv
. - extras (dict) – A map of additional arguments to be made available to the subcommand’s handler function.
- print_error (callable) – The function that formats/prints error messages before program exit on CLI errors.
- argv (list) – A sequence of strings representing the
command-line arguments. Defaults to
Command Exception Types¶
In addition to all the Parser-layer exceptions, a command or user endpoint function can raise:
-
class
face.
CommandLineError
(msg, code=1)[source]¶ A
FaceException
andSystemExit
subtype that enables safely catching runtime errors that would otherwise cause the process to exit.If instances of this exception are left uncaught, they will exit the process.
If raised from a
run()
call andprint_error
is True, face will print the error before reraising. Seeface.Command.run()
for more details.
-
class
face.
UsageError
(msg, code=1)[source]¶ Application developers should raise this
CommandLineError
subtype to indicate to users that the application user has used the command incorrectly.Instead of printing an ugly stack trace, Face will print a readable error message of your choosing, then exit with a nonzero exit code.
Middleware¶
Coming soon!
- Dependency injection (like pytest!)
- autodoc * inventory of all production-grade middlewares
Testing¶
Face provides a full-featured test client for maintaining high-quality command-line applications.
-
class
face.
CommandChecker
(cmd, env=None, chdir=None, mix_stderr=False, reraise=False)[source]¶ Face’s main testing interface.
Wrap your
Command
instance in aCommandChecker
,run()
commands with arguments, and getRunResult
objects to validate your Command’s behavior.Parameters: - cmd – The
Command
instance to test. - env (dict) – An optional base environment to use for subsequent
calls issued through this checker. Defaults to
{}
. - chdir (str) – A default path to execute this checker’s commands in. Great for temporary directories to ensure test isolation.
- mix_stderr (bool) – Set to
True
to capture stderr into stdout. This makes it easier to verify order of standard output and errors. IfTrue
, this checker’s results’ error_bytes will be set toNone
. Defaults toFalse
. - reraise (bool) – Reraise uncaught exceptions from within cmd’s
endpoint functions, instead of returning a
RunResult
instance. Defaults toFalse
.
-
run
(args, input=None, env=None, chdir=None, exit_code=0)[source]¶ The
run()
method acts as the primary entrypoint to theCommandChecker
instance. Pass arguments as a list or string, and receive aRunResult
with which to verify your command’s output.If the arguments do not result in an expected exit_code, a
CheckError
will be raised.Parameters: - args – A list or string representing arguments, as one might
find in
sys.argv
or at the command line. - input (str) – A string (or list of lines) to be passed to
the command’s stdin. Used for testing
prompt()
interactions, among others. - env (dict) – A mapping of environment variables to apply on
top of the
CommandChecker
’s base env vars. - chdir (str) – A string (or stringifiable path) path to
switch to before running the command. Defaults to
None
(runs in current directory). - exit_code (int) – An integer or list of integer exit codes
expected from running the command with args. If the
actual exit code does not match exit_code,
CheckError
is raised. Set toNone
to disable this behavior and always returnRunResult
. Defaults to0
.
Note
At this time,
run()
interacts with global process state, and is not designed for parallel usage.- args – A list or string representing arguments, as one might
find in
-
fail
(*a, **kw)[source]¶ Convenience method around
run()
, with the same signature, except that this will raise aCheckError
if the command completes with exit code0
.
-
fail_X
()¶ Test that a command fails with exit code
X
, whereX
is an integer.For testing convenience, any method of pattern
fail_X()
is the equivalent tofail(exit_code=X)
, andfail_X_Y()
is equivalent tofail(exit_code=[X, Y])
, providingX
andY
are integers.
- cmd – The
-
class
face.testing.
RunResult
(args, input, exit_code, stdout_bytes, stderr_bytes, exc_info=None, checker=None)[source]¶ Returned from
CommandChecker.run()
, complete with the relevant inputs and outputs of the run.Instances of this object are especially valuable for verifying expected output via the
stdout
andstderr
attributes.API modeled after
subprocess.CompletedProcess
for familiarity and porting of tests.-
input
¶ The string input passed to the command, if any.
-
exit_code
¶ The integer exit code returned by the command.
0
conventionally indicates success.
-
stdout
¶ The text output (“stdout”) of the command, as a decoded string. See
stdout_bytes
for the bytestring.
-
stderr
¶ The error output (“stderr”) of the command, as a decoded string. See
stderr_bytes
for the bytestring. May beNone
if mix_stderr was set toTrue
in theCommandChecker
.
-
stdout_bytes
¶ The output (“stdout”) of the command, as an encoded bytestring. See
stdout
for the decoded text.
-
stderr_bytes
¶ The error output (“stderr”) of the command, as an encoded bytestring. See
stderr
for the decoded text. May beNone
if mix_stderr was set toTrue
in theCommandChecker
.
-
exc_info
¶ A 3-tuple of the internal exception, in the same fashion as
sys.exc_info()
, representing the captured uncaught exception raised by the command function from aCommandChecker
with reraise set toTrue
. For advanced use only.
-
exception
¶ Exception instance, if an uncaught error was raised. Equivalent to
run_res.exc_info[1]
, but more readable.
-
-
exception
face.testing.
CheckError
(result, exit_codes)[source]¶ Rarely raised directly,
CheckError
is automatically raised when aCommandChecker.run()
call does not terminate with an expected error code.This error attempts to format the stdout, stderr, and stdin of the run for easier debugging.
Input / Output¶
Face includes a variety of utilities designed to make it easy to write applications that adhere to command-line conventions and user expectations.
-
face.
echo
(msg, **kw)[source]¶ A better-behaved
print()
function for command-line applications.Writes text or bytes to a file or stream and flushes. Seamlessly handles stripping ANSI color codes when the output file is not a TTY.
>>> echo('test') test
Parameters: - msg (str) – A text or byte string to echo.
- err (bool) – Set the default output file to
sys.stderr
- file (file) – Stream or other file-like object to output
to. Defaults to
sys.stdout
, orsys.stderr
if err is True. - nl (bool) – If
True
, sets end to'\n'
, the newline character. - end (str) – Explicitly set the line-ending character. Setting this overrides nl.
- color (bool) – Set to
True
/False
to always/never echo ANSI color codes. Defaults to inspecting whether file is a TTY.
-
face.
echo_err
(*a, **kw)[source]¶ A convenience function which works exactly like
echo()
, but always defaults the output file tosys.stderr
.
-
face.
prompt
(label, confirm=None, confirm_label=None, hide_input=False, err=False)[source]¶ A better-behaved
input()
function for command-line applications.Ask a user for input, confirming if necessary, returns a text string. Handles Ctrl-C and EOF more gracefully than Python’s built-ins.
Parameters: - label (str) – The prompt to display to the user.
- confirm (bool) – Pass
True
to ask the user to retype the input to confirm it. Defaults to False, unless confirm_label is passed. - confirm_label (str) – Override the confirmation prompt. Defaults
to “Retype label” if confirm is
True
. - hide_input (bool) – If
True
, disables echoing the user’s input as they type. Useful for passwords and other secret entry. Seeprompt_secret()
for a more convenient interface. Defaults toFalse
. - err (bool) – If
True
, prompts are printed onsys.stderr
. Defaults toFalse
.
prompt()
is primarily intended for simple plaintext entry. Seeprompt_secret()
for handling passwords and other secret user input.Raises
UsageError
if confirm is enabled and inputs do not match.
-
face.
prompt_secret
(label, **kw)[source]¶ A convenience function around
prompt()
, which is preconfigured for secret user input, like passwords.All arguments are the same, except hide_input is always
True
, and err defaults toTrue
, for consistency withgetpass.getpass()
.
TODO¶
- TODO: InputCancelled exception, to be handled by .run()
- TODO: stuff for prompting choices
- TODO: pre-made –color flag(s) (looks at isatty)
Face FAQs¶
TODO
What sets Face apart from other CLI libraries?¶
In the Python world, you certainly have a lot of choices among argument parsers. Software isn’t a competition, but there are many good reasons to choose face.
- Rich dependency semantics guarantee that endpoints and their dependencies line up before the Command will build to start up.
- Streamlined, Pythonic API.
- Handy testing tools
- Focus on CLI UX (arg order, discouraging required flag)
- TODO: contrast with argparse, optparse, click, etc.
Why is Face so picky about argument order?¶
In short, command-line user experience and history hygiene. While it’s easy for us to be tempted to add flags to the ends of commands, anyone reading that command later is going to suffer:
cmd subcmd posarg1 --flag arg posarg2
Does posarg2
look more like a positional argument or an argument
of --flag
?
This is also why Face doesn’t allow non-leaf commands to accept positional arguments (is it a subcommand or an argument?), or flags which support more than one whitespace-separated argument.
Any recommended patterns for laying out CLI code?¶
- Dedicated cli.py which constructs commands.
- main function should take argv as an argument
if __name__ == '__main__': main(sys.argv)
- Entrypoints are nicer than
-m