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

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 io
24import os
25import traceback
26from unittest.mock import MagicMock
28from ..core.utils import iterable
31# CLI_BUTLER_MOCK_ENV is set by some tests as an environment variable, it
32# indicates to the cli_handle_exception function that instead of executing the
33# command implementation function it should use the Mocker class for unit test
34# verification.
35mockEnvVarKey = "CLI_BUTLER_MOCK_ENV"
36mockEnvVar = {mockEnvVarKey: "1"}
38# This is used as the metavar argument to Options that accept multiple string
39# inputs, which may be comma-separarated. For example:
40# --my-opt foo,bar --my-opt baz.
41# Other arguments to the Option should include multiple=true and
42# callback=split_kv.
43typeStrAcceptsMultiple = "TEXT ..."
46class Mocker:
48 mock = MagicMock()
50 def __init__(self, *args, **kwargs):
51 """Mocker is a helper class for unit tests. It can be imported and
52 called and later imported again and call can be verified.
54 For convenience, constructor arguments are forwarded to the call
55 function.
56 """
57 self.__call__(*args, **kwargs)
59 def __call__(self, *args, **kwargs):
60 """Creates a MagicMock and stores it in a static variable that can
61 later be verified.
62 """
63 Mocker.mock(*args, **kwargs)
66def clickResultMsg(result):
67 """Get a standard assert message from a click result
69 Parameters
70 ----------
71 result : click.Result
72 The result object returned from click.testing.CliRunner.invoke
74 Returns
75 -------
76 msg : `str`
77 The message string.
78 """
79 return f"output: {result.output} exception: {result.exception}"
82def addArgumentHelp(doc, helpText):
83 """Add a Click argument's help message to a function's documentation.
85 This is needed because click presents arguments in the order the argument
86 decorators are applied to a function, top down. But, the evaluation of the
87 decorators happens bottom up, so if arguments just append their help to the
88 function's docstring, the argument descriptions appear in reverse order
89 from the order they are applied in.
91 Parameters
92 ----------
93 doc : `str`
94 The function's docstring.
95 helpText : `str`
96 The argument's help string to be inserted into the function's
97 docstring.
99 Returns
100 -------
101 doc : `str`
102 Updated function documentation.
103 """
104 if doc is None:
105 doc = helpText
106 else:
107 doclines = doc.splitlines()
108 doclines.insert(1, helpText)
109 doclines.insert(1, "\n")
110 doc = "\n".join(doclines)
111 return doc
114def split_commas(context, param, values):
115 """Process a tuple of values, where each value may contain comma-separated
116 values, and return a single list of all the passed-in values.
118 This function can be passed to the 'callback' argument of a click.option to
119 allow it to process comma-separated values (e.g. "--my-opt a,b,c").
121 Parameters
122 ----------
123 context : `click.Context` or `None`
124 The current execution context. Unused, but Click always passes it to
125 callbacks.
126 param : `click.core.Option` or `None`
127 The parameter being handled. Unused, but Click always passes it to
128 callbacks.
129 values : [`str`]
130 All the values passed for this option. Strings may contain commas,
131 which will be treated as delimiters for separate values.
133 Returns
134 -------
135 list of string
136 The passed in values separated by commas and combined into a single
137 list.
138 """
139 valueList = []
140 for value in iterable(values):
141 valueList.extend(value.split(","))
142 return valueList
145def split_kv(context, param, values, separator="="):
146 """Process a tuple of values that are key-value pairs separated by a given
147 separator. Multiple pairs may be comma separated. Return a dictionary of
148 all the passed-in values.
150 This function can be passed to the 'callback' argument of a click.option to
151 allow it to process comma-separated values (e.g. "--my-opt a=1,b=2").
153 Parameters
154 ----------
155 context : `click.Context` or `None`
156 The current execution context. Unused, but Click always passes it to
157 callbacks.
158 param : `click.core.Option` or `None`
159 The parameter being handled. Unused, but Click always passes it to
160 callbacks.
161 values : [`str`]
162 All the values passed for this option. Strings may contain commas,
163 which will be treated as delimiters for separate values.
164 separator : str, optional
165 The character that separates key-value pairs. May not be a comma or an
166 empty space (for space separators use Click's default implementation
167 for tuples; `type=(str, str)`). By default "=".
169 Returns
170 -------
171 `dict` : [`str`, `str`]
172 The passed-in values in dict form.
174 Raises
175 ------
176 `click.ClickException`
177 Raised if the separator is not found in an entry, or if duplicate keys
178 are encountered.
179 """
180 if separator in (",", " "):
181 raise RuntimeError(f"'{separator}' is not a supported separator for key-value pairs.")
182 vals = split_commas(context, param, values)
183 ret = {}
184 for val in vals:
185 try:
186 k, v = val.split(separator)
187 except ValueError:
188 raise click.ClickException(f"Missing or invalid key-value separator in value '{val}'")
189 if k in ret:
190 raise click.ClickException(f"Duplicate entries for '{k}' in '{values}'")
191 ret[k] = v
192 return ret
195def to_upper(context, param, value):
196 """Convert a value to upper case.
198 Parameters
199 ----------
200 context : click.Context
202 values : string
203 The value to be converted.
205 Returns
206 -------
207 string
208 A copy of the passed-in value, converted to upper case.
209 """
210 return value.upper()
213def cli_handle_exception(func, *args, **kwargs):
214 """Wrap a function call in an exception handler that raises a
215 ClickException if there is an Exception.
217 Also provides support for unit testing by testing for an environment
218 variable, and if it is present prints the function name, args, and kwargs
219 to stdout so they can be read and verified by the unit test code.
221 Parameters
222 ----------
223 func : function
224 A function to be called and exceptions handled. Will pass args & kwargs
225 to the function.
227 Returns
228 -------
229 The result of calling func.
231 Raises
232 ------
233 click.ClickException
234 An exception to be handled by the Click CLI tool.
235 """
236 if mockEnvVarKey in os.environ:
237 Mocker(*args, **kwargs)
238 return
239 try:
240 return func(*args, **kwargs)
241 except Exception:
242 msg = io.StringIO()
243 msg.write("An error occurred during command execution:\n")
244 traceback.print_exc(file=msg)
245 msg.seek(0)
246 raise click.ClickException(msg.read())