Coverage for python/lsst/daf/butler/cli/utils.py : 44%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22import click
23import enum
24import io
25import os
26import traceback
27from unittest.mock import MagicMock
29from ..core.utils import iterable
32# CLI_BUTLER_MOCK_ENV is set by some tests as an environment variable, it
33# indicates to the cli_handle_exception function that instead of executing the
34# command implementation function it should use the Mocker class for unit test
35# verification.
36mockEnvVarKey = "CLI_BUTLER_MOCK_ENV"
37mockEnvVar = {mockEnvVarKey: "1"}
39# This is used as the metavar argument to Options that accept multiple string
40# inputs, which may be comma-separarated. For example:
41# --my-opt foo,bar --my-opt baz.
42# Other arguments to the Option should include multiple=true and
43# callback=split_kv.
44typeStrAcceptsMultiple = "TEXT ..."
45typeStrAcceptsSingle = "TEXT"
48def textTypeStr(multiple):
49 """Get the text type string for CLI help documentation.
51 Parameters
52 ----------
53 multiple : `bool`
54 True if multiple text values are allowed, False if only one value is
55 allowed.
57 Returns
58 -------
59 textTypeStr : `str`
60 The type string to use.
61 """
62 return typeStrAcceptsMultiple if multiple else typeStrAcceptsSingle
65# The ParameterType enum is used to indicate a click Argument or Option (both
66# of which are subclasses of click.Parameter).
67class ParameterType(enum.Enum):
68 ARGUMENT = 0
69 OPTION = 1
72class Mocker:
74 mock = MagicMock()
76 def __init__(self, *args, **kwargs):
77 """Mocker is a helper class for unit tests. It can be imported and
78 called and later imported again and call can be verified.
80 For convenience, constructor arguments are forwarded to the call
81 function.
82 """
83 self.__call__(*args, **kwargs)
85 def __call__(self, *args, **kwargs):
86 """Creates a MagicMock and stores it in a static variable that can
87 later be verified.
88 """
89 Mocker.mock(*args, **kwargs)
92def clickResultMsg(result):
93 """Get a standard assert message from a click result
95 Parameters
96 ----------
97 result : click.Result
98 The result object returned from click.testing.CliRunner.invoke
100 Returns
101 -------
102 msg : `str`
103 The message string.
104 """
105 return f"output: {result.output} exception: {result.exception}"
108def addArgumentHelp(doc, helpText):
109 """Add a Click argument's help message to a function's documentation.
111 This is needed because click presents arguments in the order the argument
112 decorators are applied to a function, top down. But, the evaluation of the
113 decorators happens bottom up, so if arguments just append their help to the
114 function's docstring, the argument descriptions appear in reverse order
115 from the order they are applied in.
117 Parameters
118 ----------
119 doc : `str`
120 The function's docstring.
121 helpText : `str`
122 The argument's help string to be inserted into the function's
123 docstring.
125 Returns
126 -------
127 doc : `str`
128 Updated function documentation.
129 """
130 if doc is None:
131 doc = helpText
132 else:
133 doclines = doc.splitlines()
134 doclines.insert(1, helpText)
135 doclines.insert(1, "\n")
136 doc = "\n".join(doclines)
137 return doc
140def split_commas(context, param, values):
141 """Process a tuple of values, where each value may contain comma-separated
142 values, and return a single list of all the passed-in values.
144 This function can be passed to the 'callback' argument of a click.option to
145 allow it to process comma-separated values (e.g. "--my-opt a,b,c").
147 Parameters
148 ----------
149 context : `click.Context` or `None`
150 The current execution context. Unused, but Click always passes it to
151 callbacks.
152 param : `click.core.Option` or `None`
153 The parameter being handled. Unused, but Click always passes it to
154 callbacks.
155 values : [`str`]
156 All the values passed for this option. Strings may contain commas,
157 which will be treated as delimiters for separate values.
159 Returns
160 -------
161 list of string
162 The passed in values separated by commas and combined into a single
163 list.
164 """
165 valueList = []
166 for value in iterable(values):
167 valueList.extend(value.split(","))
168 return valueList
171def split_kv(context, param, values, separator="="):
172 """Process a tuple of values that are key-value pairs separated by a given
173 separator. Multiple pairs may be comma separated. Return a dictionary of
174 all the passed-in values.
176 This function can be passed to the 'callback' argument of a click.option to
177 allow it to process comma-separated values (e.g. "--my-opt a=1,b=2").
179 Parameters
180 ----------
181 context : `click.Context` or `None`
182 The current execution context. Unused, but Click always passes it to
183 callbacks.
184 param : `click.core.Option` or `None`
185 The parameter being handled. Unused, but Click always passes it to
186 callbacks.
187 values : [`str`]
188 All the values passed for this option. Strings may contain commas,
189 which will be treated as delimiters for separate values.
190 separator : str, optional
191 The character that separates key-value pairs. May not be a comma or an
192 empty space (for space separators use Click's default implementation
193 for tuples; `type=(str, str)`). By default "=".
195 Returns
196 -------
197 `dict` : [`str`, `str`]
198 The passed-in values in dict form.
200 Raises
201 ------
202 `click.ClickException`
203 Raised if the separator is not found in an entry, or if duplicate keys
204 are encountered.
205 """
206 if separator in (",", " "):
207 raise RuntimeError(f"'{separator}' is not a supported separator for key-value pairs.")
208 vals = split_commas(context, param, values)
209 ret = {}
210 for val in vals:
211 try:
212 k, v = val.split(separator)
213 except ValueError:
214 raise click.ClickException(f"Missing or invalid key-value separator in value '{val}'")
215 if k in ret:
216 raise click.ClickException(f"Duplicate entries for '{k}' in '{values}'")
217 ret[k] = v
218 return ret
221def to_upper(context, param, value):
222 """Convert a value to upper case.
224 Parameters
225 ----------
226 context : click.Context
228 values : string
229 The value to be converted.
231 Returns
232 -------
233 string
234 A copy of the passed-in value, converted to upper case.
235 """
236 return value.upper()
239def cli_handle_exception(func, *args, **kwargs):
240 """Wrap a function call in an exception handler that raises a
241 ClickException if there is an Exception.
243 Also provides support for unit testing by testing for an environment
244 variable, and if it is present prints the function name, args, and kwargs
245 to stdout so they can be read and verified by the unit test code.
247 Parameters
248 ----------
249 func : function
250 A function to be called and exceptions handled. Will pass args & kwargs
251 to the function.
253 Returns
254 -------
255 The result of calling func.
257 Raises
258 ------
259 click.ClickException
260 An exception to be handled by the Click CLI tool.
261 """
262 if mockEnvVarKey in os.environ:
263 Mocker(*args, **kwargs)
264 return
265 try:
266 return func(*args, **kwargs)
267 except Exception:
268 msg = io.StringIO()
269 msg.write("An error occurred during command execution:\n")
270 traceback.print_exc(file=msg)
271 msg.seek(0)
272 raise click.ClickException(msg.read())