Coverage for tests / test_cliUtilSplitKv.py: 22%

139 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-06 08:30 +0000

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 

30import unittest 

31from functools import partial 

32from unittest.mock import MagicMock 

33 

34import click 

35 

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 # Check first 137 characters because python 3.14 adds more information 

180 # to the error message. 

181 self.assertEqual( 

182 result.output[:137], 

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

184 "multiple values allowed: too many values to unpack (expected 2", 

185 ) 

186 

187 def test_choice(self): 

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

189 mock = MagicMock() 

190 

191 @click.command() 

192 @click.option( 

193 "--metasyntactic-var", 

194 callback=partial( 

195 split_kv, 

196 unseparated_okay=True, 

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

198 normalize=True, 

199 ), 

200 ) 

201 def cli(metasyntactic_var): 

202 mock(metasyntactic_var) 

203 

204 # check a valid choice without a kv separator 

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

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

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

208 

209 # check a valid choice with a kv separator 

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

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

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

213 

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

215 # return a non-zero exit code. 

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

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

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

219 

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

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

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

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

224 

225 def test_separatorDash(self): 

226 def split_kv_dash(context, param, values): 

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

228 

229 mock = MagicMock() 

230 

231 @click.command() 

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

233 def cli(value): 

234 mock(value) 

235 

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

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

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

239 

240 def test_separatorFunctoolsDash(self): 

241 mock = MagicMock() 

242 

243 @click.command() 

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

245 def cli(value): 

246 mock(value) 

247 

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

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

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

251 

252 def test_separatorSpace(self): 

253 @click.command() 

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

255 def cli(value): 

256 pass 

257 

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

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

260 

261 def test_separatorComma(self): 

262 @click.command() 

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

264 def cli(value): 

265 pass 

266 

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

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

269 

270 def test_normalizeWithoutChoice(self): 

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

272 

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

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

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

276 anything. 

277 """ 

278 mock = MagicMock() 

279 

280 @click.command() 

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

282 def cli(value): 

283 mock(value) 

284 

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

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

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

288 

289 def test_addToDefaultValue(self): 

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

291 added to the default value set in the option. 

292 """ 

293 mock = MagicMock() 

294 

295 @click.command() 

296 @click.option( 

297 "--value", 

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

299 default=["INFO"], 

300 multiple=True, 

301 ) 

302 def cli(value): 

303 mock(value) 

304 

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

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

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

308 

309 def test_replaceDefaultValue(self): 

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

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

312 different. 

313 """ 

314 mock = MagicMock() 

315 

316 @click.command() 

317 @click.option( 

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

319 ) 

320 def cli(value): 

321 mock(value) 

322 

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

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

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

326 

327 

328if __name__ == "__main__": 

329 unittest.main()