Coverage for tests/test_cliUtilSplitKv.py: 16%

140 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-04 02:55 -0700

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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28"""Unit tests for the daf_butler shared CLI options. 

29""" 

30 

31import unittest 

32from functools import partial 

33from unittest.mock import MagicMock 

34 

35import click 

36from lsst.daf.butler.cli.utils import LogCliRunner, clickResultMsg, split_kv 

37 

38 

39class SplitKvTestCase(unittest.TestCase): 

40 """Tests that call split_kv directly.""" 

41 

42 def test_single_dict(self): 

43 """Test that a single kv pair converts to a dict.""" 

44 self.assertEqual(split_kv("context", "param", "first=1"), {"first": "1"}) 

45 

46 def test_single_tuple(self): 

47 """Test that a single kv pair converts to a tuple when 

48 return_type=tuple. 

49 """ 

50 self.assertEqual(split_kv("context", "param", "first=1", return_type=tuple), (("first", "1"),)) 

51 

52 def test_multiple_dict(self): 

53 """Test that multiple comma separated kv pairs convert to a dict.""" 

54 self.assertEqual(split_kv("context", "param", "first=1,second=2"), {"first": "1", "second": "2"}) 

55 

56 def test_multiple_tuple(self): 

57 """Test that multiple comma separated kv pairs convert to a tuple when 

58 return_type=tuple. 

59 """ 

60 self.assertEqual( 

61 split_kv("context", "param", "first=1,second=2", return_type=tuple), 

62 (("first", "1"), ("second", "2")), 

63 ) 

64 

65 def test_unseparated(self): 

66 """Test that a value without a key converts to a kv pair with an empty 

67 string key. 

68 """ 

69 self.assertEqual( 

70 split_kv("context", "param", "first,second=2", unseparated_okay=True), 

71 {"": "first", "second": "2"}, 

72 ) 

73 

74 def test_notMultiple(self): 

75 """Test that multiple values are rejected if multiple=False.""" 

76 with self.assertRaisesRegex( 

77 click.ClickException, 

78 "Could not parse key-value pair " 

79 "'first=1,second=2' using separator '=', with multiple values not " 

80 "allowed.", 

81 ): 

82 split_kv("context", "param", "first=1,second=2", multiple=False) 

83 

84 def test_wrongSeparator(self): 

85 """Test that an input with the wrong separator raises.""" 

86 with self.assertRaises(click.ClickException): 

87 split_kv("context", "param", "first-1") 

88 

89 def test_missingSeparator(self): 

90 """Test that an input with no separator raises when 

91 unseparated_okay=False (this is the default value). 

92 """ 

93 with self.assertRaises(click.ClickException): 

94 split_kv("context", "param", "first 1") 

95 

96 def test_unseparatedOkay(self): 

97 """Test that that the default key is used for values without a 

98 separator when unseparated_okay=True. 

99 """ 

100 self.assertEqual(split_kv("context", "param", "foo", unseparated_okay=True), {"": "foo"}) 

101 

102 def test_unseparatedOkay_list(self): 

103 """Test that that the default key is used for values without a 

104 separator when unseparated_okay=True and the return_type is tuple. 

105 """ 

106 self.assertEqual( 

107 split_kv("context", "param", "foo,bar", unseparated_okay=True, return_type=tuple), 

108 (("", "foo"), ("", "bar")), 

109 ) 

110 

111 def test_unseparatedOkay_defaultKey(self): 

112 """Test that that the default key can be set and is used for values 

113 without a separator when unseparated_okay=True. 

114 """ 

115 self.assertEqual( 

116 split_kv("context", "param", "foo", unseparated_okay=True, default_key=...), {...: "foo"} 

117 ) 

118 

119 def test_dashSeparator(self): 

120 """Test that specifying a separator is accepted and converts arguments 

121 to a dict. 

122 """ 

123 self.assertEqual( 

124 split_kv("context", "param", "first-1,second-2", separator="-"), {"first": "1", "second": "2"} 

125 ) 

126 

127 def test_reverseKv(self): 

128 self.assertEqual( 

129 split_kv( 

130 "context", 

131 "param", 

132 "first=1,second", 

133 unseparated_okay=True, 

134 default_key="key", 

135 reverse_kv=True, 

136 ), 

137 {"1": "first", "second": "key"}, 

138 ) 

139 

140 def test_invalidResultType(self): 

141 with self.assertRaises(click.ClickException): 

142 split_kv( 

143 "context", 

144 "param", 

145 "first=1,second=2", 

146 return_type=set, 

147 ) 

148 

149 

150class SplitKvCmdTestCase(unittest.TestCase): 

151 """Tests using split_kv with a command.""" 

152 

153 def setUp(self): 

154 self.runner = LogCliRunner() 

155 

156 def test_cli(self): 

157 mock = MagicMock() 

158 

159 @click.command() 

160 @click.option("--value", callback=split_kv, multiple=True) 

161 def cli(value): 

162 mock(value) 

163 

164 result = self.runner.invoke(cli, ["--value", "first=1"]) 

165 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

166 mock.assert_called_with({"first": "1"}) 

167 

168 result = self.runner.invoke(cli, ["--value", "first=1,second=2"]) 

169 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

170 mock.assert_called_with({"first": "1", "second": "2"}) 

171 

172 result = self.runner.invoke(cli, ["--value", "first=1", "--value", "second=2"]) 

173 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

174 mock.assert_called_with({"first": "1", "second": "2"}) 

175 

176 # double separator "==" should fail: 

177 result = self.runner.invoke(cli, ["--value", "first==1"]) 

178 self.assertEqual(result.exit_code, 1) 

179 self.assertEqual( 

180 result.output, 

181 "Error: Could not parse key-value pair 'first==1' using separator '=', with " 

182 "multiple values allowed: too many values to unpack (expected 2)\n", 

183 ) 

184 

185 def test_choice(self): 

186 choices = ["FOO", "BAR", "BAZ"] 

187 mock = MagicMock() 

188 

189 @click.command() 

190 @click.option( 

191 "--metasyntactic-var", 

192 callback=partial( 

193 split_kv, 

194 unseparated_okay=True, 

195 choice=click.Choice(choices=choices, case_sensitive=False), 

196 normalize=True, 

197 ), 

198 ) 

199 def cli(metasyntactic_var): 

200 mock(metasyntactic_var) 

201 

202 # check a valid choice without a kv separator 

203 result = self.runner.invoke(cli, ["--metasyntactic-var", "FOO"]) 

204 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

205 mock.assert_called_with({"": "FOO"}) 

206 

207 # check a valid choice with a kv separator 

208 result = self.runner.invoke(cli, ["--metasyntactic-var", "lsst.daf.butler=BAR"]) 

209 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

210 mock.assert_called_with({"lsst.daf.butler": "BAR"}) 

211 

212 # check that invalid choices with and without kv separators fail & 

213 # return a non-zero exit code. 

214 for val in ("BOZ", "lsst.daf.butler=BOZ"): 

215 result = self.runner.invoke(cli, ["--metasyntactic-var", val]) 

216 self.assertNotEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

217 

218 # check value normalization (lower case "foo" should become "FOO") 

219 result = self.runner.invoke(cli, ["--metasyntactic-var", "lsst.daf.butler=foo"]) 

220 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

221 mock.assert_called_with({"lsst.daf.butler": "FOO"}) 

222 

223 def test_separatorDash(self): 

224 def split_kv_dash(context, param, values): 

225 return split_kv(context, param, values, separator="-") 

226 

227 mock = MagicMock() 

228 

229 @click.command() 

230 @click.option("--value", callback=split_kv_dash, multiple=True) 

231 def cli(value): 

232 mock(value) 

233 

234 result = self.runner.invoke(cli, ["--value", "first-1"]) 

235 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

236 mock.assert_called_with({"first": "1"}) 

237 

238 def test_separatorFunctoolsDash(self): 

239 mock = MagicMock() 

240 

241 @click.command() 

242 @click.option("--value", callback=partial(split_kv, separator="-"), multiple=True) 

243 def cli(value): 

244 mock(value) 

245 

246 result = self.runner.invoke(cli, ["--value", "first-1", "--value", "second-2"]) 

247 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

248 mock.assert_called_with({"first": "1", "second": "2"}) 

249 

250 def test_separatorSpace(self): 

251 @click.command() 

252 @click.option("--value", callback=partial(split_kv, separator=" "), multiple=True) 

253 def cli(value): 

254 pass 

255 

256 result = self.runner.invoke(cli, ["--value", "first 1"]) 

257 self.assertEqual(str(result.exception), "' ' is not a supported separator for key-value pairs.") 

258 

259 def test_separatorComma(self): 

260 @click.command() 

261 @click.option("--value", callback=partial(split_kv, separator=","), multiple=True) 

262 def cli(value): 

263 pass 

264 

265 result = self.runner.invoke(cli, ["--value", "first,1"]) 

266 self.assertEqual(str(result.exception), "',' is not a supported separator for key-value pairs.") 

267 

268 def test_normalizeWithoutChoice(self): 

269 """Test that normalize=True without Choice fails gracefully. 

270 

271 Normalize uses values in the provided Choice to create the normalized 

272 value. Without a provided Choice, it can't normalize. Verify that this 

273 does not cause a crash or other bad behavior, it just doesn't normalize 

274 anything. 

275 """ 

276 mock = MagicMock() 

277 

278 @click.command() 

279 @click.option("--value", callback=partial(split_kv, normalize=True)) 

280 def cli(value): 

281 mock(value) 

282 

283 result = self.runner.invoke(cli, ["--value", "foo=bar"]) 

284 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

285 mock.assert_called_with(dict(foo="bar")) 

286 

287 def test_addToDefaultValue(self): 

288 """Verify that if add_to_default is True that passed-in values are 

289 added to the default value set in the option. 

290 """ 

291 mock = MagicMock() 

292 

293 @click.command() 

294 @click.option( 

295 "--value", 

296 callback=partial(split_kv, add_to_default=True, unseparated_okay=True), 

297 default=["INFO"], 

298 multiple=True, 

299 ) 

300 def cli(value): 

301 mock(value) 

302 

303 result = self.runner.invoke(cli, ["--value", "lsst.daf.butler=DEBUG"]) 

304 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

305 mock.assert_called_with({"": "INFO", "lsst.daf.butler": "DEBUG"}) 

306 

307 def test_replaceDefaultValue(self): 

308 """Verify that if add_to_default is False (this is the default value), 

309 that passed-in values replace any default value, even if keys are 

310 different. 

311 """ 

312 mock = MagicMock() 

313 

314 @click.command() 

315 @click.option( 

316 "--value", callback=partial(split_kv, unseparated_okay=True), default=["INFO"], multiple=True 

317 ) 

318 def cli(value): 

319 mock(value) 

320 

321 result = self.runner.invoke(cli, ["--value", "lsst.daf.butler=DEBUG"]) 

322 self.assertEqual(result.exit_code, 0, msg=clickResultMsg(result)) 

323 mock.assert_called_with({"lsst.daf.butler": "DEBUG"}) 

324 

325 

326if __name__ == "__main__": 

327 unittest.main()