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

1"""Tests for muutils.cli.arg_bool module.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6import pytest 

7from pytest import mark, param 

8 

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) 

16 

17 

18# ============================================================================ 

19# Tests for parse_bool_token 

20# ============================================================================ 

21 

22 

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 

32 

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 

40 

41 

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 

54 

55 

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

60 

61 with pytest.raises(argparse.ArgumentTypeError, match="expected one of"): 

62 parse_bool_token("maybe") 

63 

64 with pytest.raises(argparse.ArgumentTypeError, match="expected one of"): 

65 parse_bool_token("2") 

66 

67 with pytest.raises(argparse.ArgumentTypeError, match="expected one of"): 

68 parse_bool_token("") 

69 

70 

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

75 

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 ) 

91 

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) 

95 

96 

97# ============================================================================ 

98# Tests for BoolFlagOrValue 

99# ============================================================================ 

100 

101 

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 ) 

112 

113 # Bare flag should be True 

114 args = parser.parse_args(["--flag"]) 

115 assert args.flag is True 

116 

117 # No flag should use default 

118 args = parser.parse_args([]) 

119 assert args.flag is False 

120 

121 

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 ) 

134 

135 # --no-flag should be False 

136 args = parser.parse_args(["--no-flag"]) 

137 assert args.flag is False 

138 

139 # --flag should be True (bare) 

140 args = parser.parse_args(["--flag"]) 

141 assert args.flag is True 

142 

143 # No flag should use default 

144 args = parser.parse_args([]) 

145 assert args.flag is True 

146 

147 

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 ) 

157 

158 # --flag true 

159 args = parser.parse_args(["--flag", "true"]) 

160 assert args.flag is True 

161 

162 # --flag false 

163 args = parser.parse_args(["--flag", "false"]) 

164 assert args.flag is False 

165 

166 # --flag 1 

167 args = parser.parse_args(["--flag", "1"]) 

168 assert args.flag is True 

169 

170 # --flag 0 

171 args = parser.parse_args(["--flag", "0"]) 

172 assert args.flag is False 

173 

174 # --flag yes 

175 args = parser.parse_args(["--flag", "yes"]) 

176 assert args.flag is True 

177 

178 # --flag no 

179 args = parser.parse_args(["--flag", "no"]) 

180 assert args.flag is False 

181 

182 

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 ) 

192 

193 # --flag=true 

194 args = parser.parse_args(["--flag=true"]) 

195 assert args.flag is True 

196 

197 # --flag=false 

198 args = parser.parse_args(["--flag=false"]) 

199 assert args.flag is False 

200 

201 # --flag=1 

202 args = parser.parse_args(["--flag=1"]) 

203 assert args.flag is True 

204 

205 # --flag=0 

206 args = parser.parse_args(["--flag=0"]) 

207 assert args.flag is False 

208 

209 

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 ) 

220 

221 # Bare flag should error 

222 with pytest.raises(SystemExit): 

223 parser.parse_args(["--flag"]) 

224 

225 # Explicit value should work 

226 args = parser.parse_args(["--flag", "true"]) 

227 assert args.flag is True 

228 

229 

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 ) 

239 

240 # Invalid token should error 

241 with pytest.raises(SystemExit): 

242 parser.parse_args(["--flag", "invalid"]) 

243 

244 with pytest.raises(SystemExit): 

245 parser.parse_args(["--flag", "maybe"]) 

246 

247 

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 ) 

260 

261 # --no-flag with value should error 

262 with pytest.raises(SystemExit): 

263 parser.parse_args(["--no-flag", "true"]) 

264 

265 with pytest.raises(SystemExit): 

266 parser.parse_args(["--no-flag=false"]) 

267 

268 

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 ) 

281 

282 # --no-flag should error when allow_no=False 

283 with pytest.raises(SystemExit): 

284 parser.parse_args(["--no-flag"]) 

285 

286 

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 ) 

298 

299 args = parser.parse_args(["--flag", "enabled"]) 

300 assert args.flag is True 

301 

302 args = parser.parse_args(["--flag", "disabled"]) 

303 assert args.flag is False 

304 

305 # Default tokens should not work 

306 with pytest.raises(SystemExit): 

307 parser.parse_args(["--flag", "true"]) 

308 

309 

310def test_BoolFlagOrValue_invalid_nargs(): 

311 """Test that BoolFlagOrValue raises ValueError for invalid nargs.""" 

312 parser = argparse.ArgumentParser() 

313 

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 ) 

321 

322 with pytest.raises(ValueError, match="requires nargs='?'"): 

323 parser.add_argument( 

324 "--flag2", 

325 action=BoolFlagOrValue, 

326 nargs="*", 

327 ) 

328 

329 

330def test_BoolFlagOrValue_type_not_allowed(): 

331 """Test that BoolFlagOrValue raises ValueError when type= is provided.""" 

332 parser = argparse.ArgumentParser() 

333 

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 ) 

341 

342 

343# ============================================================================ 

344# Tests for add_bool_flag 

345# ============================================================================ 

346 

347 

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

352 

353 # Bare flag 

354 args = parser.parse_args(["--feature"]) 

355 assert args.feature is True 

356 

357 # Explicit true 

358 args = parser.parse_args(["--feature", "true"]) 

359 assert args.feature is True 

360 

361 # Explicit false 

362 args = parser.parse_args(["--feature", "false"]) 

363 assert args.feature is False 

364 

365 # Equals syntax 

366 args = parser.parse_args(["--feature=true"]) 

367 assert args.feature is True 

368 

369 args = parser.parse_args(["--feature=false"]) 

370 assert args.feature is False 

371 

372 # No flag (default) 

373 args = parser.parse_args([]) 

374 assert args.feature is False 

375 

376 

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) 

381 

382 # --feature 

383 args = parser.parse_args(["--feature"]) 

384 assert args.feature is True 

385 

386 # --no-feature 

387 args = parser.parse_args(["--no-feature"]) 

388 assert args.feature is False 

389 

390 # No flag (default) 

391 args = parser.parse_args([]) 

392 assert args.feature is False 

393 

394 

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) 

399 

400 args = parser.parse_args(["--some-flag"]) 

401 assert args.some_flag is True 

402 assert not hasattr(args, "some-flag") 

403 

404 args = parser.parse_args(["--some-flag", "false"]) 

405 assert args.some_flag is False 

406 

407 

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 ) 

418 

419 args = parser.parse_args(["--feature", "enabled"]) 

420 assert args.feature is True 

421 

422 args = parser.parse_args(["--feature", "disabled"]) 

423 assert args.feature is False 

424 

425 # Default tokens should not work 

426 with pytest.raises(SystemExit): 

427 parser.parse_args(["--feature", "true"]) 

428 

429 

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) 

434 

435 # Bare flag should error 

436 with pytest.raises(SystemExit): 

437 parser.parse_args(["--feature"]) 

438 

439 # Explicit value should work 

440 args = parser.parse_args(["--feature", "true"]) 

441 assert args.feature is True 

442 

443 

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) 

448 

449 # No flag should use default=True 

450 args = parser.parse_args([]) 

451 assert args.feature is True 

452 

453 # Explicit false should override 

454 args = parser.parse_args(["--feature", "false"]) 

455 assert args.feature is False 

456 

457 

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) 

464 

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 

476 

477 

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

482 

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 

489 

490 assert action is not None 

491 assert action.help == "Custom help text" 

492 

493 

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) 

498 

499 action = None 

500 for act in parser._actions: 

501 if hasattr(act, "dest") and act.dest == "my_feature": 

502 action = act 

503 break 

504 

505 assert action is not None 

506 assert action.help is not None 

507 assert "enable/disable my feature" in action.help 

508 

509 

510# ============================================================================ 

511# Integration and edge case tests 

512# ============================================================================ 

513 

514 

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 ) 

524 

525 # This should work with nargs='?', only one value accepted 

526 args = parser.parse_args(["--flag", "true"]) 

527 assert args.flag is True 

528 

529 

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 

554 

555 

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) 

572 

573 

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)