Commit 799c31e7 authored by André Anjos's avatar André Anjos 💬

Merge branch 'click-helper' into 'master'

[click_helper][ResourceOption] Improvements

See merge request !115
parents e7292440 aa8832c1
Pipeline #41006 passed with stages
in 10 minutes and 32 seconds
......@@ -6,10 +6,6 @@ import logging
import traceback
logger = logging.getLogger(__name__)
try:
basestring
except NameError:
basestring = str
def bool_option(name, short_name, desc, dflt=False, **kwargs):
......@@ -179,7 +175,7 @@ def verbosity_option(**kwargs):
class ConfigCommand(click.Command):
"""A click.Command that can take options both form command line options and
configuration files. In order to use this class, you have to use the
configuration files. In order to use this class, you **have to** use the
:any:`ResourceOption` class also.
Attributes
......@@ -299,18 +295,30 @@ file.""".format(
class ResourceOption(click.Option):
"""A click.Option that automatically loads resources.
It uses :any:`load` to convert provided strings in the command line into Python
objects.
It also integrates with the ConfigCommand class.
"""An extended click.Option that automatically loads resources from config
files.
This class comes with two different functionalities that are independent and
could be combined:
1. If used in commands that are inherited from :any:`ConfigCommand`, it will
lookup inside the config files (that are provided as argument to the
command) to resolve its value. Values given explicitly in the command
line take precedence.
2. If `entry_point_group` is provided, it will treat values given to it (by
any means) as resources to be loaded. Loading is done using :any:`load`.
See :ref:`bob.extension.config.resource` for more information. The final
value cannot be a string.
You may use this class in three ways:
1. Using this class without using :any:`ConfigCommand` AND providing
`entry_point_group`.
2. Using this class with :any:`ConfigCommand` AND providing `entry_point_group`.
3. Using this class with :any:`ConfigCommand` AND without providing
`entry_point_group`.
1. Using this class (without using :any:`ConfigCommand`) AND (providing
`entry_point_group`).
2. Using this class (with :any:`ConfigCommand`) AND (providing
`entry_point_group`).
3. Using this class (with :any:`ConfigCommand`) AND (without providing
`entry_point_group`).
Using this class without :any:`ConfigCommand` and without providing
`entry_point_group` does nothing and is not allowed.
......@@ -381,11 +389,7 @@ class ResourceOption(click.Option):
# true.
if hasattr(ctx, "config_context"):
value = ctx.config_context.get(self.name)
else:
logger.debug(
"config_context attribute not found in context. Did you mean to "
"use the ConfigCommand class with this ResourceOption class?"
)
# if not from config files, lookup the environment variables
if value is None:
value = self.value_from_envvar(ctx)
......@@ -399,12 +403,11 @@ class ResourceOption(click.Option):
# if the value is a string and an entry_point_group is provided, load it
if self.entry_point_group is not None:
attribute_name = self.entry_point_group.split(".")[-1]
while isinstance(value, basestring):
while isinstance(value, str):
value = load(
[value],
entry_point_group=self.entry_point_group,
attribute_name=attribute_name,
attribute_name=self.name,
)
return value
......
......@@ -3,19 +3,25 @@ import time
import pkg_resources
from click.testing import CliRunner
from bob.extension.scripts.click_helper import (
verbosity_option, bool_option, list_float_option,
ConfigCommand, ResourceOption, AliasedGroup, assert_click_runner_result)
verbosity_option,
bool_option,
list_float_option,
ConfigCommand,
ResourceOption,
AliasedGroup,
assert_click_runner_result,
)
def test_verbosity_option():
for VERBOSITY, OPTIONS in zip([0, 1, 2, 3],
[[], ['-v'], ['-vv'], ['-vvv']]):
for VERBOSITY, OPTIONS in zip([0, 1, 2, 3], [[], ["-v"], ["-vv"], ["-vvv"]]):
@click.command()
@verbosity_option()
def cli(verbose):
ctx = click.get_current_context()
verbose = ctx.meta['verbosity']
verbose = ctx.meta["verbosity"]
assert verbose == VERBOSITY, verbose
runner = CliRunner()
......@@ -24,20 +30,19 @@ def test_verbosity_option():
def test_bool_option():
@click.command()
@bool_option('i-am-test', 'T', 'test test test', True)
@bool_option("i-am-test", "T", "test test test", True)
def cli(i_am_test):
ctx = click.get_current_context()
is_test = ctx.meta['i_am_test']
is_test = ctx.meta["i_am_test"]
assert i_am_test == is_test
assert is_test
@click.command()
@bool_option('i-am-test', 'T', 'test test test', False)
@bool_option("i-am-test", "T", "test test test", False)
def cli2(i_am_test):
ctx = click.get_current_context()
is_test = ctx.meta['i_am_test']
is_test = ctx.meta["i_am_test"]
assert i_am_test == is_test
assert not is_test
......@@ -50,107 +55,106 @@ def test_bool_option():
def test_list_float_option():
@click.command()
@list_float_option('test-list', 'T', 'Test list')
@list_float_option("test-list", "T", "Test list")
def cli(test_list):
ctx = click.get_current_context()
test = ctx.meta['test_list']
test = ctx.meta["test_list"]
assert test == test_list
assert test == [1, 2, 3]
runner = CliRunner()
result = runner.invoke(cli, ['-T', '1,2,3'])
result = runner.invoke(cli, ["-T", "1,2,3"])
assert_click_runner_result(result)
def test_list_float_option_empty():
@click.command()
@list_float_option('test-list', 'T', 'Test list')
@list_float_option("test-list", "T", "Test list")
def cli(test_list):
ctx = click.get_current_context()
test = ctx.meta['test_list']
test = ctx.meta["test_list"]
assert test is None
runner = CliRunner()
result = runner.invoke(cli, ['-T', ' '])
result = runner.invoke(cli, ["-T", " "])
assert_click_runner_result(result)
def test_commands_with_config_1():
# random test
@click.command(
cls=ConfigCommand, entry_point_group='bob.extension.test_config_load')
cls=ConfigCommand, entry_point_group="bob.extension.test_config_load"
)
def cli(**kwargs):
pass
runner = CliRunner()
result = runner.invoke(cli, ['basic_config'])
result = runner.invoke(cli, ["basic_config"])
assert_click_runner_result(result)
def test_commands_with_config_2():
# test option with valid default value
@click.command(
cls=ConfigCommand, entry_point_group='bob.extension.test_config_load')
@click.option(
'-a', cls=ResourceOption, default=3)
cls=ConfigCommand, entry_point_group="bob.extension.test_config_load"
)
@click.option("-a", cls=ResourceOption, default=3)
def cli(a, **kwargs):
click.echo('{}'.format(a))
click.echo("{}".format(a))
runner = CliRunner()
result = runner.invoke(cli, [])
assert_click_runner_result(result)
assert result.output.strip() == '3', result.output
assert result.output.strip() == "3", result.output
result = runner.invoke(cli, ['basic_config'])
result = runner.invoke(cli, ["basic_config"])
assert_click_runner_result(result)
assert result.output.strip() == '1', result.output
assert result.output.strip() == "1", result.output
result = runner.invoke(cli, ['-a', 2])
result = runner.invoke(cli, ["-a", 2])
assert_click_runner_result(result)
assert result.output.strip() == '2', result.output
assert result.output.strip() == "2", result.output
result = runner.invoke(cli, ['-a', 3, 'basic_config'])
result = runner.invoke(cli, ["-a", 3, "basic_config"])
assert_click_runner_result(result)
assert result.output.strip() == '3', result.output
assert result.output.strip() == "3", result.output
result = runner.invoke(cli, ['basic_config', '-a', 3])
result = runner.invoke(cli, ["basic_config", "-a", 3])
assert_click_runner_result(result)
assert result.output.strip() == '3', result.output
assert result.output.strip() == "3", result.output
def test_commands_with_config_3():
# test required options
@click.command(
cls=ConfigCommand, entry_point_group='bob.extension.test_config_load')
@click.option(
'-a', cls=ResourceOption, required=True)
cls=ConfigCommand, entry_point_group="bob.extension.test_config_load"
)
@click.option("-a", cls=ResourceOption, required=True)
def cli(a, **kwargs):
click.echo('{}'.format(a))
click.echo("{}".format(a))
runner = CliRunner()
result = runner.invoke(cli, [])
assert_click_runner_result(result, exit_code=2)
result = runner.invoke(cli, ['basic_config'])
result = runner.invoke(cli, ["basic_config"])
assert_click_runner_result(result)
assert result.output.strip() == '1', result.output
assert result.output.strip() == "1", result.output
result = runner.invoke(cli, ['-a', 2])
result = runner.invoke(cli, ["-a", 2])
assert_click_runner_result(result)
assert result.output.strip() == '2', result.output
assert result.output.strip() == "2", result.output
result = runner.invoke(cli, ['-a', 3, 'basic_config'])
result = runner.invoke(cli, ["-a", 3, "basic_config"])
assert_click_runner_result(result)
assert result.output.strip() == '3', result.output
assert result.output.strip() == "3", result.output
result = runner.invoke(cli, ['basic_config', '-a', 3])
result = runner.invoke(cli, ["basic_config", "-a", 3])
assert_click_runner_result(result)
assert result.output.strip() == '3', result.output
assert result.output.strip() == "3", result.output
def test_prefix_aliasing():
......@@ -162,31 +166,33 @@ def test_prefix_aliasing():
def test():
click.echo("OK")
@cli.command(name='test-aaa')
@cli.command(name="test-aaa")
def test_aaa():
click.echo("AAA")
runner = CliRunner()
result = runner.invoke(cli, ['te'], catch_exceptions=False)
result = runner.invoke(cli, ["te"], catch_exceptions=False)
assert result.exit_code != 0, (result.exit_code, result.output)
result = runner.invoke(cli, ['test'], catch_exceptions=False)
result = runner.invoke(cli, ["test"], catch_exceptions=False)
assert_click_runner_result(result)
assert 'OK' in result.output, (result.exit_code, result.output)
assert "OK" in result.output, (result.exit_code, result.output)
result = runner.invoke(cli, ['test-a'], catch_exceptions=False)
result = runner.invoke(cli, ["test-a"], catch_exceptions=False)
assert_click_runner_result(result)
assert 'AAA' in result.output, (result.exit_code, result.output)
assert "AAA" in result.output, (result.exit_code, result.output)
def _assert_config_dump(ref, ref_date):
today = time.strftime("%d/%m/%Y")
# uncomment below to re-write tests
# open(ref, 'wt').write(open('TEST_CONF').read())
with open('TEST_CONF', 'r') as f, open(ref, 'r') as f2:
with open("TEST_CONF", "r") as f, open(ref, "r") as f2:
text = f.read()
ref_text = f2.read().replace(ref_date, today)
assert text == ref_text, '\n'.join([text, "########################\n"*2, ref_text])
assert text == ref_text, "\n".join(
[text, "########################\n" * 2, ref_text]
)
def test_config_dump():
......@@ -194,21 +200,28 @@ def test_config_dump():
def cli():
pass
@cli.command(cls=ConfigCommand, epilog='Examples!')
@click.option('-t', '--test', required=True, default="/my/path/test.txt",
help="Path leading to test blablabla", cls=ResourceOption)
@cli.command(cls=ConfigCommand, epilog="Examples!")
@click.option(
"-t",
"--test",
required=True,
default="/my/path/test.txt",
help="Path leading to test blablabla",
cls=ResourceOption,
)
@verbosity_option(cls=ResourceOption)
def test(config, test, **kwargs):
"""Test command"""
pass
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(
cli, ['test', '-H', 'TEST_CONF'], catch_exceptions=False)
ref = pkg_resources.resource_filename('bob.extension',
'data/test_dump_config.py')
result = runner.invoke(cli, ["test", "-H", "TEST_CONF"], catch_exceptions=False)
ref = pkg_resources.resource_filename(
"bob.extension", "data/test_dump_config.py"
)
assert_click_runner_result(result)
_assert_config_dump(ref, '08/07/2018')
_assert_config_dump(ref, "08/07/2018")
def test_config_dump2():
......@@ -216,19 +229,38 @@ def test_config_dump2():
def cli():
pass
@cli.command(cls=ConfigCommand, entry_point_group='bob.extension.test_dump_config')
@click.option('--database', '-d', required=True, cls=ResourceOption,
entry_point_group='bob.extension.test_dump_config', help="bla bla bla")
@click.option('--annotator', '-a', required=True, cls=ResourceOption,
entry_point_group='bob.extension.test_dump_config', help="bli bli bli")
@click.option('--output-dir', '-o', required=True, cls=ResourceOption,
help="blo blo blo")
@click.option('--force', '-f', is_flag=True, cls=ResourceOption,
help="lalalalalala")
@click.option('--array', type=click.INT, default=1, cls=ResourceOption,
help="lililili")
@click.option('--database-directories-file', cls=ResourceOption,
default='~/databases.txt', help="lklklklk")
@cli.command(cls=ConfigCommand, entry_point_group="bob.extension.test_dump_config")
@click.option(
"--database",
"-d",
required=True,
cls=ResourceOption,
entry_point_group="bob.extension.test_dump_config",
help="bla bla bla",
)
@click.option(
"--annotator",
"-a",
required=True,
cls=ResourceOption,
entry_point_group="bob.extension.test_dump_config",
help="bli bli bli",
)
@click.option(
"--output-dir", "-o", required=True, cls=ResourceOption, help="blo blo blo"
)
@click.option(
"--force", "-f", is_flag=True, cls=ResourceOption, help="lalalalalala"
)
@click.option(
"--array", type=click.INT, default=1, cls=ResourceOption, help="lililili"
)
@click.option(
"--database-directories-file",
cls=ResourceOption,
default="~/databases.txt",
help="lklklklk",
)
@verbosity_option(cls=ResourceOption)
def test(**kwargs):
"""Blablabla bli blo
......@@ -246,55 +278,66 @@ def test_config_dump2():
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(
cli, ['test', '-H', 'TEST_CONF'], catch_exceptions=False)
ref = pkg_resources.resource_filename('bob.extension',
'data/test_dump_config2.py')
result = runner.invoke(cli, ["test", "-H", "TEST_CONF"], catch_exceptions=False)
ref = pkg_resources.resource_filename(
"bob.extension", "data/test_dump_config2.py"
)
assert_click_runner_result(result)
_assert_config_dump(ref, '08/07/2018')
_assert_config_dump(ref, "08/07/2018")
def test_config_command_with_callback_options():
@click.command(cls=ConfigCommand, entry_point_group='bob.extension.test_config_load')
@verbosity_option(cls=ResourceOption, envvar='VERBOSE')
@click.command(
cls=ConfigCommand, entry_point_group="bob.extension.test_config_load"
)
@verbosity_option(cls=ResourceOption, envvar="VERBOSE")
@click.pass_context
def cli(ctx, **kwargs):
verbose = ctx.meta['verbosity']
verbose = ctx.meta["verbosity"]
assert verbose == 2, verbose
runner = CliRunner()
result = runner.invoke(cli, ['verbose_config'], catch_exceptions=False)
result = runner.invoke(cli, ["verbose_config"], catch_exceptions=False)
assert_click_runner_result(result)
runner = CliRunner(env=dict(VERBOSE='1'))
result = runner.invoke(cli, ['verbose_config'], catch_exceptions=False)
runner = CliRunner(env=dict(VERBOSE="1"))
result = runner.invoke(cli, ["verbose_config"], catch_exceptions=False)
assert_click_runner_result(result)
runner = CliRunner(env=dict(VERBOSE='2'))
runner = CliRunner(env=dict(VERBOSE="2"))
result = runner.invoke(cli, catch_exceptions=False)
assert_click_runner_result(result)
def test_resource_option():
# tests of ResourceOption used with ConfigCommand are done in other tests.
# test usage without ConfigCommand and with entry_point_group
@click.command()
@click.option('-a', '--a', cls=ResourceOption, entry_point_group='bob.extension.test_config_load')
@click.option(
"-a",
"--a",
cls=ResourceOption,
entry_point_group="bob.extension.test_config_load",
)
def cli(a):
assert a == 1, a
runner = CliRunner()
result = runner.invoke(cli, ['-a', 'bob.extension.data.resource_config2'], catch_exceptions=False)
result = runner.invoke(
cli,
["-a", "bob.extension.data.resource_config2"],
catch_exceptions=False,
)
assert_click_runner_result(result)
# test usage without ConfigCommand and without entry_point_group
# should raise a TypeError
@click.command()
@click.option('-a', '--a', cls=ResourceOption)
@click.option("-a", "--a", cls=ResourceOption)
def cli(a):
raise ValueError("Should not have reached here!")
runner = CliRunner()
result = runner.invoke(cli, ['-a', '1'], catch_exceptions=True)
result = runner.invoke(cli, ["-a", "1"], catch_exceptions=True)
assert_click_runner_result(result, exit_code=1, exception_type=TypeError)
......@@ -63,22 +63,22 @@ def test_entry_point_configs():
def test_load_resource():
for p, ref in [
(os.path.join(path, 'resource_config2.py'), 1),
(os.path.join(path, 'resource_config2.py:test_config_load'), 1),
(os.path.join(path, 'resource_config2.py:a'), 1),
(os.path.join(path, 'resource_config2.py:b'), 2),
('resource1', 1),
('resource2', 2),
('bob.extension.data.resource_config2', 1),
('bob.extension.data.resource_config2:test_config_load', 1),
('bob.extension.data.resource_config2:a', 1),
('bob.extension.data.resource_config2:b', 2),
]:
c = load([p], entry_point_group='bob.extension.test_config_load',
attribute_name='test_config_load')
attribute_name='a')
assert c == ref, c
try:
load(['bob.extension.data.resource_config2:c'],
entry_point_group='bob.extension.test_config_load',
attribute_name='test_config_load')
attribute_name='a')
assert False, 'The code above should have raised an ImportError'
except ImportError:
pass
"""A script to help annotate databases.
"""
# Avoid importing packages here! Importing packages here will slowdown your command
# line's --help option and its auto-complete feature in terminal (if enabled). Instead,
# put your imports inside the function.
import logging
import click
from bob.extension.scripts.click_helper import (
......@@ -8,16 +11,13 @@ from bob.extension.scripts.click_helper import (
logger = logging.getLogger(__name__)
ANNOTATE_EPILOG = '''\b
@click.command(entry_point_group='bob.bio.config', cls=ConfigCommand,
epilog='''\b
Examples:
$ bob bio annotate -vvv -d <database> -a <annotator> -o /tmp/annotations
$ jman submit --array 64 -- bob bio annotate ... --array 64
'''
@click.command(entry_point_group='bob.bio.config', cls=ConfigCommand,
epilog=ANNOTATE_EPILOG)
''')
@click.option('--database', '-d', required=True, cls=ResourceOption,
entry_point_group='bob.bio.database',
help='''The database that you want to annotate.''')
......@@ -40,3 +40,8 @@ def annotate(database, annotator, output_dir, force, array, **kwargs):
back using :any:`bob.db.base.read_annotation_file` (annotation_type='json')
"""
log_parameters(logger)
# Add imports needed for your code here:
import numpy as np
np.zeros(10)
......@@ -17,7 +17,7 @@ Python-based Configuration System
---------------------------------
This package also provides a configuration system that can be used by packages
in the |project|-echosystem to load *run-time* configuration for applications
in the |project|-ecosystem to load *run-time* configuration for applications
(for package-level static variable configuration use :ref:`bob.extension.rc`).
It can be used to accept complex configurations from users through
command-line.
......@@ -59,7 +59,7 @@ Then, the object ``configuration`` would look like this:
.. doctest::
>>> print("a = %d\nb = %d"%(configuration.a, configuration.b))
>>> print(f"a = {configuration.a}\nb = {configuration.b}")
a = 1
b = 3
......@@ -96,9 +96,10 @@ Then, one can chain-load them like this:
>>> file1 = os.path.join(path, 'basic_config.py')
>>> file2 = os.path.join(path, 'load_config.py')
>>> configuration = load([file1, file2])
>>> print("a = %d \nb = %d"%(configuration.a, configuration.b)) # doctest: +NORMALIZE_WHITESPACE
>>> print(f"a = {configuration.a} \nb = {configuration.b} \nc = {configuration.c}") # doctest: +NORMALIZE_WHITESPACE
a = 1
b = 6
c = 4
The user wanting to override the values needs to manage the overriding and the
......@@ -123,6 +124,8 @@ to provide the group name of the entry points:
b = 6
.. _bob.extension.config.resource:
Resource Loading
================
......@@ -140,73 +143,16 @@ The loaded value can be either 1 or 2:
.. doctest:: load_resource
>>> group = 'bob.extension.test_config_load' # the group name of entry points
>>> attribute_name = 'test_config_load' # the common variable name
>>> attribute_name = 'a' # the common variable name
>>> value = load(['bob.extension.data.resource_config2'], entry_point_group=group, attribute_name=attribute_name)
>>> value == 1
True
>>> # attribute_name can be ovverriden using the `path:attribute_name` syntax
>>> value = load(['bob.extension.data.resource_config2:b'], entry_point_group=group, attribute_name=attribute_name)
>>> value == 2
True
.. _bob.extension.processors:
Stacked Processing
------------------
:any:`bob.extension.processors.SequentialProcessor` and
:any:`bob.extension.processors.ParallelProcessor` are provided to help you
build complex processing mechanisms. You can use these processors to apply a
chain of processes on your data. For example,
:any:`bob.extension.processors.SequentialProcessor` accepts a list of callables
and applies them on the data one by one sequentially. :
.. doctest::
>>> import numpy as np; from numpy import array
>>> from functools import partial
>>> from bob.extension.processors import SequentialProcessor
>>> raw_data = np.array([[1, 2, 3], [1, 2, 3]])
>>> seq_processor = SequentialProcessor(
... [np.cast['float64'], lambda x: x / 2, partial(np.mean, axis=1)])
>>> np.allclose(seq_processor(raw_data),
... array([ 1., 1.]))
True
>>> np.all(seq_processor(raw_data) ==
... np.mean(np.cast['float64'](raw_data) / 2, axis=1))
True
:any:`bob.extension.processors.ParallelProcessor` accepts a list of callables
and applies each them on the data independently and returns all the results.
For example:
.. doctest::
>>> from bob.extension.processors import ParallelProcessor
>>> raw_data = np.array([[1, 2, 3], [1, 2, 3]])
>>> parallel_processor = ParallelProcessor(
... [np.cast['float64'], lambda x: x / 2.0])
>>> np.allclose(list(parallel_processor(raw_data)),
... [array([[ 1., 2., 3.],
... [ 1., 2., 3.]]),
... array([[ 0.5, 1. , 1.5],
... [ 0.5, 1. , 1.5]])])
True
The data may be further processed using a
:any:`bob.extension.processors.SequentialProcessor`:
.. doctest::
>>> total_processor = SequentialProcessor(
... [parallel_processor, list, partial(np.concatenate, axis=1)])
>>> np.allclose(total_processor(raw_data),
... array([[ 1. , 2. , 3. , 0.5, 1. , 1.5],
... [ 1. , 2. , 3. , 0.5, 1. , 1.5]]))
True
.. _bob.extension.cli:
Unified Command Line Mechanism
......@@ -228,11 +174,6 @@ commands by default::
config The manager for bob's global configuration.
...
.. warning::
This feature is experimental and most probably will break compatibility.
If you are not willing to fix your code after changes are made here,
please do not use this feature.
This command line is implemented using click_. You can extend the commands of
this script through setuptools entry points (this is implemented using
......@@ -243,15 +184,13 @@ independently; then, advertise it as a command under bob script using the
.. note::
If you are still not sure how this must be done, maybe you don't know how
to use click_ yet.
to use click_ and `click-plugins`_ 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.
:caption: "bob/extension/scripts/config.py" implementation of the ``bob config`` command.