Coverage for tests / test_cliUtilSplitKv.py: 22%
139 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 08:41 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 08:41 +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/>.
28"""Unit tests for the daf_butler shared CLI options."""
30import unittest
31from functools import partial
32from unittest.mock import MagicMock
34import click
36from lsst.daf.butler.cli.utils import LogCliRunner, clickResultMsg, split_kv
39class SplitKvTestCase(unittest.TestCase):
40 """Tests that call split_kv directly."""
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"})
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"),))
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"})
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 )
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 )
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)
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")
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")
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"})
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 )
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 )
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 )
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 )
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 )
150class SplitKvCmdTestCase(unittest.TestCase):
151 """Tests using split_kv with a command."""
153 def setUp(self):
154 self.runner = LogCliRunner()
156 def test_cli(self):
157 mock = MagicMock()
159 @click.command()
160 @click.option("--value", callback=split_kv, multiple=True)
161 def cli(value):
162 mock(value)
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"})
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"})
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"})
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 )
187 def test_choice(self):
188 choices = ["FOO", "BAR", "BAZ"]
189 mock = MagicMock()
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)
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"})
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"})
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))
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"})
225 def test_separatorDash(self):
226 def split_kv_dash(context, param, values):
227 return split_kv(context, param, values, separator="-")
229 mock = MagicMock()
231 @click.command()
232 @click.option("--value", callback=split_kv_dash, multiple=True)
233 def cli(value):
234 mock(value)
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"})
240 def test_separatorFunctoolsDash(self):
241 mock = MagicMock()
243 @click.command()
244 @click.option("--value", callback=partial(split_kv, separator="-"), multiple=True)
245 def cli(value):
246 mock(value)
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"})
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
258 result = self.runner.invoke(cli, ["--value", "first 1"])
259 self.assertEqual(str(result.exception), "' ' is not a supported separator for key-value pairs.")
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
267 result = self.runner.invoke(cli, ["--value", "first,1"])
268 self.assertEqual(str(result.exception), "',' is not a supported separator for key-value pairs.")
270 def test_normalizeWithoutChoice(self):
271 """Test that normalize=True without Choice fails gracefully.
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()
280 @click.command()
281 @click.option("--value", callback=partial(split_kv, normalize=True))
282 def cli(value):
283 mock(value)
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"))
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()
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)
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"})
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()
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)
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"})
328if __name__ == "__main__":
329 unittest.main()