======
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.

|pypi_version| |build_status| |coverage|

-----------
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
------

Any bug reports or
feature requests can
be made using GitHub' `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

.. |build_status| image:: https://secure.travis-ci.org/aliles/begins.png?branch=master
   :target: https://travis-ci.org/aliles/begins
   :alt: Current build status

.. |coverage| image:: https://coveralls.io/repos/aliles/begins/badge.png?branch=master
   :target: https://coveralls.io/r/aliles/begins?branch=master
   :alt: Latest PyPI version

.. |pypi_version| image:: https://pypip.in/v/begins/badge.png
   :target: https://crate.io/packages/begins/
   :alt: Latest PyPI version
