.. _guide: ===================== Guide to using begins ===================== -------- Overview -------- 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. ----------- Why begins? ----------- 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. ------------ Requirements ------------ 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. ------------ Installation ------------ *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. --------------------------------- Setting a programs starting point --------------------------------- 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. ---------------------------- Parsing command line options ---------------------------- 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. --------------- Boolean options --------------- If a command line option has a default value that is a ``bool`` object. (``True`` or ``False``) The command line option will be flags rather than an option that accepts a value. Two flags are generated, one to set a ``True`` value and one to set a ``False`` value. The two commands will be of the form ``--flag`` and ``--no-flag``. For example:: >>> import begin >>> @begin.start ... def main(enable=False, disable=True): ... pass Using ``--enable`` or ``--no-disable`` when invoking this program will invert the associated option. The options ``--no-enable`` and ``--disable`` have not effect. ------------ Sub-Commands ------------ *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. It is possible to create a sub-command with a different name from the decorated function's name. To do this pass the desired sub-command name using the ``name`` keyword argument:: >>> import begin >>> @begin.subcommand(name='colour') # doctest: +SKIP ... def question(answer): ... "What is your favourite colour?" Sub-commands can also 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. --------------------- Multiple 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 ``--``. ------------------- Sub-Command Context ------------------- There are use cases where it is desirable to pass state from the main function to a subsequent sub-command. To support this Begins provides the ``begin.context`` object. This object will have the following properties: * ``last_return``, value returned by previous command function. * ``return_values``, iterable of all return values from previous commands. * ``opts_previous``, iterable of options object used by previous commands. * ``opts_current``, options object for current command. * ``opts_next``, iterable of options object for following commands. * **(deprecated)** ``return_value``, replaced by ``last_return``. Any other properties set on the ``begin.context`` object will not be altered by begins. The ``last_return`` property and ``return_values`` will always be populated, even in the value returned from the main function or a sub-command function is the ``None`` object. The length and order of the ``return_values`` will match those of ``opts_previous``. --------------------- Environment Variables --------------------- 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 ------------------- 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``. --------------------- Argument type casting --------------------- 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. ----------------- Automatic casting ----------------- For simple, built-in types *begins* can automatically type cast arguments. This is achieved by passing the parameter ``_automatic`` to ``begin.convert()``:: >>> import begin >>> @begin.start ... @begin.convert(_automatic=True) ... def main(host='127.0.0.1', port=8080, debug=False): ... "Run web application" This example is functionally equivalent to the example above. Automatic type casting works for the following built-in types. * ``int`` or ``long`` * ``float`` * ``boolean`` * ``tuple`` or ``list`` Additional casting functions can be provided with the same call to the ``begin.convert()`` decorator. Alternatively, use of ``begin.convert()`` can be dispensed by passing ``True`` to ``begin.start()`` via the ``auto_convert`` parameter:: >>> import begin >>> @begin.start(auto_convert=True) ... def main(host='127.0.0.1', port=8080, debug=False): ... "Run web application" Again, this example is functionally equivalent to the example above. The limitation of using ``auto_convert`` is that it is not longer possible to provide additional casting functions. ----------------------- Command Line Extensions ----------------------- 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. Tracebacks ---------- 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 of ``true``, ``t``, ``yes``, ``y``, ``on`` or ``1`` to enable tracebacks. * ``directory``: write tracebacks to this directory. Options are expected to be in a ``tracebacks`` section. Logging ------- 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 of ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` or ``CRITICAL``. * ``file``: output log messages to this file. * ``format``: log message format. Options are expected to be in a ``logging`` section. ----------------------- Command Line Formatting ----------------------- The default `argparse`_ help formatter may not always meet your needs. An alternate formatter can be provided using the ``formatter_class`` argument to ``begin.start()``:: >>> import begin, argparse >>> @begin.start(formatter_class=argparse.RawTextHelpFormatter) ... def main(): ... pass Any of the `formatter classes`_ provided by the argparse module can be used. Alternatively, ``begin.formatters`` provides a mechanism to compose new formatter class according to your requirements.:: >>> from begin import formatters >>> formatter_class = formatters.compose(formatters.RawDescription, formatters.RawArguments) The following mixin classes are provided for use with ``begin.formatters.compose()`` * RawDescription * RawArguments * ArgumentDefaults * RemoveSubcommandsLine One or more of these may be passed to ``begin.formatters.compose()`` to create a new formatter class. ------------ Entry Points ------------ 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. .. _issues: ------ Issues ------ Any bug reports or feature requests can be made using Github's `issues system`_. .. _Github: https://github.com/aliles/begins .. _Python: http://python.org .. _Python Package Index: https://pypi.python.org/pypi .. _Pip: http://www.pip-installer.org .. _argparse: https://pypi.python.org/pypi/argparse .. _automatic script creation: http://peak.telecommunity.com/DevCenter/setuptools#automatic-script-creation .. _issues system: https://github.com/aliles/begins/issues .. _entry points: http://peak.telecommunity.com/DevCenter/setuptools#dynamic-discovery-of-services-and-plugins .. _funcsigs: https://pypi.python.org/pypi/funcsigs .. _function annotations: http://www.python.org/dev/peps/pep-3107/ .. _formatter classes: http://docs.python.org/dev/library/argparse.html#formatter-class .. _setuptools: https://pypi.python.org/pypi/setuptools .. _sub-commands: http://docs.python.org/dev/library/argparse.html#sub-commands