
Exploring the Demo App
**********************

The cliff source package includes a "demoapp" directory containing an
example main program with several command plugins.


Setup
=====

To install and experiment with the demo app you should create a
virtual environment and activate it. This will make it easy to remove
the app later, since it doesn't do anything useful and you aren't
likely to want to hang onto it after you understand how it works.

   $ pip install virtualenv
   $ virtualenv .venv
   $ . .venv/bin/activate
   (.venv)$

Next, install cliff in the same environment.

   (.venv)$ python setup.py install

Finally, install the demo application into the virtual environment.

   (.venv)$ cd demoapp
   (.venv)$ python setup.py install


Usage
=====

Both cliff and the demo installed, you can now run the command
"cliffdemo".

For basic command usage instructions and a list of the commands
available from the plugins, run:

   (.venv)$ cliffdemo -h

or:

   (.venv)$ cliffdemo --help

Run the "simple" command by passing its name as argument to
"cliffdemo".

   (.venv)$ cliffdemo simple

The "simple" command prints this output to the console:

   sending greeting
   hi!

To see help for an individual command, use the "help" command:

   (.venv)$ cliffdemo help files


The Source
==========

The "cliffdemo" application is defined in a "cliffdemo" package
containing several modules.


main.py
-------

The main application is defined in "main.py":

   import logging
   import sys

   from cliff.app import App
   from cliff.commandmanager import CommandManager


   class DemoApp(App):

       log = logging.getLogger(__name__)

       def __init__(self):
           super(DemoApp, self).__init__(
               description='cliff demo app',
               version='0.1',
               command_manager=CommandManager('cliff.demo'),
               )

       def initialize_app(self, argv):
           self.log.debug('initialize_app')

       def prepare_to_run_command(self, cmd):
           self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__)

       def clean_up(self, cmd, result, err):
           self.log.debug('clean_up %s', cmd.__class__.__name__)
           if err:
               self.log.debug('got an error: %s', err)


   def main(argv=sys.argv[1:]):
       myapp = DemoApp()
       return myapp.run(argv)


   if __name__ == '__main__':
       sys.exit(main(sys.argv[1:]))

The "DemoApp" class inherits from "App" and overrides "__init__()" to
set the program description and version number. It also passes a
"CommandManager" instance configured to look for plugins in the
"cliff.demo" namespace.

The "initialize_app()" method of "DemoApp" will be invoked after the
main program arguments are parsed, but before any command processing
is performed and before the application enters interactive mode. This
hook is intended for opening connections to remote web services,
databases, etc. using arguments passed to the main application.

The "prepare_to_run_command()" method of "DemoApp" will be invoked
after a command is identified, but before the command is given its
arguments and run. This hook is intended for pre-command validation or
setup that must be repeated and cannot be handled by
"initialize_app()".

The "clean_up()" method of "DemoApp" is invoked after a command runs.
If the command raised an exception, the exception object is passed to
"clean_up()". Otherwise the "err" argument is "None".

The "main()" function defined in "main.py" is registered as a console
script entry point so that "DemoApp" can be run from the command line
(see the discussion of "setup.py" below).


simple.py
---------

Two commands are defined in "simple.py":

   import logging

   from cliff.command import Command


   class Simple(Command):
       "A simple command that prints a message."

       log = logging.getLogger(__name__)

       def take_action(self, parsed_args):
           self.log.info('sending greeting')
           self.log.debug('debugging')
           self.app.stdout.write('hi!\n')


   class Error(Command):
       "Always raises an error"

       log = logging.getLogger(__name__)

       def take_action(self, parsed_args):
           self.log.info('causing error')
           raise RuntimeError('this is the expected exception')

"Simple" demonstrates using logging to emit messages on the console at
different verbose levels.

   (.venv)$ cliffdemo simple
   sending greeting
   hi!

   (.venv)$ cliffdemo -v simple
   prepare_to_run_command Simple
   sending greeting
   debugging
   hi!
   clean_up Simple

   (.venv)$ cliffdemo -q simple
   hi!

"Error" always raises a "RuntimeError" exception when it is invoked,
and can be used to experiment with the error handling features of
cliff.

   (.venv)$ cliffdemo error
   causing error
   ERROR: this is the expected exception

   (.venv)$ cliffdemo -v error
   prepare_to_run_command Error
   causing error
   ERROR: this is the expected exception
   clean_up Error
   got an error: this is the expected exception

   (.venv)$ cliffdemo --debug error
   causing error
   this is the expected exception
   Traceback (most recent call last):
     File ".../cliff/app.py", line 218, in run_subcommand
       result = cmd.run(parsed_args)
     File ".../cliff/command.py", line 43, in run
       self.take_action(parsed_args)
     File ".../demoapp/cliffdemo/simple.py", line 24, in take_action
       raise RuntimeError('this is the expected exception')
   RuntimeError: this is the expected exception
   Traceback (most recent call last):
     File "/Users/dhellmann/Envs/cliff/bin/cliffdemo", line 9, in <module>
       load_entry_point('cliffdemo==0.1', 'console_scripts', 'cliffdemo')()
     File ".../demoapp/cliffdemo/main.py", line 33, in main
       return myapp.run(argv)
     File ".../cliff/app.py", line 160, in run
       result = self.run_subcommand(remainder)
     File ".../cliff/app.py", line 218, in run_subcommand
       result = cmd.run(parsed_args)
     File ".../cliff/command.py", line 43, in run
       self.take_action(parsed_args)
     File ".../demoapp/cliffdemo/simple.py", line 24, in take_action
       raise RuntimeError('this is the expected exception')
   RuntimeError: this is the expected exception


list.py
-------

"list.py" includes a single command derived from "cliff.lister.Lister"
which prints a list of the files in the current directory.

   import logging
   import os

   from cliff.lister import Lister


   class Files(Lister):
       """Show a list of files in the current directory.

       The file name and size are printed by default.
       """

       log = logging.getLogger(__name__)

       def take_action(self, parsed_args):
           return (('Name', 'Size'),
                   ((n, os.stat(n).st_size) for n in os.listdir('.'))
                   )

"Files" prepares the data, and "Lister" manages the output formatter
and printing the data to the console.

   (.venv)$ cliffdemo files
   +---------------+------+
   |      Name     | Size |
   +---------------+------+
   | build         |  136 |
   | cliffdemo.log | 2546 |
   | Makefile      | 5569 |
   | source        |  408 |
   +---------------+------+

   (.venv)$ cliffdemo files -f csv
   "Name","Size"
   "build",136
   "cliffdemo.log",2690
   "Makefile",5569
   "source",408


show.py
-------

"show.py" includes a single command derived from "cliff.show.ShowOne"
which prints the properties of the named file.

   import logging
   import os

   from cliff.show import ShowOne


   class File(ShowOne):
       "Show details about a file"

       log = logging.getLogger(__name__)

       def get_parser(self, prog_name):
           parser = super(File, self).get_parser(prog_name)
           parser.add_argument('filename', nargs='?', default='.')
           return parser

       def take_action(self, parsed_args):
           stat_data = os.stat(parsed_args.filename)
           columns = ('Name',
                      'Size',
                      'UID',
                      'GID',
                      'Modified Time',
                      )
           data = (parsed_args.filename,
                   stat_data.st_size,
                   stat_data.st_uid,
                   stat_data.st_gid,
                   stat_data.st_mtime,
                   )
           return (columns, data)

"File" prepares the data, and "ShowOne" manages the output formatter
and printing the data to the console.

   (.venv)$ cliffdemo file setup.py
   +---------------+--------------+
   |     Field     |    Value     |
   +---------------+--------------+
   | Name          | setup.py     |
   | Size          | 5825         |
   | UID           | 502          |
   | GID           | 20           |
   | Modified Time | 1335569964.0 |
   +---------------+--------------+


setup.py
--------

The demo application is packaged using distribute, the modern
implementation of setuptools.

   #!/usr/bin/env python

   PROJECT = 'cliffdemo'

   # Change docs/sphinx/conf.py too!
   VERSION = '0.1'

   # Bootstrap installation of Distribute
   import distribute_setup
   distribute_setup.use_setuptools()

   from setuptools import setup, find_packages

   from distutils.util import convert_path
   from fnmatch import fnmatchcase
   import os
   import sys

   try:
       long_description = open('README.rst', 'rt').read()
   except IOError:
       long_description = ''

   setup(
       name=PROJECT,
       version=VERSION,

       description='Demo app for cliff',
       long_description=long_description,

       author='Doug Hellmann',
       author_email='doug.hellmann@gmail.com',

       url='https://github.com/dreamhost/cliff',
       download_url='https://github.com/dreamhost/cliff/tarball/master',

       classifiers=['Development Status :: 3 - Alpha',
                    'License :: OSI Approved :: Apache Software License',
                    'Programming Language :: Python',
                    'Programming Language :: Python :: 2',
                    'Programming Language :: Python :: 2.7',
                    'Programming Language :: Python :: 3',
                    'Programming Language :: Python :: 3.2',
                    'Intended Audience :: Developers',
                    'Environment :: Console',
                    ],

       platforms=['Any'],

       scripts=[],

       provides=[],
       install_requires=['distribute', 'cliff'],

       namespace_packages=[],
       packages=find_packages(),
       include_package_data=True,

       entry_points={
           'console_scripts': [
               'cliffdemo = cliffdemo.main:main'
               ],
           'cliff.demo': [
               'simple = cliffdemo.simple:Simple',
               'two_part = cliffdemo.simple:Simple',
               'error = cliffdemo.simple:Error',
               'list files = cliffdemo.list:Files',
               'files = cliffdemo.list:Files',
               'file = cliffdemo.show:File',
               'show file = cliffdemo.show:File',
               ],
           },

       zip_safe=False,
       )

The important parts of the packaging instructions are the
"entry_points" settings. All of the commands are registered in the
"cliff.demo" namespace. Each main program should define its own
command namespace so that it only loads the command plugins that it
should be managing.
