Coverage for tests / unit / cli / test_arg_bool.py: 100%
239 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-18 02:51 -0700
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-18 02:51 -0700
1"""Tests for muutils.cli.arg_bool module."""
3from __future__ import annotations
5import argparse
6import pytest
7from pytest import mark, param
9from muutils.cli.arg_bool import (
10 parse_bool_token,
11 BoolFlagOrValue,
12 add_bool_flag,
13 TRUE_SET_DEFAULT,
14 FALSE_SET_DEFAULT,
15)
18# ============================================================================
19# Tests for parse_bool_token
20# ============================================================================
23def test_parse_bool_token_valid():
24 """Test parse_bool_token with valid true/false tokens."""
25 # True tokens from default set
26 assert parse_bool_token("true") is True
27 assert parse_bool_token("1") is True
28 assert parse_bool_token("t") is True
29 assert parse_bool_token("yes") is True
30 assert parse_bool_token("y") is True
31 assert parse_bool_token("on") is True
33 # False tokens from default set
34 assert parse_bool_token("false") is False
35 assert parse_bool_token("0") is False
36 assert parse_bool_token("f") is False
37 assert parse_bool_token("no") is False
38 assert parse_bool_token("n") is False
39 assert parse_bool_token("off") is False
42def test_parse_bool_token_case_insensitive():
43 """Test parse_bool_token is case-insensitive."""
44 assert parse_bool_token("TRUE") is True
45 assert parse_bool_token("True") is True
46 assert parse_bool_token("TrUe") is True
47 assert parse_bool_token("FALSE") is False
48 assert parse_bool_token("False") is False
49 assert parse_bool_token("FaLsE") is False
50 assert parse_bool_token("YES") is True
51 assert parse_bool_token("NO") is False
52 assert parse_bool_token("ON") is True
53 assert parse_bool_token("OFF") is False
56def test_parse_bool_token_invalid():
57 """Test parse_bool_token with invalid tokens raises ArgumentTypeError."""
58 with pytest.raises(argparse.ArgumentTypeError, match="expected one of"):
59 parse_bool_token("invalid")
61 with pytest.raises(argparse.ArgumentTypeError, match="expected one of"):
62 parse_bool_token("maybe")
64 with pytest.raises(argparse.ArgumentTypeError, match="expected one of"):
65 parse_bool_token("2")
67 with pytest.raises(argparse.ArgumentTypeError, match="expected one of"):
68 parse_bool_token("")
71def test_parse_bool_token_custom_sets():
72 """Test parse_bool_token with custom true/false sets."""
73 custom_true = {"enabled", "active"}
74 custom_false = {"disabled", "inactive"}
76 assert (
77 parse_bool_token("enabled", true_set=custom_true, false_set=custom_false)
78 is True
79 )
80 assert (
81 parse_bool_token("ACTIVE", true_set=custom_true, false_set=custom_false) is True
82 )
83 assert (
84 parse_bool_token("disabled", true_set=custom_true, false_set=custom_false)
85 is False
86 )
87 assert (
88 parse_bool_token("INACTIVE", true_set=custom_true, false_set=custom_false)
89 is False
90 )
92 # Default tokens should not work with custom sets
93 with pytest.raises(argparse.ArgumentTypeError):
94 parse_bool_token("true", true_set=custom_true, false_set=custom_false)
97# ============================================================================
98# Tests for BoolFlagOrValue
99# ============================================================================
102def test_BoolFlagOrValue_bare_flag():
103 """Test bare flag (--flag with no value) → True."""
104 parser = argparse.ArgumentParser()
105 parser.add_argument(
106 "--flag",
107 action=BoolFlagOrValue,
108 nargs="?",
109 default=False,
110 allow_bare=True,
111 )
113 # Bare flag should be True
114 args = parser.parse_args(["--flag"])
115 assert args.flag is True
117 # No flag should use default
118 args = parser.parse_args([])
119 assert args.flag is False
122def test_BoolFlagOrValue_negated():
123 """Test negated flag (--no-flag) → False."""
124 parser = argparse.ArgumentParser()
125 parser.add_argument(
126 "--flag",
127 "--no-flag",
128 dest="flag",
129 action=BoolFlagOrValue,
130 nargs="?",
131 default=True,
132 allow_no=True,
133 )
135 # --no-flag should be False
136 args = parser.parse_args(["--no-flag"])
137 assert args.flag is False
139 # --flag should be True (bare)
140 args = parser.parse_args(["--flag"])
141 assert args.flag is True
143 # No flag should use default
144 args = parser.parse_args([])
145 assert args.flag is True
148def test_BoolFlagOrValue_explicit_values():
149 """Test explicit values: --flag true, --flag false."""
150 parser = argparse.ArgumentParser()
151 parser.add_argument(
152 "--flag",
153 action=BoolFlagOrValue,
154 nargs="?",
155 default=False,
156 )
158 # --flag true
159 args = parser.parse_args(["--flag", "true"])
160 assert args.flag is True
162 # --flag false
163 args = parser.parse_args(["--flag", "false"])
164 assert args.flag is False
166 # --flag 1
167 args = parser.parse_args(["--flag", "1"])
168 assert args.flag is True
170 # --flag 0
171 args = parser.parse_args(["--flag", "0"])
172 assert args.flag is False
174 # --flag yes
175 args = parser.parse_args(["--flag", "yes"])
176 assert args.flag is True
178 # --flag no
179 args = parser.parse_args(["--flag", "no"])
180 assert args.flag is False
183def test_BoolFlagOrValue_equals_syntax():
184 """Test --flag=true and --flag=false syntax."""
185 parser = argparse.ArgumentParser()
186 parser.add_argument(
187 "--flag",
188 action=BoolFlagOrValue,
189 nargs="?",
190 default=False,
191 )
193 # --flag=true
194 args = parser.parse_args(["--flag=true"])
195 assert args.flag is True
197 # --flag=false
198 args = parser.parse_args(["--flag=false"])
199 assert args.flag is False
201 # --flag=1
202 args = parser.parse_args(["--flag=1"])
203 assert args.flag is True
205 # --flag=0
206 args = parser.parse_args(["--flag=0"])
207 assert args.flag is False
210def test_BoolFlagOrValue_allow_bare_false():
211 """Test error on bare flag when allow_bare=False."""
212 parser = argparse.ArgumentParser()
213 parser.add_argument(
214 "--flag",
215 action=BoolFlagOrValue,
216 nargs="?",
217 default=False,
218 allow_bare=False,
219 )
221 # Bare flag should error
222 with pytest.raises(SystemExit):
223 parser.parse_args(["--flag"])
225 # Explicit value should work
226 args = parser.parse_args(["--flag", "true"])
227 assert args.flag is True
230def test_BoolFlagOrValue_invalid_token():
231 """Test --flag invalid raises error."""
232 parser = argparse.ArgumentParser()
233 parser.add_argument(
234 "--flag",
235 action=BoolFlagOrValue,
236 nargs="?",
237 default=False,
238 )
240 # Invalid token should error
241 with pytest.raises(SystemExit):
242 parser.parse_args(["--flag", "invalid"])
244 with pytest.raises(SystemExit):
245 parser.parse_args(["--flag", "maybe"])
248def test_BoolFlagOrValue_no_flag_with_value_error():
249 """Test --no-flag with a value raises error."""
250 parser = argparse.ArgumentParser()
251 parser.add_argument(
252 "--flag",
253 "--no-flag",
254 dest="flag",
255 action=BoolFlagOrValue,
256 nargs="?",
257 default=True,
258 allow_no=True,
259 )
261 # --no-flag with value should error
262 with pytest.raises(SystemExit):
263 parser.parse_args(["--no-flag", "true"])
265 with pytest.raises(SystemExit):
266 parser.parse_args(["--no-flag=false"])
269def test_BoolFlagOrValue_allow_no_false():
270 """Test error when using --no-flag but allow_no=False."""
271 parser = argparse.ArgumentParser()
272 parser.add_argument(
273 "--flag",
274 "--no-flag",
275 dest="flag",
276 action=BoolFlagOrValue,
277 nargs="?",
278 default=True,
279 allow_no=False,
280 )
282 # --no-flag should error when allow_no=False
283 with pytest.raises(SystemExit):
284 parser.parse_args(["--no-flag"])
287def test_BoolFlagOrValue_custom_true_false_sets():
288 """Test BoolFlagOrValue with custom true/false sets."""
289 parser = argparse.ArgumentParser()
290 parser.add_argument(
291 "--flag",
292 action=BoolFlagOrValue,
293 nargs="?",
294 default=False,
295 true_set={"enabled", "active"},
296 false_set={"disabled", "inactive"},
297 )
299 args = parser.parse_args(["--flag", "enabled"])
300 assert args.flag is True
302 args = parser.parse_args(["--flag", "disabled"])
303 assert args.flag is False
305 # Default tokens should not work
306 with pytest.raises(SystemExit):
307 parser.parse_args(["--flag", "true"])
310def test_BoolFlagOrValue_invalid_nargs():
311 """Test that BoolFlagOrValue raises ValueError for invalid nargs."""
312 parser = argparse.ArgumentParser()
314 # nargs other than '?' or None should raise ValueError
315 with pytest.raises(ValueError, match="requires nargs='?'"):
316 parser.add_argument(
317 "--flag",
318 action=BoolFlagOrValue,
319 nargs=1,
320 )
322 with pytest.raises(ValueError, match="requires nargs='?'"):
323 parser.add_argument(
324 "--flag2",
325 action=BoolFlagOrValue,
326 nargs="*",
327 )
330def test_BoolFlagOrValue_type_not_allowed():
331 """Test that BoolFlagOrValue raises ValueError when type= is provided."""
332 parser = argparse.ArgumentParser()
334 with pytest.raises(ValueError, match="does not accept type="):
335 parser.add_argument(
336 "--flag",
337 action=BoolFlagOrValue,
338 nargs="?",
339 type=str,
340 )
343# ============================================================================
344# Tests for add_bool_flag
345# ============================================================================
348def test_add_bool_flag_integration():
349 """Test full integration with various argument combinations."""
350 parser = argparse.ArgumentParser()
351 add_bool_flag(parser, "feature", default=False, help="Enable feature")
353 # Bare flag
354 args = parser.parse_args(["--feature"])
355 assert args.feature is True
357 # Explicit true
358 args = parser.parse_args(["--feature", "true"])
359 assert args.feature is True
361 # Explicit false
362 args = parser.parse_args(["--feature", "false"])
363 assert args.feature is False
365 # Equals syntax
366 args = parser.parse_args(["--feature=true"])
367 assert args.feature is True
369 args = parser.parse_args(["--feature=false"])
370 assert args.feature is False
372 # No flag (default)
373 args = parser.parse_args([])
374 assert args.feature is False
377def test_add_bool_flag_allow_no():
378 """Test both --flag and --no-flag work when allow_no=True."""
379 parser = argparse.ArgumentParser()
380 add_bool_flag(parser, "feature", default=False, allow_no=True)
382 # --feature
383 args = parser.parse_args(["--feature"])
384 assert args.feature is True
386 # --no-feature
387 args = parser.parse_args(["--no-feature"])
388 assert args.feature is False
390 # No flag (default)
391 args = parser.parse_args([])
392 assert args.feature is False
395def test_add_bool_flag_dest_conversion():
396 """Test 'some-flag' → namespace.some_flag."""
397 parser = argparse.ArgumentParser()
398 add_bool_flag(parser, "some-flag", default=False)
400 args = parser.parse_args(["--some-flag"])
401 assert args.some_flag is True
402 assert not hasattr(args, "some-flag")
404 args = parser.parse_args(["--some-flag", "false"])
405 assert args.some_flag is False
408def test_add_bool_flag_custom_true_false_sets():
409 """Test add_bool_flag with custom true/false sets."""
410 parser = argparse.ArgumentParser()
411 add_bool_flag(
412 parser,
413 "feature",
414 default=False,
415 true_set={"enabled", "on"},
416 false_set={"disabled", "off"},
417 )
419 args = parser.parse_args(["--feature", "enabled"])
420 assert args.feature is True
422 args = parser.parse_args(["--feature", "disabled"])
423 assert args.feature is False
425 # Default tokens should not work
426 with pytest.raises(SystemExit):
427 parser.parse_args(["--feature", "true"])
430def test_add_bool_flag_allow_bare_false():
431 """Test add_bool_flag with allow_bare=False."""
432 parser = argparse.ArgumentParser()
433 add_bool_flag(parser, "feature", default=False, allow_bare=False)
435 # Bare flag should error
436 with pytest.raises(SystemExit):
437 parser.parse_args(["--feature"])
439 # Explicit value should work
440 args = parser.parse_args(["--feature", "true"])
441 assert args.feature is True
444def test_add_bool_flag_default_true():
445 """Test add_bool_flag with default=True."""
446 parser = argparse.ArgumentParser()
447 add_bool_flag(parser, "feature", default=True)
449 # No flag should use default=True
450 args = parser.parse_args([])
451 assert args.feature is True
453 # Explicit false should override
454 args = parser.parse_args(["--feature", "false"])
455 assert args.feature is False
458def test_add_bool_flag_multiple_flags():
459 """Test multiple boolean flags in the same parser."""
460 parser = argparse.ArgumentParser()
461 add_bool_flag(parser, "feature-a", default=False)
462 add_bool_flag(parser, "feature-b", default=True)
463 add_bool_flag(parser, "feature-c", default=False, allow_no=True)
465 args = parser.parse_args(
466 [
467 "--feature-a",
468 "--feature-b",
469 "false",
470 "--no-feature-c",
471 ]
472 )
473 assert args.feature_a is True
474 assert args.feature_b is False
475 assert args.feature_c is False
478def test_add_bool_flag_help_text():
479 """Test that help text is generated or used correctly."""
480 parser = argparse.ArgumentParser()
481 add_bool_flag(parser, "feature", default=False, help="Custom help text")
483 # Check that the help is stored (can't easily test output without parsing help text)
484 action = None
485 for act in parser._actions:
486 if hasattr(act, "dest") and act.dest == "feature":
487 action = act
488 break
490 assert action is not None
491 assert action.help == "Custom help text"
494def test_add_bool_flag_default_help():
495 """Test that default help text is generated when not provided."""
496 parser = argparse.ArgumentParser()
497 add_bool_flag(parser, "my-feature", default=False)
499 action = None
500 for act in parser._actions:
501 if hasattr(act, "dest") and act.dest == "my_feature":
502 action = act
503 break
505 assert action is not None
506 assert action.help is not None
507 assert "enable/disable my feature" in action.help
510# ============================================================================
511# Integration and edge case tests
512# ============================================================================
515def test_multiple_values_error():
516 """Test that passing multiple values to a flag raises an error."""
517 parser = argparse.ArgumentParser()
518 parser.add_argument(
519 "--flag",
520 action=BoolFlagOrValue,
521 nargs="?",
522 default=False,
523 )
525 # This should work with nargs='?', only one value accepted
526 args = parser.parse_args(["--flag", "true"])
527 assert args.flag is True
530@mark.parametrize(
531 "token, expected",
532 [
533 param("true", True, id="true"),
534 param("false", False, id="false"),
535 param("1", True, id="1"),
536 param("0", False, id="0"),
537 param("yes", True, id="yes"),
538 param("no", False, id="no"),
539 param("on", True, id="on"),
540 param("off", False, id="off"),
541 param("t", True, id="t"),
542 param("f", False, id="f"),
543 param("y", True, id="y"),
544 param("n", False, id="n"),
545 param("TRUE", True, id="TRUE"),
546 param("FALSE", False, id="FALSE"),
547 param("Yes", True, id="Yes"),
548 param("No", False, id="No"),
549 ],
550)
551def test_parse_bool_token_parametrized(token: str, expected: bool):
552 """Parametrized test for all valid boolean tokens."""
553 assert parse_bool_token(token) == expected
556@mark.parametrize(
557 "invalid_token",
558 [
559 param("invalid", id="invalid"),
560 param("maybe", id="maybe"),
561 param("2", id="2"),
562 param("-1", id="-1"),
563 param("", id="empty"),
564 param("truee", id="truee"),
565 param("yess", id="yess"),
566 ],
567)
568def test_parse_bool_token_invalid_parametrized(invalid_token: str):
569 """Parametrized test for invalid boolean tokens."""
570 with pytest.raises(argparse.ArgumentTypeError):
571 parse_bool_token(invalid_token)
574def test_constants_exist():
575 """Test that the default token sets are defined correctly."""
576 assert isinstance(TRUE_SET_DEFAULT, set)
577 assert isinstance(FALSE_SET_DEFAULT, set)
578 assert len(TRUE_SET_DEFAULT) > 0
579 assert len(FALSE_SET_DEFAULT) > 0
580 assert "true" in TRUE_SET_DEFAULT
581 assert "false" in FALSE_SET_DEFAULT
582 assert TRUE_SET_DEFAULT.isdisjoint(FALSE_SET_DEFAULT)