From 8f62fd08017516bb580761b67e5ed6509cf04647 Mon Sep 17 00:00:00 2001
From: Amir MOHAMMADI <amir.mohammadi@idiap.ch>
Date: Thu, 2 Jan 2020 15:37:05 +0100
Subject: [PATCH] [click] Change the ConfigCommand and ResourceOption logic
 Fixes #66

---
 bob/extension/data/defaults-config    |   2 +-
 bob/extension/data/verbose_config.py  |   1 +
 bob/extension/scripts/click_helper.py | 774 ++++++++++++++------------
 bob/extension/test_click_helper.py    |  60 +-
 setup.py                              |   1 +
 5 files changed, 456 insertions(+), 382 deletions(-)
 create mode 100644 bob/extension/data/verbose_config.py

diff --git a/bob/extension/data/defaults-config b/bob/extension/data/defaults-config
index f332e2b..39e8f86 100644
--- a/bob/extension/data/defaults-config
+++ b/bob/extension/data/defaults-config
@@ -1,4 +1,4 @@
 {
     "bob.db.atnt.directory": "/home/bob/databases/atnt",
     "bob.db.mobio.directory": "/home/bob/databases/mobio"
-}
+}
\ No newline at end of file
diff --git a/bob/extension/data/verbose_config.py b/bob/extension/data/verbose_config.py
new file mode 100644
index 0000000..181d8cd
--- /dev/null
+++ b/bob/extension/data/verbose_config.py
@@ -0,0 +1 @@
+verbose = 2
diff --git a/bob/extension/scripts/click_helper.py b/bob/extension/scripts/click_helper.py
index b8d4667..3b5e8f6 100644
--- a/bob/extension/scripts/click_helper.py
+++ b/bob/extension/scripts/click_helper.py
@@ -5,399 +5,457 @@ import click
 import logging
 import traceback
 
-# This needs to be bob so that logger is configured for all bob packages.
-logger = logging.getLogger('bob')
+logger = logging.getLogger(__name__)
 try:
-  basestring
+    basestring
 except NameError:
-  basestring = str
+    basestring = str
 
 
 def bool_option(name, short_name, desc, dflt=False, **kwargs):
-  '''Generic provider for boolean options
-
-  Parameters
-  ----------
-  name : str
-      name of the option
-  short_name : str
-      short name for the option
-  desc : str
-      short description for the option
-  dflt : bool or None
-      Default value
-  **kwargs
-      All kwargs are passed to click.option.
-
-  Returns
-  -------
-  callable
-      A decorator to be used for adding this option.
-  '''
-  def custom_bool_option(func):
-    def callback(ctx, param, value):
-      ctx.meta[name.replace('-', '_')] = value
-      return value
-    return click.option(
-        '-%s/-n%s' % (short_name, short_name), '--%s/--no-%s' % (name, name),
-        default=dflt, help=desc, show_default=True, callback=callback,
-        is_eager=True, **kwargs)(func)
-  return custom_bool_option
-
-
-def list_float_option(name, short_name, desc, nitems=None, dflt=None,
-                      **kwargs):
-  '''Get option to get a list of float f
-
-  Parameters
-  ----------
-  name : str
-      name of the option
-  short_name : str
-      short name for the option
-  desc : str
-      short description for the option
-  nitems : obj:`int`, optional
-      If given, the parsed list must contains this number of items.
-  dflt : :any:`list`, optional
-      List of default  values for axes.
-  **kwargs
-      All kwargs are passed to click.option.
-
-  Returns
-  -------
-  callable
-      A decorator to be used for adding this option.
-  '''
-  def custom_list_float_option(func):
-    def callback(ctx, param, value):
-      if value is None or not value.replace(' ', ''):
-        value = None
-      elif value is not None:
-        tmp = value.split(',')
-        if nitems is not None and len(tmp) != nitems:
-          raise click.BadParameter(
-              '%s Must provide %d axis limits' % (name, nitems)
-          )
-        try:
-          value = [float(i) for i in tmp]
-        except Exception:
-          raise click.BadParameter('Inputs of %s be floats' % name)
-      ctx.meta[name.replace('-', '_')] = value
-      return value
-    return click.option(
-        '-' + short_name, '--' + name, default=dflt, show_default=True,
-        help=desc + ' Provide just a space (\' \') to cancel default values.',
-        callback=callback, **kwargs)(func)
-  return custom_list_float_option
+    """Generic provider for boolean options
+
+    Parameters
+    ----------
+    name : str
+        name of the option
+    short_name : str
+        short name for the option
+    desc : str
+        short description for the option
+    dflt : bool or None
+        Default value
+    **kwargs
+        All kwargs are passed to click.option.
+
+    Returns
+    -------
+    callable
+        A decorator to be used for adding this option.
+    """
+
+    def custom_bool_option(func):
+        def callback(ctx, param, value):
+            ctx.meta[name.replace("-", "_")] = value
+            return value
+
+        return click.option(
+            "-%s/-n%s" % (short_name, short_name),
+            "--%s/--no-%s" % (name, name),
+            default=dflt,
+            help=desc,
+            show_default=True,
+            callback=callback,
+            is_eager=True,
+            **kwargs
+        )(func)
+
+    return custom_bool_option
+
+
+def list_float_option(name, short_name, desc, nitems=None, dflt=None, **kwargs):
+    """Get option to get a list of float f
+
+    Parameters
+    ----------
+    name : str
+        name of the option
+    short_name : str
+        short name for the option
+    desc : str
+        short description for the option
+    nitems : obj:`int`, optional
+        If given, the parsed list must contains this number of items.
+    dflt : :any:`list`, optional
+        List of default  values for axes.
+    **kwargs
+        All kwargs are passed to click.option.
+
+    Returns
+    -------
+    callable
+        A decorator to be used for adding this option.
+    """
+
+    def custom_list_float_option(func):
+        def callback(ctx, param, value):
+            if value is None or not value.replace(" ", ""):
+                value = None
+            elif value is not None:
+                tmp = value.split(",")
+                if nitems is not None and len(tmp) != nitems:
+                    raise click.BadParameter(
+                        "%s Must provide %d axis limits" % (name, nitems)
+                    )
+                try:
+                    value = [float(i) for i in tmp]
+                except Exception:
+                    raise click.BadParameter("Inputs of %s be floats" % name)
+            ctx.meta[name.replace("-", "_")] = value
+            return value
+
+        return click.option(
+            "-" + short_name,
+            "--" + name,
+            default=dflt,
+            show_default=True,
+            help=desc + " Provide just a space (' ') to cancel default values.",
+            callback=callback,
+            **kwargs
+        )(func)
+
+    return custom_list_float_option
 
 
 def open_file_mode_option(**kwargs):
-  '''Get open mode file option
-
-  Parameters
-  ----------
-  **kwargs
-      All kwargs are passed to click.option.
-
-  Returns
-  -------
-  callable
-      A decorator to be used for adding this option.
-  '''
-  def custom_open_file_mode_option(func):
-    def callback(ctx, param, value):
-      if value not in ['w', 'a', 'w+', 'a+']:
-        raise click.BadParameter('Incorrect open file mode')
-      ctx.meta['open_mode'] = value
-      return value
-    return click.option(
-        '-om', '--open-mode', default='w',
-        help='File open mode',
-        callback=callback, **kwargs)(func)
-  return custom_open_file_mode_option
+    """Get open mode file option
 
+    Parameters
+    ----------
+    **kwargs
+        All kwargs are passed to click.option.
 
-def verbosity_option(**kwargs):
-  """Adds a -v/--verbose option to a click command.
-
-  Parameters
-  ----------
-  **kwargs
-      All kwargs are passed to click.option.
-
-  Returns
-  -------
-  callable
-      A decorator to be used for adding this option.
-  """
-  def custom_verbosity_option(f):
-    def callback(ctx, param, value):
-      ctx.meta['verbosity'] = value
-      set_verbosity_level(logger, value)
-      logger.debug("Logging of the `bob' logger was set to %d", value)
-      return value
-    return 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).",
-        callback=callback, **kwargs)(f)
-  return custom_verbosity_option
+    Returns
+    -------
+    callable
+        A decorator to be used for adding this option.
+    """
 
+    def custom_open_file_mode_option(func):
+        def callback(ctx, param, value):
+            if value not in ["w", "a", "w+", "a+"]:
+                raise click.BadParameter("Incorrect open file mode")
+            ctx.meta["open_mode"] = value
+            return value
 
-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
-  :any:`ResourceOption` class also.
+        return click.option(
+            "-om",
+            "--open-mode",
+            default="w",
+            help="File open mode",
+            callback=callback,
+            **kwargs
+        )(func)
 
-  Attributes
-  ----------
-  config_argument_name : str
-      The name of the config argument.
-  entry_point_group : str
-      The name of entry point that will be used to load the config files.
-  """
-
-  def __init__(self, name, context_settings=None, callback=None, params=None,
-               help=None, epilog=None, short_help=None,
-               options_metavar='[OPTIONS]',
-               add_help_option=True, entry_point_group=None,
-               config_argument_name='CONFIG', **kwargs):
-    self.config_argument_name = config_argument_name
-    self.entry_point_group = entry_point_group
-    # Augment help for the config file argument
-    self.extra_help = '''\n\nIt is possible to pass one or several Python files
-(or names of ``{entry_point_group}`` entry points or module names) as {CONFIG}
-arguments to the command line which contain the parameters listed below as
-Python variables. The options through the command-line (see below) will
-override the values of configuration files. You can run this command with
-``<COMMAND> -H example_config.py`` to create a template config
-file.'''.format(CONFIG=config_argument_name,
-                entry_point_group=entry_point_group)
-    help = (help or '').rstrip() + self.extra_help
-    # kwargs['help'] = help
-    click.Command.__init__(
-        self, name, context_settings=context_settings, callback=callback,
-        params=params, help=help, epilog=epilog, short_help=short_help,
-        options_metavar=options_metavar, add_help_option=add_help_option,
-        **kwargs)
-    # Add the config argument to the command
-    click.argument(config_argument_name, nargs=-1)(self)
-    # Option for config file generation
-    click.option('-H', '--dump-config', type=click.File(mode='wt'),
-                 help="Name of the config file to be generated")(self)
-
-  def is_resource(self, param, ctx):
-    """Checks if the param is an option and is also in the current context."""
-    return (param.name in ctx.params and
-            param.name != 'dump_config' and
-            isinstance(param, click.Option))
-
-  def invoke(self, ctx):
-    dump_file = ctx.params.get('dump_config')
-    if dump_file is not None:
-      click.echo("Configuration file '{}' was written; exiting".format(
-          dump_file.name))
-      return self.dump_config(ctx)
-    config_files = ctx.params[self.config_argument_name.lower()]
-    # load and normalize context from config files
-    config_context = load(
-        config_files, entry_point_group=self.entry_point_group)
-    config_context = mod_to_context(config_context)
-    for param in self.params:
-      if not self.is_resource(param, ctx):
-        continue
-      value = ctx.params[param.name]
-      if not hasattr(param, 'user_provided'):
-        if value == param.default:
-          param.user_provided = False
-        else:
-          param.user_provided = True
-      if not param.user_provided and param.name in config_context:
-        ctx.params[param.name] = param.full_process_value(
-            ctx, config_context[param.name])
-      # raise exceptions if the value is required.
-      if hasattr(param, 'real_required'):
-        param.required = param.real_required
-        try:
-          ctx.params[param.name] = param.full_process_value(
-              ctx, ctx.params[param.name])
-        finally:
-          # make sure to set this back to False for future invocations
-          param.required = False
-
-    return super(ConfigCommand, self).invoke(ctx)
-
-  def dump_config(self, ctx):
-    """Generate configuration file from parameters and context
+    return custom_open_file_mode_option
+
+
+def verbosity_option(**kwargs):
+    """Adds a -v/--verbose option to a click command.
 
     Parameters
     ----------
-    ctx : object
-        Click context
+    **kwargs
+        All kwargs are passed to click.option.
+
+    Returns
+    -------
+    callable
+        A decorator to be used for adding this option.
     """
-    config_file = ctx.params['dump_config']
-    logger.debug("Generating configuration file `%s'...", config_file)
-    config_file.write("'''")
-    config_file.write('Configuration file automatically generated at '
-                      '%s\n%s\n' % (time.strftime("%d/%m/%Y"),
-                                    ctx.command_path))
 
-    if self.help:
-      h = self.help.replace(self.extra_help, '').replace('\b\n', '')
-      config_file.write('\n{}'.format(h.rstrip()))
+    def custom_verbosity_option(f):
+        def callback(ctx, param, value):
+            ctx.meta["verbosity"] = value
+            # This needs to be bob so that logger is configured for all bob packages.
+            logger = logging.getLogger("bob")
+            set_verbosity_level(logger, value)
+            logger.debug("Logging of the `bob' logger was set to %d", value)
+            return value
 
-    if self.epilog:
-      config_file.write('\n\n{}'.format(self.epilog.replace('\b\n', '')))
+        return 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).",
+            callback=callback,
+            **kwargs
+        )(f)
 
-    config_file.write("'''\n")
+    return custom_verbosity_option
 
-    for param in self.params:
-      if not self.is_resource(param, ctx):
-        continue
 
-      config_file.write('\n# %s = %s\n' % (param.name,
-                                           str(ctx.params[param.name])))
-      config_file.write("'''")
+def configs_argument(cmd, config_argument_name, entry_point_group):
+    """This a
+
+    Parameters
+    ----------
+    cmd : TYPE
+        Description
+    config_argument_name : TYPE
+        Description
+    entry_point_group : TYPE
+        Description
+    """
 
-      if param.required or (isinstance(param, ResourceOption) and
-                            param.real_required):
-        begin, dflt = 'Required parameter', ''
-      else:
-        begin, dflt = 'Optional parameter', ' [default: {}]'.format(
-            param.default)
-      config_file.write(
-          "%s: %s (%s)%s" % (
-              begin, param.name, ', '.join(param.opts), dflt))
 
-      if param.help is not None:
-        config_file.write("\n%s" % param.help)
+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
+    :any:`ResourceOption` class also.
 
-      if isinstance(param, ResourceOption) and \
-              param.entry_point_group is not None:
-        config_file.write("\nRegistered entries are: {}".format(
-            resource_keys(param.entry_point_group)))
+    Attributes
+    ----------
+    config_argument_name : str
+      The name of the config argument.
+    entry_point_group : str
+      The name of entry point that will be used to load the config files.
+    """
 
-      config_file.write("'''\n")
+    def __init__(
+        self,
+        name,
+        *args,
+        help=None,
+        entry_point_group=None,
+        **kwargs
+    ):
+        self.entry_point_group = entry_point_group
+        configs_argument_name = "CONFIG"
+        # Augment help for the config file argument
+        self.extra_help = """\n\nIt is possible to pass one or several Python files
+(or names of ``{entry_point_group}`` entry points or module names) as {CONFIG}
+arguments to the command line which contain the parameters listed below as
+Python variables. The options through the command-line (see below) will
+override the values of configuration files. You can run this command with
+``<COMMAND> -H example_config.py`` to create a template config
+file.""".format(
+            CONFIG=configs_argument_name, entry_point_group=entry_point_group
+        )
+        help = (help or "").rstrip() + self.extra_help
+        super().__init__(name, *args, help=help, **kwargs)
+
+        # Add the config argument to the command
+        def configs_argument_callback(ctx, param, value):
+            config_context = load(value, entry_point_group=self.entry_point_group)
+            config_context = mod_to_context(config_context)
+            ctx.config_context = config_context
+            logger.debug("Augmenting context with config context")
+            return value
+
+        click.argument(
+            configs_argument_name,
+            nargs=-1,
+            callback=configs_argument_callback,
+            is_eager=True,
+        )(self)
+
+        # Option for config file generation
+        click.option(
+            "-H",
+            "--dump-config",
+            type=click.File(mode="wt"),
+            help="Name of the config file to be generated",
+            is_eager=True,
+            callback=self.dump_config,
+        )(self)
+
+    def dump_config(self, ctx, param, value):
+        """Generate configuration file from parameters and context
+
+        Parameters
+        ----------
+        ctx : object
+            Click context
+        """
+        config_file = value
+        if config_file is None:
+            return
+        logger.debug("Generating configuration file `%s'...", config_file)
+        config_file.write("'''")
+        config_file.write(
+            "Configuration file automatically generated at "
+            "%s\n%s\n" % (time.strftime("%d/%m/%Y"), ctx.command_path)
+        )
+
+        if self.help:
+            h = self.help.replace(self.extra_help, "").replace("\b\n", "")
+            config_file.write("\n{}".format(h.rstrip()))
+
+        if self.epilog:
+            config_file.write("\n\n{}".format(self.epilog.replace("\b\n", "")))
+
+        config_file.write("'''\n")
+
+        for param in self.params:
+            if not isinstance(param, ResourceOption):
+                continue
+
+            config_file.write("\n# %s = %s\n" % (param.name, str(param.default)))
+            config_file.write("'''")
+
+            if param.required:
+                begin, dflt = "Required parameter", ""
+            else:
+                begin, dflt = (
+                    "Optional parameter",
+                    " [default: {}]".format(param.default),
+                )
+            config_file.write(
+                "%s: %s (%s)%s" % (begin, param.name, ", ".join(param.opts), dflt)
+            )
+
+            if param.help is not None:
+                config_file.write("\n%s" % param.help)
+
+            if (
+                isinstance(param, ResourceOption)
+                and param.entry_point_group is not None
+            ):
+                config_file.write(
+                    "\nRegistered entries are: {}".format(
+                        resource_keys(param.entry_point_group)
+                    )
+                )
+
+            config_file.write("'''\n")
+            click.echo(
+                "Configuration file '{}' was written; exiting".format(config_file.name)
+            )
+
+        config_file.close()
+        ctx.exit()
 
 
 class ResourceOption(click.Option):
-  """A click.Option that is aware if the user actually provided this option
-  through command-line or it holds a default value. The option can also be a
-  resource that will be automatically loaded.
-
-  Attributes
-  ----------
-  entry_point_group : str or None
-      If provided, the strings values to this option are assumed to be entry
-      points from ``entry_point_group`` that need to be loaded.
-  real_required : bool
-      Holds the real value of ``required`` here. The ``required`` value is
-      hidden from click since the option may be loaded later through the
-      configuration files.
-  user_provided : bool
-      True if the user actually provided this option through command-line or
-      using environment variables.
-  """
-
-  def __init__(self, param_decls=None, show_default=False, prompt=False,
-               confirmation_prompt=False, hide_input=False, is_flag=None,
-               flag_value=None, multiple=False, count=False,
-               allow_from_autoenv=True, type=None, help=None,
-               entry_point_group=None, required=False, **kwargs):
-    self.entry_point_group = entry_point_group
-    self.real_required = required
-    kwargs['required'] = False
-    if entry_point_group is not None:
-      name, _, _ = self._parse_decls(param_decls, kwargs.get('expose_value'))
-      help = help or ''
-      help += (
-          ' Can be a ``{entry_point_group}`` entry point, a module name, or '
-          'a path to a Python file which contains a variable named `{name}`.')
-      help = help.format(entry_point_group=entry_point_group, name=name)
-    click.Option.__init__(
-        self, param_decls=param_decls, show_default=show_default,
-        prompt=prompt, confirmation_prompt=confirmation_prompt,
-        hide_input=hide_input, is_flag=is_flag, flag_value=flag_value,
-        multiple=multiple, count=count, allow_from_autoenv=allow_from_autoenv,
-        type=type, help=help, **kwargs)
-
-  def consume_value(self, ctx, opts):
-    value = opts.get(self.name)
-    self.user_provided = True
-    if value is None:
-      value = ctx.lookup_default(self.name)
-      self.user_provided = False
-    if value is None:
-      value = self.value_from_envvar(ctx)
-      if value is not None:
-        self.user_provided = True
-    return value
-
-  def full_process_value(self, ctx, value):
-    value = super(ResourceOption,
-                  self).full_process_value(ctx, value)
-
-    if self.entry_point_group is not None:
-      attribute_name = self.entry_point_group.split('.')[-1]
-      while isinstance(value, basestring):
-        value = load([value], entry_point_group=self.entry_point_group,
-                     attribute_name=attribute_name)
-    return value
+    """A click.Option that is aware if the user actually provided this option
+    through command-line or it holds a default value. The option can also be a
+    resource that will be automatically loaded.
+
+    Attributes
+    ----------
+    entry_point_group : str or None
+        If provided, the strings values to this option are assumed to be entry
+        points from ``entry_point_group`` that need to be loaded.
+    """
+
+    def __init__(
+        self,
+        param_decls=None,
+        show_default=False,
+        prompt=False,
+        confirmation_prompt=False,
+        hide_input=False,
+        is_flag=None,
+        flag_value=None,
+        multiple=False,
+        count=False,
+        allow_from_autoenv=True,
+        type=None,
+        help=None,
+        entry_point_group=None,
+        required=False,
+        **kwargs
+    ):
+        self.entry_point_group = entry_point_group
+        if entry_point_group is not None:
+            name, _, _ = self._parse_decls(param_decls, kwargs.get("expose_value"))
+            help = help or ""
+            help += (
+                " Can be a ``{entry_point_group}`` entry point, a module name, or "
+                "a path to a Python file which contains a variable named `{name}`."
+            )
+            help = help.format(entry_point_group=entry_point_group, name=name)
+        super().__init__(
+            param_decls=param_decls,
+            show_default=show_default,
+            prompt=prompt,
+            confirmation_prompt=confirmation_prompt,
+            hide_input=hide_input,
+            is_flag=is_flag,
+            flag_value=flag_value,
+            multiple=multiple,
+            count=count,
+            allow_from_autoenv=allow_from_autoenv,
+            type=type,
+            help=help,
+            required=required,
+            **kwargs
+        )
+
+    def consume_value(self, ctx, opts):
+        logger.debug("consuming resource option for option %s", self.name)
+        value = opts.get(self.name)
+        # if value is not given from command line, lookup the environment variables
+        if value is None:
+            value = self.value_from_envvar(ctx)
+        # if not from environment variables, lookup the config files
+        if value is None:
+            value = ctx.config_context.get(self.name)
+        # if not from config files, lookup the default value
+        if value is None:
+            value = ctx.lookup_default(self.name)
+        return value
+
+    def full_process_value(self, ctx, value):
+        value = super().full_process_value(ctx, value)
+
+        # 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):
+                value = load(
+                    [value],
+                    entry_point_group=self.entry_point_group,
+                    attribute_name=attribute_name,
+                )
+        return value
 
 
 class AliasedGroup(click.Group):
-  ''' Class that handles prefix aliasing for commands
-
-  Basically just implements get_command that is used by click to choose the
-  comamnd based on the name.
-
-  Example
-  -------
-  To enable prefix aliasing of commands for a given group,
-  just set ``cls=AliasedGroup`` parameter in click.group decorator.
-  '''
-
-  def get_command(self, ctx, cmd_name):
-    rv = click.Group.get_command(self, ctx, cmd_name)
-    if rv is not None:
-      return rv
-    matches = [x for x in self.list_commands(ctx)
-               if x.startswith(cmd_name)]
-    if not matches:
-      return None
-    elif len(matches) == 1:
-      return click.Group.get_command(self, ctx, matches[0])
-    ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))
+    """ Class that handles prefix aliasing for commands
+
+    Basically just implements get_command that is used by click to choose the
+    comamnd based on the name.
+
+    Example
+    -------
+    To enable prefix aliasing of commands for a given group,
+    just set ``cls=AliasedGroup`` parameter in click.group decorator.
+    """
+
+    def get_command(self, ctx, cmd_name):
+        rv = click.Group.get_command(self, ctx, cmd_name)
+        if rv is not None:
+            return rv
+        matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]
+        if not matches:
+            return None
+        elif len(matches) == 1:
+            return click.Group.get_command(self, ctx, matches[0])
+        ctx.fail("Too many matches: %s" % ", ".join(sorted(matches)))
 
 
 def log_parameters(logger_handle, ignore=tuple()):
-  """Logs the click parameters with the logging module.
-
-  Parameters
-  ----------
-  logger_handle : object
-      The logger handle to write debug information into.
-  ignore : tuple
-      The keys in ignore will not be logged.
-  """
-  ctx = click.get_current_context()
-  # do not sort the ctx.params dict. The insertion order is kept in Python 3
-  # and is useful (but not necessary so works on Python 2 too).
-  for k, v in ctx.params.items():
-    if k in ignore:
-      continue
-    logger_handle.debug('%s: %s', k, v)
+    """Logs the click parameters with the logging module.
+
+    Parameters
+    ----------
+    logger_handle : object
+        The logger handle to write debug information into.
+    ignore : tuple
+        The keys in ignore will not be logged.
+    """
+    ctx = click.get_current_context()
+    # do not sort the ctx.params dict. The insertion order is kept in Python 3
+    # and is useful (but not necessary so works on Python 2 too).
+    for k, v in ctx.params.items():
+        if k in ignore:
+            continue
+        logger_handle.debug("%s: %s", k, v)
 
 
 def assert_click_runner_result(result, exit_code=0):
-  """Helper for asserting click runner results"""
-  m = ("Click command exited with code `{}' and exception:\n{}"
-       "\nThe output was:\n{}")
-  exception = 'None' if result.exc_info is None else \
-      ''.join(traceback.format_exception(*result.exc_info))
-  m = m.format(result.exit_code, exception, result.output)
-  assert result.exit_code == exit_code, m
-  if exit_code == 0:
-    assert not result.exception, m
+    """Helper for asserting click runner results"""
+    m = "Click command exited with code `{}' and exception:\n{}" "\nThe output was:\n{}"
+    exception = (
+        "None"
+        if result.exc_info is None
+        else "".join(traceback.format_exception(*result.exc_info))
+    )
+    m = m.format(result.exit_code, exception, result.output)
+    assert result.exit_code == exit_code, m
+    if exit_code == 0:
+        assert not result.exception, m
diff --git a/bob/extension/test_click_helper.py b/bob/extension/test_click_helper.py
index a31499a..5ea42e9 100644
--- a/bob/extension/test_click_helper.py
+++ b/bob/extension/test_click_helper.py
@@ -4,7 +4,7 @@ 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)
+    ConfigCommand, ResourceOption, AliasedGroup, assert_click_runner_result)
 
 
 def test_verbosity_option():
@@ -20,7 +20,7 @@ def test_verbosity_option():
 
         runner = CliRunner()
         result = runner.invoke(cli, OPTIONS, catch_exceptions=False)
-        assert result.exit_code == 0, (result.exit_code, result.output)
+        assert_click_runner_result(result)
 
 
 def test_bool_option():
@@ -43,10 +43,10 @@ def test_bool_option():
 
     runner = CliRunner()
     result = runner.invoke(cli)
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
 
     result = runner.invoke(cli2)
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
 
 
 def test_list_float_option():
@@ -61,7 +61,7 @@ def test_list_float_option():
 
     runner = CliRunner()
     result = runner.invoke(cli, ['-T', '1,2,3'])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
 
 
 def test_list_float_option_empty():
@@ -75,7 +75,7 @@ def test_list_float_option_empty():
 
     runner = CliRunner()
     result = runner.invoke(cli, ['-T', ' '])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
 
 
 def test_commands_with_config_1():
@@ -87,7 +87,7 @@ def test_commands_with_config_1():
 
     runner = CliRunner()
     result = runner.invoke(cli, ['basic_config'])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
 
 
 def test_commands_with_config_2():
@@ -102,23 +102,23 @@ def test_commands_with_config_2():
     runner = CliRunner()
 
     result = runner.invoke(cli, [])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert result.output.strip() == '3', result.output
 
     result = runner.invoke(cli, ['basic_config'])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert result.output.strip() == '1', result.output
 
     result = runner.invoke(cli, ['-a', 2])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert result.output.strip() == '2', result.output
 
     result = runner.invoke(cli, ['-a', 3, 'basic_config'])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert result.output.strip() == '3', result.output
 
     result = runner.invoke(cli, ['basic_config', '-a', 3])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert result.output.strip() == '3', result.output
 
 
@@ -134,22 +134,22 @@ def test_commands_with_config_3():
     runner = CliRunner()
 
     result = runner.invoke(cli, [])
-    assert result.exit_code == 2, (result.exit_code, result.output)
+    assert_click_runner_result(result, exit_code=2)
 
     result = runner.invoke(cli, ['basic_config'])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert result.output.strip() == '1', result.output
 
     result = runner.invoke(cli, ['-a', 2])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert result.output.strip() == '2', result.output
 
     result = runner.invoke(cli, ['-a', 3, 'basic_config'])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert result.output.strip() == '3', result.output
 
     result = runner.invoke(cli, ['basic_config', '-a', 3])
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert result.output.strip() == '3', result.output
 
 
@@ -171,11 +171,11 @@ def test_prefix_aliasing():
     assert result.exit_code != 0, (result.exit_code, result.output)
 
     result = runner.invoke(cli, ['test'], catch_exceptions=False)
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert 'OK' in result.output, (result.exit_code, result.output)
 
     result = runner.invoke(cli, ['test-a'], catch_exceptions=False)
-    assert result.exit_code == 0, (result.exit_code, result.output)
+    assert_click_runner_result(result)
     assert 'AAA' in result.output, (result.exit_code, result.output)
 
 
@@ -186,7 +186,7 @@ def _assert_config_dump(ref, ref_date):
     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, ref_text])
+        assert text == ref_text, '\n'.join([text, "########################\n"*2, ref_text])
 
 
 def test_config_dump():
@@ -197,7 +197,7 @@ def test_config_dump():
     @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()
+    @verbosity_option(cls=ResourceOption)
     def test(config, test, **kwargs):
         """Test command"""
         pass
@@ -207,7 +207,7 @@ def test_config_dump():
             cli, ['test', '-H', 'TEST_CONF'], catch_exceptions=False)
         ref = pkg_resources.resource_filename('bob.extension',
                                               'data/test_dump_config.py')
-        assert result.exit_code == 0, (result.exit_code, result.output)
+        assert_click_runner_result(result)
         _assert_config_dump(ref, '08/07/2018')
 
 
@@ -250,5 +250,19 @@ def test_config_dump2():
             cli, ['test', '-H', 'TEST_CONF'], catch_exceptions=False)
         ref = pkg_resources.resource_filename('bob.extension',
                                               'data/test_dump_config2.py')
-        assert result.exit_code == 0, (result.exit_code, result.output)
+        assert_click_runner_result(result)
         _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)
+    @click.pass_context
+    def cli(ctx, **kwargs):
+        verbose = ctx.meta['verbosity']
+        assert verbose == 2, verbose
+
+    runner = CliRunner()
+    result = runner.invoke(cli, ['verbose_config'], catch_exceptions=False)
+    assert_click_runner_result(result)
diff --git a/setup.py b/setup.py
index 44bc6c8..b20fb58 100644
--- a/setup.py
+++ b/setup.py
@@ -34,6 +34,7 @@ setup(
         # some test entry_points
         'bob.extension.test_config_load': [
             'basic_config = bob.extension.data.basic_config',
+            'verbose_config = bob.extension.data.verbose_config',
             'resource_config = bob.extension.data.resource_config',
             'subpackage_config = bob.extension.data.subpackage.config',
             'resource1 = bob.extension.data.resource_config2',
-- 
GitLab