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

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"}
39class Mocker:
41 mock = MagicMock()
43 def __init__(self, *args, **kwargs):
44 """Mocker is a helper class for unit tests. It can be imported and
45 called and later imported again and call can be verified.
47 For convenience, constructor arguments are forwarded to the call
48 function.
49 """
50 self.__call__(*args, **kwargs)
52 def __call__(self, *args, **kwargs):
53 """Creates a MagicMock and stores it in a static variable that can
54 later be verified.
55 """
56 Mocker.mock(*args, **kwargs)
59def split_commas(context, param, values):
60 """Process a tuple of values, where each value may contain comma-separated
61 values, and return a single list of all the passed-in values.
63 This function can be passed to the 'callback' argument of a click.option to
64 allow it to process comma-separated values (e.g. "--my-opt a,b,c").
66 Parameters
67 ----------
68 context : `click.Context` or `None`
69 The current execution context. Unused, but Click always passes it to
70 callbacks.
71 param : `click.core.Option` or `None`
72 The parameter being handled. Unused, but Click always passes it to
73 callbacks.
74 values : [`str`]
75 All the values passed for this option. Strings may contain commas,
76 which will be treated as delimiters for separate values.
78 Returns
79 -------
80 list of string
81 The passed in values separated by commas and combined into a single
82 list.
83 """
84 valueList = []
85 for value in iterable(values):
86 valueList.extend(value.split(","))
87 return valueList
90def split_kv(context, param, values, separator="="):
91 """Process a tuple of values that are key-value pairs separated by a given
92 separator. Multiple pairs may be comma separated. Return a dictionary of
93 all the passed-in values.
95 This function can be passed to the 'callback' argument of a click.option to
96 allow it to process comma-separated values (e.g. "--my-opt a=1,b=2").
98 Parameters
99 ----------
100 context : `click.Context` or `None`
101 The current execution context. Unused, but Click always passes it to
102 callbacks.
103 param : `click.core.Option` or `None`
104 The parameter being handled. Unused, but Click always passes it to
105 callbacks.
106 values : [`str`]
107 All the values passed for this option. Strings may contain commas,
108 which will be treated as delimiters for separate values.
109 separator : str, optional
110 The character that separates key-value pairs. May not be a comma or an
111 empty space (for space separators use Click's default implementation
112 for tuples; `type=(str, str)`). By default "=".
114 Returns
115 -------
116 `dict` : [`str`, `str`]
117 The passed-in values in dict form.
119 Raises
120 ------
121 `click.ClickException`
122 Raised if the separator is not found in an entry, or if duplicate keys
123 are encountered.
124 """
125 if separator in (",", " "):
126 raise RuntimeError(f"'{separator}' is not a supported separator for key-value pairs.")
127 vals = split_commas(context, param, values)
128 ret = {}
129 for val in vals:
130 try:
131 k, v = val.split(separator)
132 except ValueError:
133 raise click.ClickException(f"Missing or invalid key-value separator in value '{val}'")
134 if k in ret:
135 raise click.ClickException(f"Duplicate entries for '{k}' in '{values}'")
136 ret[k] = v
137 return ret
140def to_upper(context, param, value):
141 """Convert a value to upper case.
143 Parameters
144 ----------
145 context : click.Context
147 values : string
148 The value to be converted.
150 Returns
151 -------
152 string
153 A copy of the passed-in value, converted to upper case.
154 """
155 return value.upper()
158def cli_handle_exception(func, *args, **kwargs):
159 """Wrap a function call in an exception handler that raises a
160 ClickException if there is an Exception.
162 Also provides support for unit testing by testing for an environment
163 variable, and if it is present prints the function name, args, and kwargs
164 to stdout so they can be read and verified by the unit test code.
166 Parameters
167 ----------
168 func : function
169 A function to be called and exceptions handled. Will pass args & kwargs
170 to the function.
172 Returns
173 -------
174 The result of calling func.
176 Raises
177 ------
178 click.ClickException
179 An exception to be handled by the Click CLI tool.
180 """
181 if mockEnvVarKey in os.environ:
182 Mocker(*args, **kwargs)
183 return
184 try:
185 return func(*args, **kwargs)
186 except Exception:
187 msg = io.StringIO()
188 msg.write("An error occurred during command execution:\n")
189 traceback.print_exc(file=msg)
190 msg.seek(0)
191 raise click.ClickException(msg.read())