diff --git a/bob/extension/log.py b/bob/extension/log.py new file mode 100644 index 0000000000000000000000000000000000000000..3c12d2f4965b989abb6f905a83e10137e7cd0edf --- /dev/null +++ b/bob/extension/log.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# Andre Anjos <andre.anjos@idiap.ch> +# Sat 19 Oct 12:51:01 2013 +"""Sets-up logging, centrally for Bob. +""" + +import sys +import logging + +# get the default root logger of Bob +_logger = logging.getLogger('bob') + +# by default, warning and error messages should be written to sys.stderr +_warn_err = logging.StreamHandler(sys.stderr) +_warn_err.setLevel(logging.WARNING) +_logger.addHandler(_warn_err) + +# debug and info messages are written to sys.stdout + + +class _InfoFilter: + def filter(self, record): + return record.levelno <= logging.INFO + + +_debug_info = logging.StreamHandler(sys.stdout) +_debug_info.setLevel(logging.DEBUG) +_debug_info.addFilter(_InfoFilter()) +_logger.addHandler(_debug_info) + + +# helper functions to instantiate and set-up logging +def setup(logger_name, + format="%(name)s@%(asctime)s -- %(levelname)s: %(message)s"): + """This function returns a logger object that is set up to perform logging + using Bob loggers. + + Parameters + ---------- + logger_name : str + The name of the module to generate logs for + format : :obj:`str`, optional + The format of the logs, see :py:class:`logging.LogRecord` for more + details. By default, the log contains the logger name, the log time, the + log level and the massage. + + Returns + ------- + logger : :py:class:`logging.Logger` + The logger configured for logging. The same logger can be retrieved using + the :py:func:`logging.getLogger` function. + """ + # generate new logger object + logger = logging.getLogger(logger_name) + + # add log the handlers if not yet done + if not logger_name.startswith("bob") and not logger.handlers: + logger.addHandler(_warn_err) + logger.addHandler(_debug_info) + + # this formats the logger to print the desired information + formatter = logging.Formatter(format) + # we have to set the formatter to all handlers registered in the current + # logger + for handler in logger.handlers: + handler.setFormatter(formatter) + + # set the same formatter for bob loggers + for handler in _logger.handlers: + handler.setFormatter(formatter) + + return logger + + +def set_verbosity_level(logger, level): + """Sets the log level for the given logger. + + Parameters + ---------- + logger : :py:class:`logging.Logger` or str + The logger to generate logs for, or the name of the module to generate + logs for. + level : int + Possible log levels are: 0: Error; 1: Warning; 2: Info; 3: Debug. + + Raises + ------ + ValueError + If the level is not in range(0, 4). + """ + if level not in range(0, 4): + raise ValueError( + "The verbosity level %d does not exist. Please reduce the number of " + "'--verbose' parameters in your command line" % level) + # set up the verbosity level of the logging system + log_level = { + 0: logging.ERROR, + 1: logging.WARNING, + 2: logging.INFO, + 3: logging.DEBUG + }[level] + + # set this log level to the logger with the specified name + if isinstance(logger, str): + logger = logging.getLogger(logger) + logger.setLevel(log_level) + # set the same log level for the bob logger + _logger.setLevel(log_level) + + +__all__ = [_ for _ in dir() if not _.startswith('_')] diff --git a/bob/extension/rc_config.py b/bob/extension/rc_config.py index 6cb9fe91bc97078649c07335a07dd2b7a57a6ea1..c84252cf11a5db41ecfce717047af165b842f4a5 100644 --- a/bob/extension/rc_config.py +++ b/bob/extension/rc_config.py @@ -67,18 +67,21 @@ def _loadrc(): return json.load(f, object_hook=_default_none_dict) -def _dumprc(context, f): - """Saves the context into the global rc file. +def _rc_to_str(context): + """Converts the configurations into a pretty JSON formatted string. Parameters ---------- context : dict All the configurations to save into the rc file. - f : obj - An object that provides a ``f.write()`` function. + + Returns + ------- + str + The configurations in a JSON formatted string. """ - json.dump(context, f, sort_keys=True, indent=4, separators=(',', ': ')) + return json.dumps(context, sort_keys=True, indent=4, separators=(',', ': ')) def _saverc(context): @@ -92,4 +95,4 @@ def _saverc(context): path = _get_rc_path() with open(path, 'wt') as f: - _dumprc(context, f) + f.write(_rc_to_str(context)) diff --git a/bob/extension/scripts/config.py b/bob/extension/scripts/config.py index 1babfce85413f601ea5f2e7fca16deca1c8a3476..ca779b1c5bd740485e18725916842263b131f907 100644 --- a/bob/extension/scripts/config.py +++ b/bob/extension/scripts/config.py @@ -1,102 +1,87 @@ """The manager for bob's main configuration. """ -from __future__ import (absolute_import, division, - print_function, unicode_literals) from .. import rc -from ..rc_config import _saverc, _dumprc, _get_rc_path -from argparse import RawDescriptionHelpFormatter +from ..rc_config import _saverc, _rc_to_str, _get_rc_path import logging -import sys +import click +logger = logging.getLogger(__name__) -def setup_parser(parser): - from . import __doc__ as docs - # creates a top-level parser for this database - top_level = parser.add_parser('config', - formatter_class=RawDescriptionHelpFormatter, - help=docs) - subparsers = top_level.add_subparsers(title="subcommands") +@click.group() +def config(): + """The manager for bob's global configuration.""" + # Load the config file again. This may be needed since the environment + # variable might change the config path during the tests. Otherwise, this + # should not be important. + logger.debug('Reloading the global configuration file.') + from ..rc_config import _loadrc + rc.clear() + rc.update(_loadrc()) - # add commands - show_command(subparsers) - get_command(subparsers) - set_command(subparsers) - return subparsers +@config.command() +def show(): + """Shows the configuration. - -def show(arguments=None): - """Shows the content of bob's global configuration file. + Displays the content of bob's global configuration file. """ - print("Displaying `{}':".format(_get_rc_path())) - _dumprc(rc, sys.stdout) - print() - + log_file = click.get_current_context().meta['log_file'] + click.echo("Displaying `{}':".format(_get_rc_path()), log_file) + click.echo(_rc_to_str(rc), log_file) -def show_command(subparsers): - parser = subparsers.add_parser('show', help=show.__doc__) - parser.set_defaults(func=show) - return parser +@config.command() +@click.argument('key') +def get(key): + """Prints a key. -def get(arguments): - """Gets the specified configuration from bob's global configuration file. + Retrieves the value of the requested key and displays it. - Parameters - ---------- - arguments : argparse.Namespace - A set of arguments passed by the command-line parser + \b + Arguments + --------- + key : str + The key to return its value from the configuration. - - Returns - ------- - int - A POSIX compliant return value of ``0`` if the key exists, or ``1`` - otherwise. + \b + Fails + ----- + * If the key is not found. """ - value = rc[arguments.key] + log_file = click.get_current_context().meta['log_file'] + value = rc[key] if value is None: - return 1 - print(value) - return 0 - + raise click.ClickException( + "The requested key `{}' does not exist".format(key)) + click.echo(value, log_file) -def get_command(subparsers): - parser = subparsers.add_parser('get', help=get.__doc__) - parser.add_argument("key", help="The requested key.") - parser.set_defaults(func=get) - return parser +@config.command() +@click.argument('key') +@click.argument('value') +def set(key, value): + """Sets the value for a key. -def set(arguments): - """Sets the specified configuration to the provided value in bob's global + Sets the value of the specified configuration key in bob's global configuration file. - Parameters - ---------- - arguments : argparse.Namespace - A set of arguments passed by the command-line parser - - - Returns - ------- - int - A POSIX compliant return value of ``0`` if the operation is successful, - or ``1`` otherwise. + \b + Arguments + --------- + key : str + The key to set the value for. + value : str + The value of the key. + + \b + Fails + ----- + * If something goes wrong. """ try: - rc[arguments.key] = arguments.value + rc[key] = value _saverc(rc) except Exception: - logging.warn("Could not configure the rc file", exc_info=True) - return 1 - return 0 - - -def set_command(subparsers): - parser = subparsers.add_parser('set', help=set.__doc__) - parser.add_argument("key", help="The key.") - parser.add_argument("value", help="The value.") - parser.set_defaults(func=set) - return parser + logger.error("Could not configure the rc file", exc_info=True) + raise click.ClickException("Failed to change the configuration.") diff --git a/bob/extension/scripts/main_cli.py b/bob/extension/scripts/main_cli.py index 0f120cf7aa214740687ee9d6f17881473b4f1b5b..3c6154e537f5438cf561a40b6e189f1339c86cba 100644 --- a/bob/extension/scripts/main_cli.py +++ b/bob/extension/scripts/main_cli.py @@ -1,47 +1,30 @@ """This is the main entry to bob's scripts. -As of now it just supports `bob config`. """ - -from __future__ import (absolute_import, division, - print_function, unicode_literals) -import argparse - -epilog = """ For a list of available commands: - >>> %(prog)s --help - - For a list of actions on each command: - >>> %(prog)s <command> --help -""" - - -def create_parser(**kwargs): - """Creates a parser for the central manager taking into consideration the - options for every module that can provide those.""" - - parser = argparse.ArgumentParser(**kwargs) - subparsers = parser.add_subparsers(title='commands') - - return parser, subparsers - - -def main(argv=None): - from argparse import RawDescriptionHelpFormatter - parser, subparsers = create_parser( - description=__doc__, epilog=epilog, - formatter_class=RawDescriptionHelpFormatter) - - # for now there is only the config command so we'll just add it here. - # Normally, this would be added in a better way in future. Maybe something - # similar to bob_dbmanage.py - from .config import setup_parser - setup_parser(subparsers) - - args = parser.parse_args(args=argv) - if hasattr(args, 'func'): - return args.func(args) - else: - return parser.parse_args(args=['--help']) - - -if __name__ == '__main__': - main() +import pkg_resources +import click +from click_plugins import with_plugins +from ..log import setup, set_verbosity_level +logger = setup('bob') + + +@with_plugins(pkg_resources.iter_entry_points('bob.cli')) +@click.group() +@click.option( + '-v', + '--verbose', + count=True, + help="Increase the verbosity level from 0 (only error messages) to 1 " + "(warnings), 2 (log messages), 3 (debug information) by adding the " + "--verbose option as often as desired (e.g. '-vvv' for debug).") +@click.option( + '--log', + type=click.File('wb'), + help='Redirects the prints of the scripts to FILENAME.') +def main(verbose, log): + """The main command line interface for bob. + Look below for available commands.""" + set_verbosity_level(logger, verbose) + logger.debug("Logging of the `bob' logger was set to %d", verbose) + ctx = click.get_current_context() + ctx.meta['verbosity'] = verbose + ctx.meta['log_file'] = log diff --git a/bob/extension/test_rc.py b/bob/extension/test_rc.py index 27f6e360624c91c2ff091b6f4265220f0c395cf4..18729604f9a3db55f620c0753f2b87ae71146a03 100644 --- a/bob/extension/test_rc.py +++ b/bob/extension/test_rc.py @@ -2,30 +2,68 @@ from .rc_config import _loadrc, ENVNAME from .scripts import main_cli +from click.testing import CliRunner import os import pkg_resources -import tempfile path = pkg_resources.resource_filename('bob.extension', 'data') def test_rc_env(): - os.environ[ENVNAME] = os.path.join(path, 'defaults-config') - c = _loadrc() # should load from environment variable - REFERENCE = { - "bob.db.atnt.directory": "/home/bob/databases/atnt", - "bob.db.mobio.directory": "/home/bob/databases/mobio" - } + os.environ[ENVNAME] = os.path.join(path, 'defaults-config') + c = _loadrc() # should load from environment variable + REFERENCE = { + "bob.db.atnt.directory": "/home/bob/databases/atnt", + "bob.db.mobio.directory": "/home/bob/databases/mobio" + } - assert c == REFERENCE - assert c['random'] is None + assert c == REFERENCE + assert c['random'] is None def test_bob_config(): - os.environ[ENVNAME] = os.path.join(path, 'defaults-config') - main_cli(['config', 'get', 'bob.db.atnt.directory']) - with tempfile.NamedTemporaryFile('wt') as f: - os.environ[ENVNAME] = f.name - main_cli(['config', 'set', 'bob.db.atnt.directory', - '/home/bob/databases/atnt']) - main_cli(['config', 'show']) + defaults_config = os.path.join(path, 'defaults-config') + runner = CliRunner(env={ENVNAME: defaults_config}) + + # test config show + result = runner.invoke(main_cli, ['config', 'show']) + assert result.exit_code == 0, result.exit_code + assert 'defaults-config' in result.output, result.output + assert open(defaults_config).read() in result.output, result.output + + # test config get (existing key) + result = runner.invoke(main_cli, + ['config', 'get', 'bob.db.atnt.directory']) + assert result.exit_code == 0, result.exit_code + assert result.output == '/home/bob/databases/atnt\n', result.output + + # test config get (non-existing key) + result = runner.invoke(main_cli, ['config', 'get', 'bob.db.atnt']) + assert result.exit_code == 1, result.exit_code + + # test config set + runner = CliRunner() + with runner.isolated_filesystem(): + bobrcfile = 'bobrc' + result = runner.invoke( + main_cli, [ + 'config', 'set', 'bob.db.atnt.directory', + '/home/bob/databases/orl_faces' + ], + env={ + ENVNAME: bobrcfile + }) + assert result.exit_code == 0, result.exit_code + + # read the config back to make sure it is ok. + result = runner.invoke( + main_cli, ['config', 'show'], env={ + ENVNAME: bobrcfile + }) + assert result.exit_code == 0, result.exit_code + expected_output = '''Displaying `bobrc': +{ + "bob.db.atnt.directory": "/home/bob/databases/orl_faces" +} +''' + assert expected_output == result.output, result.output diff --git a/bob/extension/utils.py b/bob/extension/utils.py index edc583e9cf84bc6989ddc60ce4648c0466ca55d2..2ee3bbfd6885de4e5796929fa3d2c918cfa3bc37 100644 --- a/bob/extension/utils.py +++ b/bob/extension/utils.py @@ -516,6 +516,7 @@ def link_documentation(additional_packages = ['python', 'numpy'], requirements_f _add_index('docopt', 'http://docopt.readthedocs.io/en/latest/') _add_index('scikit-image', 'http://scikit-image.org/docs/dev/') _add_index('pillow', 'http://pillow.readthedocs.io/en/latest/') + _add_index('click', 'http://click.pocoo.org/') # get the server for the other packages diff --git a/doc/extra-intersphinx.txt b/doc/extra-intersphinx.txt index 70227e5aac6f4f4f2a8d22cc7be908490bab022c..d08a99ed018f86ff0be2857d471a8033a7aef8a2 100644 --- a/doc/extra-intersphinx.txt +++ b/doc/extra-intersphinx.txt @@ -1,4 +1,5 @@ python +click bob.blitz bob.db.base bob.io.image diff --git a/doc/framework.rst b/doc/framework.rst index d1e070ab1e1f43f695ff26cc2e03dcd7595e1825..13058c1690ff75af80f6da2ee25578000c24e8a6 100644 --- a/doc/framework.rst +++ b/doc/framework.rst @@ -180,5 +180,51 @@ The data may be further processed using a [ 1. , 2. , 3. , 0.5, 1. , 1.5]]) +.. _bob.extension.cli: + +Unified Command Line Mechanism +------------------------------ + +|project| comes with a command line called ``bob`` which provides a set of +commands by default:: + + $ bob --help + Usage: bob [OPTIONS] COMMAND [ARGS]... + + The main command line interface for bob. Look below for available + commands. + + Options: + -v, --verbose Increase the verbosity level from 0 (only error messages) to + 1 (warnings), 2 (log messages), 3 (debug information) by + adding the --verbose option as often as desired (e.g. '-vvv' + for debug). + --log FILENAME Redirects the prints of the scripts to FILENAME. + --help Show this message and exit. + + Commands: + config The manager for bob's global configuration. + ... + +This command line is implemented using click_. You can extend the commands of +this script through setuptools entry points (this is implemented using +`click-plugins`_). To do so you implement your command-line using click_ +independently; then, advertise it as a command under bob script using the +``bob.cli`` entry point. + +.. note:: + + If you are still not sure how this must be done, maybe you don't know how + to use click_ yet. + +This feature is experimental and may change and break compatibility in future. +For a best practice example, please look at how the ``bob config`` command is +implemented: + +.. literalinclude:: ../bob/extension/scripts/config.py + :caption: "bob/extension/scripts/config.py" implementation of the ``bob config`` command. + :language: python + + .. include:: links.rst diff --git a/doc/links.rst b/doc/links.rst index e35098d78077b4f8dd9aada76849fab3cafe829b..6fe7425d20d0ba36205b32e2459bac772a9f2ec1 100644 --- a/doc/links.rst +++ b/doc/links.rst @@ -8,6 +8,8 @@ .. _bob: https://www.idiap.ch/software/bob .. _bob's installation: https://www.idiap.ch/software/bob/install .. _c++: http://www2.research.att.com/~bs/C++.html +.. _click: http://click.pocoo.org/ +.. _click-plugins: https://github.com/click-contrib/click-plugins .. _distutils: https://docs.python.org/distutils/ .. _git: http://git-scm.com/ .. _gitlab: https://gitlab.idiap.ch/bob/ diff --git a/doc/nitpick-exceptions.txt b/doc/nitpick-exceptions.txt index 2858a4a1cc56e7b292bbfa26934ff5c395a97fc0..dbd6066c62af547013ac6f786ee2a12d92cde299 100644 --- a/doc/nitpick-exceptions.txt +++ b/doc/nitpick-exceptions.txt @@ -1,3 +1,6 @@ +# Not available in Python 2.7, but ok in Python 3.x +py:exc ValueError + # we don't link against setuptools manual py:class setuptools.extension.Extension py:class distutils.extension.Extension diff --git a/doc/py_api.rst b/doc/py_api.rst index db164fa7a2f11f501bfb48595b758f015030bcf2..175a1efe28583c0bfb1a9cb40455ddd9345c9c19 100644 --- a/doc/py_api.rst +++ b/doc/py_api.rst @@ -96,6 +96,12 @@ Stacked Processors :special-members: __init__, __call__ +Logging +------- + +.. automodule:: bob.extension.log + + Scripts ------- diff --git a/setup.py b/setup.py index aded8140d195bc566c222d4c1889525bf0ecab52..ca081dd2aecf290676cd6718361419a95ed6a809 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,6 @@ # vim: set fileencoding=utf-8 : # Andre Anjos <andre.anjos@idiap.ch> # Thu 20 Sep 2012 14:43:19 CEST - """A package that contains a helper for Bob/Python C++ extension development """ @@ -11,13 +10,7 @@ from setuptools import setup, find_packages # Define package version version = open("version.txt").read().rstrip() -requires = ['setuptools'] -import sys -if sys.version_info[0] == 2 and sys.version_info[1] <= 6: - requires.append('importlib') - setup( - name="bob.extension", version=version, description="Building of Python/C++ extensions for Bob", @@ -31,33 +24,32 @@ setup( include_package_data=True, zip_safe=False, - - - install_requires=requires, - - entry_points = { - 'console_scripts': [ - 'bob = bob.extension.scripts:main_cli', - 'bob_new_version.py = bob.extension.scripts:new_version', - 'bob_dependecy_graph.py = bob.extension.scripts:dependency_graph', - ], - # some test entry_points - 'bob.extension.test_config_load': [ - 'basic_config = bob.extension.data.basic_config', - 'resource_config = bob.extension.data.resource_config', - 'subpackage_config = bob.extension.data.subpackage.config', - ], + install_requires=['setuptools', 'click'], + + entry_points={ + 'console_scripts': [ + 'bob = bob.extension.scripts:main_cli', + 'bob_new_version.py = bob.extension.scripts:new_version', + 'bob_dependecy_graph.py = bob.extension.scripts:dependency_graph', + ], + 'bob.cli': [ + 'config = bob.extension.scripts.config:config', + ], + # some test entry_points + 'bob.extension.test_config_load': [ + 'basic_config = bob.extension.data.basic_config', + 'resource_config = bob.extension.data.resource_config', + 'subpackage_config = bob.extension.data.subpackage.config', + ], }, - - classifiers = [ - 'Framework :: Bob', - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development :: Libraries :: Python Modules', + classifiers=[ + 'Framework :: Bob', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Software Development :: Libraries :: Python Modules', ], - )