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 enum 

24import io 

25import os 

26import traceback 

27from unittest.mock import MagicMock 

28 

29from ..core.utils import iterable 

30 

31 

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

38 

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" 

46 

47 

48def textTypeStr(multiple): 

49 """Get the text type string for CLI help documentation. 

50 

51 Parameters 

52 ---------- 

53 multiple : `bool` 

54 True if multiple text values are allowed, False if only one value is 

55 allowed. 

56 

57 Returns 

58 ------- 

59 textTypeStr : `str` 

60 The type string to use. 

61 """ 

62 return typeStrAcceptsMultiple if multiple else typeStrAcceptsSingle 

63 

64 

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 

70 

71 

72class Mocker: 

73 

74 mock = MagicMock() 

75 

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. 

79 

80 For convenience, constructor arguments are forwarded to the call 

81 function. 

82 """ 

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

84 

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) 

90 

91 

92def clickResultMsg(result): 

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

94 

95 Parameters 

96 ---------- 

97 result : click.Result 

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

99 

100 Returns 

101 ------- 

102 msg : `str` 

103 The message string. 

104 """ 

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

106 

107 

108def addArgumentHelp(doc, helpText): 

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

110 

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. 

116 

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. 

124 

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 

138 

139 

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. 

143 

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

146 

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. 

158 

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 

169 

170 

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. 

175 

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

178 

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

194 

195 Returns 

196 ------- 

197 `dict` : [`str`, `str`] 

198 The passed-in values in dict form. 

199 

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 

219 

220 

221def to_upper(context, param, value): 

222 """Convert a value to upper case. 

223 

224 Parameters 

225 ---------- 

226 context : click.Context 

227 

228 values : string 

229 The value to be converted. 

230 

231 Returns 

232 ------- 

233 string 

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

235 """ 

236 return value.upper() 

237 

238 

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. 

242 

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. 

246 

247 Parameters 

248 ---------- 

249 func : function 

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

251 to the function. 

252 

253 Returns 

254 ------- 

255 The result of calling func. 

256 

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