Hide keyboard shortcuts

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/>. 

21 

22import click 

23import io 

24import os 

25import traceback 

26from unittest.mock import MagicMock 

27 

28from ..core.utils import iterable 

29 

30 

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"} 

37 

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 ..." 

44 

45 

46class Mocker: 

47 

48 mock = MagicMock() 

49 

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. 

53 

54 For convenience, constructor arguments are forwarded to the call 

55 function. 

56 """ 

57 self.__call__(*args, **kwargs) 

58 

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) 

64 

65 

66def clickResultMsg(result): 

67 """Get a standard assert message from a click result 

68 

69 Parameters 

70 ---------- 

71 result : click.Result 

72 The result object returned from click.testing.CliRunner.invoke 

73 

74 Returns 

75 ------- 

76 msg : `str` 

77 The message string. 

78 """ 

79 return f"output: {result.output} exception: {result.exception}" 

80 

81 

82def addArgumentHelp(doc, helpText): 

83 """Add a Click argument's help message to a function's documentation. 

84 

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. 

90 

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. 

98 

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 

112 

113 

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. 

117 

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"). 

120 

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. 

132 

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 

143 

144 

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. 

149 

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"). 

152 

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 "=". 

168 

169 Returns 

170 ------- 

171 `dict` : [`str`, `str`] 

172 The passed-in values in dict form. 

173 

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 

193 

194 

195def to_upper(context, param, value): 

196 """Convert a value to upper case. 

197 

198 Parameters 

199 ---------- 

200 context : click.Context 

201 

202 values : string 

203 The value to be converted. 

204 

205 Returns 

206 ------- 

207 string 

208 A copy of the passed-in value, converted to upper case. 

209 """ 

210 return value.upper() 

211 

212 

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. 

216 

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. 

220 

221 Parameters 

222 ---------- 

223 func : function 

224 A function to be called and exceptions handled. Will pass args & kwargs 

225 to the function. 

226 

227 Returns 

228 ------- 

229 The result of calling func. 

230 

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())