diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..ac99a30 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,66 @@ +============== +The begins API +============== + +.. module:: begin + :noindex: + +The :mod:`begin` module provides the bulk of *begins* decorator API. + +.. decorator:: start(auto_convert=False, cmd_delim=None, short_args=True, lexical_order=False, env_prefix=None, config_file=None, config_section=None, sub_group=None, collector=None) + + Annotates the decorated function as the programs main function. If the + module is run, the function will be automatically called after processing + options from the command line. + + If *auto_convert* is ``True`` the decorated function will also be decorated + by :func:`begin.convert` with automatic type conversion enabled. + + If *cmd_delim* is provided, it will be used as a separator on the command + line between multiple sub-commands that are to be called in sequence. + + If *short_args* is ``False`` (default is ``True``) the use of short, single + character options on the command line is suppressed. + + If *lexical_order* is ``True`` (default is ``False``) command line options + in help output will be in alphabetical order rather than the order in which + they appear in the function signature. + + If *env_prefix* is provided, the use of environment variables for changing + the default value of command line options is enabled. The value of + *env_prefix* is used as a prefix in front of environment variables. An empty + string my be provided to indicate no prefix but still enable the use of + environment variables. + + If *config_file* is provided, it's the name of a file in the current and + user's home directory from which configuration options will be loaded to + change the default values of command line options. + + If *config_section* is provided it overrides the decorated function name as + the configuration file section from which to load command line option + defaults. + + The *sub_group* changes the collector name used to load sub-commands from. + The *sub_group* name must match the *group* name provdided to the + :func:`begin.subcommand` decorator. + + The *collector* allows a :class:`begin.subcommand.Collector` of sub-commands + to be provided instead of a named collector. + +Sub-commands +============ + +.. decorator:: subcommand(name=None, group=None, collector=None) + + TODO + +TODO + +Type conversion +=============== + +.. decorator:: convert(**kwargs) + + TODO + +.. module:: begin.utils diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 0000000..5a5079a --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,891 @@ +===================== +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. + +|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: + +------ +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 diff --git a/docs/index.rst b/docs/index.rst index 106e295..493ef35 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,20 +3,23 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to begins's documentation! -====================================== +Introducing begins +================== -Contents: +*begins* creates command line programs for busy developers. -.. toctree:: - :maxdepth: 2 +Table of Contents +================= +.. toctree:: + :maxdepth: 2 -Indices and tables -================== + tutorial + guide + api -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +Issues +====== +If you encounter problems, please refer to :ref:`issues` from the guide. diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 0000000..678e3c1 --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,3 @@ +========================== +A short tutorial on begins +==========================