Skip to content

Commit

Permalink
remove support for option forwarding
Browse files Browse the repository at this point in the history
  • Loading branch information
n8pease committed Aug 22, 2020
1 parent bf86e34 commit 3e84917
Show file tree
Hide file tree
Showing 2 changed files with 5 additions and 225 deletions.
133 changes: 2 additions & 131 deletions python/lsst/daf/butler/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,10 +510,7 @@ def convert(self, value, param, ctx):
class MWOption(click.Option):
"""Overrides click.Option with desired behaviors."""

def __init__(self, *args, forward=False, **kwargs):
# `forward` indicates weather a subcommand should forward the value of
# this option to the next subcommand or not.
self.forward = forward
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def make_metavar(self):
Expand All @@ -538,15 +535,6 @@ def make_metavar(self):
metavar = f"{metavar[:-3]} ..."
return metavar

def get_help_record(self, ctx):
"""Overrides `click.Option.get_help_record`. Adds "(f)" to this
option's metavar text if its associated subcommand forwards this option
value to the next subcommand."""
rv = super().get_help_record(ctx)
if self.forward:
rv = (rv[0] + " (f)",) + rv[1:]
return rv


class MWArgument(click.Argument):
"""Overrides click.Argument with desired behaviors."""
Expand Down Expand Up @@ -609,8 +597,7 @@ class MWOptionDecorator:
"""

def __init__(self, *param_decls, **kwargs):
forward = kwargs.pop("forward", False)
self.partialOpt = partial(click.option, *param_decls, cls=partial(MWOption, forward=forward),
self.partialOpt = partial(click.option, *param_decls, cls=partial(MWOption),
**kwargs)
opt = click.Option(param_decls, **kwargs)
self._name = opt.name
Expand Down Expand Up @@ -684,119 +671,3 @@ def getFrom(ctx):
return ctx.obj
ctx.obj = MWCtxObj()
return ctx.obj


class ForwardOptions:
"""Captures CLI options to be forwarded from one subcommand to future
subcommands executed as part of a single CLI command invocation
(called "chained commands").
Attributes
----------
functionKwargs : `dict` [`str`, `str`]
The cached kwargs (argument name and argument value) from subcommands
that were called with values passed in as options on the command line.
"""

def __init__(self):
self.functionKwargs = {}

@staticmethod
def _getKwargsToSave(mwOptions, arguments, **kwargs):
"""Get kwargs that should be cached for use by subcommands invoked in
the future.
Parameters
----------
mwOptions : `list` or `tuple` [`MWOption`]
The options supported by the current command. For each option, its
kwarg will be cached if the option's `forward` parameter is `True`.
arguments : `list` [`str`]
The arguments that were passed in on the command line for the
current subcommand, split on whitespace into a list of arguments,
option flags, and option values.
Returns
-------
`dict` [`str`, `str`]
The kwargs that should be cached.
"""
saveableOptions = [opt for opt in mwOptions if opt.forward]
argumentSet = set(arguments)
passedOptions = [opt for opt in saveableOptions if set(opt.opts).intersection(argumentSet)]
return {opt.name: kwargs[opt.name] for opt in passedOptions}

def _getKwargsToUse(self, mwOptions, arguments, **kwargs):
"""Get kwargs that should be used by the current subcommand.
Parameters
----------
mwOptions : `list` or `tuple` [`MWOption`]
The options supported by the current subcommand.
arguments : `list` [`str`]
The arguments that were passed in on the command line for the
current subcommand, split on whitespace into a list of arguments,
option flags, and option values.
Returns
-------
`dict` [`str`, `str`]
kwargs that add the cached kwargs accepted by the current command
to the kwargs that were provided by CLI arguments. When a kwarg is
present in both places, the CLI argument kwarg value is used.
"""
argumentSet = set(arguments)
# get the cached value for options that were not passed in as CLI
# arguments:
cachedValues = {opt.name: self.functionKwargs[opt.name] for opt in mwOptions
if not set(opt.opts).intersection(argumentSet) and opt.name in self.functionKwargs}
updatedKwargs = copy.copy(kwargs)
updatedKwargs.update(cachedValues)
return updatedKwargs

def _save(self, mwOptions, arguments, **kwargs):
"""Save the current forwardable CLI options.
Parameters
----------
mwOptions : `list` or `tuple` [`MWOption`]
The options, which are accepted by the current command, that may be
saved.
arguments : `list` [`str`]
The arguments that were passed in on the command line for the
current subcommand, split on whitespace into a list of arguments,
option flags, and option values.
kwargs : `dict` [`str`, `str`]
Arguments that were passed into a command function. Indicated
option arguments will be extracted and cached.
"""
self.functionKwargs.update(self._getKwargsToSave(mwOptions, arguments, **kwargs))

def update(self, mwOptions, arguments, **kwargs):
"""Update what options are forwarded, drop non-forwarded options, and
update cached kwargs with new values in kwargs, and returns a new dict
that adds cached kwargs to the passed-in kwargs.
Parameters
----------
mwOptions : `list` or `tuple` [`MWOption`]
The options that will be forwarded.
arguments : `list` [`str`]
The arguments that were passed in on the command line for the
current subcommand, split on whitespace into a list of arguments,
option flags, and option values.
kwargs : `dict` [`str`, `str`]
Arguments that were passed into a command function. A new dict will
be created and returned that adds cached kwargs to the passed-in
non-default-value kwargs.
Returns
-------
kwargs : dict [`str`, `str`]
kwargs that add the cached kwargs accepted by the current command
to the kwargs that were provided by CLI arguments. When a kwarg is
present in both places, the CLI argument kwarg value is used.
"""
kwargsToUse = self._getKwargsToUse(mwOptions, arguments, **kwargs)
self._save(mwOptions, arguments, **kwargs)
return kwargsToUse
97 changes: 3 additions & 94 deletions tests/test_cliUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@

import click
import unittest
from unittest.mock import call, MagicMock
from unittest.mock import MagicMock


from lsst.daf.butler.cli import butler
from lsst.daf.butler.cli.utils import (clickResultMsg, ForwardOptions, LogCliRunner, Mocker, mockEnvVar,
MWArgumentDecorator, MWCommand, MWCtxObj, MWOption, MWOptionDecorator,
MWPath, option_section, unwrap)
from lsst.daf.butler.cli.utils import (clickResultMsg, LogCliRunner, Mocker, mockEnvVar, MWArgumentDecorator,
MWOption, MWOptionDecorator, MWPath, option_section, unwrap)
from lsst.daf.butler.cli.opt import directory_argument, repo_argument


Expand Down Expand Up @@ -355,95 +354,5 @@ def test_exist(self):
self.assertIn('"foo.txt" should not exist.', result.output)


class ForwardObjectsTest(unittest.TestCase):

mock = MagicMock()

test_option = MWOptionDecorator("-t", "--test", "--atest")

@click.group(chain=True)
def cli():
pass

@staticmethod
@cli.command(cls=MWCommand)
@click.pass_context
@test_option(forward=True)
def forwards(ctx, **kwargs):
"""A subcommand that forwards its test_option value to future
subcommands."""
def processor(objs):
newKwargs = objs.update(ctx.command.params, MWCtxObj.getFrom(ctx).args, **kwargs)
ForwardObjectsTest.mock("forwards", **newKwargs)
return objs
return processor

@staticmethod
@cli.command(cls=MWCommand)
@click.pass_context
@test_option() # default value of "foward" arg is False
def no_forward(ctx, **kwargs):
"""A subcommand that accepts test_option but does not forward the value
to future subcommands."""
def processor(objs):
newKwargs = objs.update(ctx.command.params, MWCtxObj.getFrom(ctx).args, **kwargs)
ForwardObjectsTest.mock("no_forward", **newKwargs)
return objs
return processor

@staticmethod
@cli.command(cls=MWCommand)
@click.pass_context
def no_test_option(ctx, **kwargs):
"""A subcommand that does not accept test_option."""
def processor(objs):
newKwargs = objs.update(ctx.command.params, MWCtxObj.getFrom(ctx).args, **kwargs)
ForwardObjectsTest.mock("no_test_option", **newKwargs)
return objs
return processor

@staticmethod
@cli.resultcallback()
def processCli(processors):
"""Executes the subcommand 'processor' functions for all the
subcommands in the chained command group."""
objs = ForwardOptions()
for processor in processors:
objs = processor(objs)

def setUp(self):
self.runner = click.testing.CliRunner()
self.mock.reset_mock()

def testForward(self):
"""Test that an option can be forward from one option to another."""
result = self.runner.invoke(self.cli, ["forwards", "-t", "foo", "forwards"])
print(result.output)
self.assertEqual(result.exit_code, 0, clickResultMsg(result))
self.mock.assert_has_calls((call("forwards", test="foo"),
call("forwards", test="foo")))

def testNoForward(self):
"""Test that when a subcommand that forwards an option value is called
before an option that does not use that option, that the stored option
does not get passed to the option that does not use it."""
result = self.runner.invoke(self.cli, ["forwards", "-t", "foo", "no-forward"])
self.assertEqual(result.exit_code, 0, clickResultMsg(result))
self.mock.assert_has_calls((call("forwards", test="foo"),
call("no_forward", test="foo")))

def testForwardThrough(self):
"""Test that forwarded option values persist when a subcommand that
does not use that value is called between subcommands that do use the
value."""
result = self.runner.invoke(self.cli, ["forwards", "-t", "foo",
"no-test-option",
"forwards"])
self.assertEqual(result.exit_code, 0, clickResultMsg(result))
self.mock.assert_has_calls((call("forwards", test="foo"),
call("no_test_option"),
call("forwards", test="foo")))


if __name__ == "__main__":
unittest.main()

0 comments on commit 3e84917

Please sign in to comment.