Command line programs for lazy humans.
- Decorate a function to be your programs starting point.
- Generate command line parser based on function signature.
- Search system environment for option default values.
I write a lot of small programs in Python. These programs often accept a small number of simple command line arguments. Having to write command line parsing code in each of these small programs both breaks my train of thought and greatly increases the volume of code I am writting.
Begins was implemented to remove the boilerplate code from these Python programs. It's not intended to replace the rich command line processing needed for larger applications.
For Python versions earlier than Python 3.3, the funcsigs package from the Python Package Index is required.
For Python version 2.6, the argparse package from the Python Package Index is also required.
Both of these dependencies are listed in the package configuration. If using Pip to install begins then the required dependencies will be automatically installed.
begins is available for download from the Python Package Index. To install using Pip
$ pip install begins
Alternatively, the latest development version can be installed directly from Github.
$ pip install git+https://github.com/aliles/begins.git
Please note that begins is still in an alpha state and therefore the API or behaviour could change.
The begin.start()
function can be
used as a function call
or a decorator.
If called as a function
it returns True when
called from the __main__
module.
To do this it inspects
the stack frame of the caller,
checking the __name__
global.
This allows the following Python pattern:
>>> if __name__ == '__main__': ... pass
To be replace with:
>>> import begin >>> if begin.start(): ... pass
If used as a decorator
to annotate a function
the function will be called
if defined in the __main__
module
as determined by inspecting
the current stack frame.
Any definitions that follow
the decorated function
wont be created until
after the function call
is complete.
Usage of begin.start()
as
a decorator looks like:
>>> import begin >>> @begin.start ... def run(): ... pass
By deferring the execution of the function until after the remainder of the module has loaded ensures the main function doesn't fail if depending on something defined in later code.
If begin.start()
decorates a
function accepts parameters
begin.start()
will
process the command for
options to pass as
those parameters:
>>> import begin >>> @begin.start ... def run(name='Arther', quest='Holy Grail', colour='blue', *knights): ... "tis but a scratch!"
The decorated function above will generate the following command line help:
usage: example.py [-h] [-n NAME] [-q QUEST] [-c COLOUR] [knights [knights ...]] tis but a scratch! positional arguments: knights optional arguments: -h, --help show this help message and exit -n NAME, --name NAME (default: Arther) -q QUEST, --quest QUEST (default: Holy Grail) -c COLOUR, --colour COLOUR (default: blue)
In Python3, any function annotations for a parameter become the command line option help. For example:
>>> import begin >>> @begin.start # doctest: +SKIP ... def run(name: 'What, is your name?', ... quest: 'What, is your quest?', ... colour: 'What, is your favourite colour?'): ... pass
Will generate command help like:
usage: holygrail_py3.py [-h] -n NAME -q QUEST -c COLOUR optional arguments: -h, --help show this help message and exit -n NAME, --name NAME What, is your name? -q QUEST, --quest QUEST What, is your quest? -c COLOUR, --colour COLOUR What, is your favourite colour?
Command line parsing supports:
- positional arguments
- keyword arguments
- default values
- variable length arguments
- annotations
Command line parsing
does not support
variable length keyword arguments,
commonly written as
**kwargs
.
If variable length keyword arguments
are used by
the decorated function
an exception
will be raised.
If a parameter does not have a default, failing to pass a value on the command line will cause running the program to print an error and exit.
For programs that have
a large number of options
it may be preferable to
only use long options.
To suppress short options,
pass False
as the
short_args
keyword argument to
the begin.start
decorator:
>>> import begin >>> @begin.start(short_args=False) ... def run(name='Arther', quest='Holy Grail', colour='blue', *knights): ... "tis but a scratch!"
This program will not
accept -n
, -q
or -c
as option names.
Similarity, a large number of
command line options may
be better displayed in
alphabetical order.
This can be achieved
by passing lexical_order
as True
:
>>> import begin >>> @begin.start(lexical_order=True) ... def main(charlie=3, alpha=1, beta=2): ... pass
This program will list
the command line options as
alpha
, beta
, charlie
instead of the order
in which the function
accepts them.
begins supports
using functions as
sub-commands with the
begin.subcommand()
decorator:
>>> import begin >>> @begin.subcommand # doctest: +SKIP ... def name(answer): ... "What is your name?" ... >>> @begin.subcommand # doctest: +SKIP ... def quest(answer): ... "What is your quest?" ... >>> @begin.subcommand # doctest: +SKIP ... def colour(answer): ... "What is your favourite colour?" ... >>> @begin.start ... def main(): ... pass
This example registers three sub-commands for the program:
usage: subcommands.py [-h] {colour,name,quest} ... optional arguments: -h, --help show this help message and exit Available subcommands: {colour,name,quest} colour What is your favourite colour? name What is your name? quest What is your quest?
The main function will always be called with the provided command line arguments. If a sub-command was chosen the associated function will also be called.
Sub-commands can be
registered with a
specific named group by
passing a group
argument to
the begin.subcommand
decorator.
The begin.start()
decorator can
use sub-commands from
a named group by
passing it a sub_group
argument.
Similarly, sub-commands can be
load from entry points by
passing the name
of the entry point
through the plugins
argument
to the begin.start()
decorator:
>>> import begin >>> @begin.start(plugins='begins.plugin.demo') ... def main(): ... pass
Any functions from
installed packages
that are registered with
the begins.plugin.demo
entry point
will be loaded as sub-commands.
Some commands may benefit
from being able to be called with
multiple subcommands on
the command line.
The enable multiple sub-commands
a command separator value needs
to be passed to be
passed to begin.start()
as the cmd_delim
parameter:
>>> import begin >>> @begin.subcommand # doctest: +SKIP ... def subcmd(): ... pass ... >>> @begin.start(cmd_delim='--') ... def main(): ... pass
When this program is called
from the command line
multiple instances of the
sub-command may be called
if separated by the
command delimiter --
.
Environment variables can
be used to override the
default values for
command line options.
To use environment variables
pass a prefix string to
the begin.start()
decorator through
the env_prefix
parameter:
>>> import begin >>> @begin.start(env_prefix='MP_') ... def run(name='Arther', quest='Holy Grail', colour='blue', *knights): ... "tis but a scratch!"
In the example above,
if an environment variable
MP_NAME
existed,
it's value would be
used as the default for
the name
option.
The options value can
still be set by
explicitly passing a
new value as
a command line option.
Configuration files can
also be used to
override the default values of
command line options.
To use configuration files
pass a base file name to
the begin.start()
decorator through
the config_file
parameter:
>>> import begin >>> @begin.start(config_file='.camelot.cfg') ... def run(name='Arther', quest='Holy Grail', colour='blue', *knights): ... "tis but a scratch!"
This example will
look for configuration files named
.camelot.cfg
in
the current directory and/or
the user's home directory.
A command line option's
default value can be
changed by an
option value in
a configuration file.
The configuration section
used matches the
decorated function's name
by default.
This can be changed by
passing a config_section
parameter to begin.start()
:
>>> import begin >>> @begin.start(config_file='.camelot.cfg', config_section='camelot') ... def run(name='Arther', quest='Holy Grail', colour='blue', *knights): ... "tis but a scratch!"
In this second example
the section camelot
will be used instead of
a section named run
.
Command line arguments are always passed as strings. Sometimes thought it is more convenient to receive arguments of different types. For example, this is a possible function for starting a web application:
>>> import begin >>> @begin.start ... def main(host='127.0.0.1', port='8080', debug='False'): ... port = int(port) ... debug = begin.utils.tobool(debug) ... "Run web application"
Having to convert
the port
argument to
an integer and
the debug
argument to
a boolean is
additional boilerplate code.
To avoid this begins provides
the begin.convert()
decorator.
This decorator accepts functions
as keyword arguments where
the argument name matches that of
the decorator function.
These functions are used
to convert the
types of arguments.
Rewriting the example above using
the begin.convert()
decorator:
>>> import begin >>> @begin.start ... @begin.convert(port=int, debug=begin.utils.tobool) ... def main(host='127.0.0.1', port=8080, debug=False): ... "Run web application"
The module begin.utils
contains
useful functions for
converting argument types.
There are behaviours that
are common to many
command line applications,
such as configuring the
logging
and
cgitb
modules.
begins provides
function decorators that
extend a program's
command line arguments to
configure these modules.
begin.tracebacks()
begin.logging()
To use these decorators
they need to decorate
the main function
before begin.start()
is applied.
The begin.tracebacks()
decorator
adds command line options for
extended traceback reports to
be generated for
unhandled exceptions:
>>> import begin >>> @begin.start ... @begin.tracebacks ... def main(*message): ... pass
The example above will now have the following additional argument group:
tracebacks: Extended traceback reports on failure --tracebacks Enable extended traceback reports --tbdir TBDIR Write tracebacks to directory
Passing --tracebacks
will
cause extended traceback reports
to be generated for
unhandled exceptions.
Traceback options may also be set using configuration files, if Configuration files are supported. The follow options are used.
enabled
: use any oftrue
,t
,yes
,y
,on
or1
to enable tracebacks.directory
: write tracebacks to this directory.
Options are expected to
be in a tracebacks
section.
The begin.logging()
decorator
adds command line options for
configuring the logging module:
>>> import logging >>> import begin >>> @begin.start ... @begin.logging ... def main(*message): ... for msg in message: ... logging.info(msg)
The example above will now have two additional optional arguments as well as an additional argument group:
optional arguments: -h, --help show this help message and exit -v, --verbose Increse logging output -q, --quiet Decrease logging output logging: Detailed control of logging output --loglvl {DEBUG,INFO,WARNING,ERROR,CRITICAL} Set explicit log level --logfile LOGFILE Ouput log messages to file --logfmt LOGFMT Log message format
The logging level
defaults to INFO
.
It can be adjusted
by passing --quiet
,
--verbose
or
explicitly using --loglvl
.
The default log format depends on whether log output is being directed to standard out or file. The raw log text is written to standard out. The log message written to file output includes:
- Time
- Log level
- Filename and line number
- Message
The message format can
be overridden using
the --logfmt
option.
Logging options may also be set using configuration files, if Configuration files are supported. The follow options are used.
level
: log level, must be one ofDEBUG
,INFO
,WARNING
,ERROR
orCRITICAL
.file
: output log messages to this file.format
: log message format.
Options are expected to
be in a logging
section.
The setuptools package supports automatic script creation to automatically create command line scripts. These command line scripts use the entry points system from setuptools.
To support the
use of entry points,
functions decorated by
begin.start()
have
an instance method called
start()
that must be
used to configure the
entry point:
setup( # ... entry_points = { 'console_scripts': [ 'program = package.module:main.start' ] }
Use of the start()
method is
required because the
main function is not
called from the __main__
module
by the entryp points system.
Any bug reports or feature requests can be made using GitHub' issues system.