Coverage for tests / unit / errormode / test_errormode_functionality.py: 61%

425 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-18 02:51 -0700

1from __future__ import annotations 

2 

3import warnings 

4 

5from muutils.errormode import ErrorMode 

6 

7import pytest 

8 

9 

10def test_except(): 

11 with pytest.raises(ValueError): 

12 ErrorMode.EXCEPT.process("test-except", except_cls=ValueError) 

13 

14 with pytest.raises(TypeError): 

15 ErrorMode.EXCEPT.process("test-except", except_cls=TypeError) 

16 

17 with pytest.raises(RuntimeError): 

18 ErrorMode.EXCEPT.process("test-except", except_cls=RuntimeError) 

19 

20 with pytest.raises(KeyError): 

21 ErrorMode.EXCEPT.process("test-except", except_cls=KeyError) 

22 

23 with pytest.raises(KeyError): 

24 ErrorMode.EXCEPT.process( 

25 "test-except", except_cls=KeyError, except_from=ValueError("base exception") 

26 ) 

27 

28 

29def test_warn(): 

30 with pytest.warns(UserWarning): 

31 ErrorMode.WARN.process("test-warn", warn_cls=UserWarning) 

32 

33 with pytest.warns(Warning): 

34 ErrorMode.WARN.process("test-warn", warn_cls=Warning) 

35 

36 with pytest.warns(DeprecationWarning): 

37 ErrorMode.WARN.process("test-warn", warn_cls=DeprecationWarning) 

38 

39 

40def test_ignore(): 

41 with warnings.catch_warnings(record=True) as w: 

42 ErrorMode.IGNORE.process("test-ignore") 

43 

44 ErrorMode.IGNORE.process("test-ignore", except_cls=ValueError) 

45 ErrorMode.IGNORE.process("test-ignore", except_from=TypeError("base exception")) 

46 

47 ErrorMode.IGNORE.process("test-ignore", warn_cls=UserWarning) 

48 

49 assert len(w) == 0, f"There should be no warnings: {w}" 

50 

51 

52def test_except_custom(): 

53 class MyCustomError(ValueError): 

54 pass 

55 

56 with pytest.raises(MyCustomError): 

57 ErrorMode.EXCEPT.process("test-except", except_cls=MyCustomError) 

58 

59 

60def test_warn_custom(): 

61 class MyCustomWarning(Warning): 

62 pass 

63 

64 with pytest.warns(MyCustomWarning): 

65 ErrorMode.WARN.process("test-warn", warn_cls=MyCustomWarning) 

66 

67 

68def test_except_mode_chained_exception(): 

69 try: 

70 # set up the base exception 

71 try: 

72 raise KeyError("base exception") 

73 except Exception as base_exception: 

74 # catch it, raise another exception with it as the cause 

75 ErrorMode.EXCEPT.process( 

76 "Test chained exception", 

77 except_cls=RuntimeError, 

78 except_from=base_exception, 

79 ) 

80 # catch the outer exception 

81 except RuntimeError as e: 

82 assert str(e) == "Test chained exception" 

83 # check that the cause is the base exception 

84 assert isinstance(e.__cause__, KeyError) 

85 assert repr(e.__cause__) == "KeyError('base exception')" 

86 else: 

87 assert False, "Expected RuntimeError with cause KeyError" 

88 

89 

90def test_logging_global(): 

91 import muutils.errormode as errormode 

92 

93 log: list[str] = [] 

94 

95 def log_func(msg: str): 

96 log.append(msg) 

97 

98 ErrorMode.LOG.process("test-log-print") 

99 

100 errormode.GLOBAL_LOG_FUNC = log_func 

101 

102 ErrorMode.LOG.process("test-log") 

103 ErrorMode.LOG.process("test-log-2") 

104 

105 assert log == ["test-log", "test-log-2"] 

106 

107 ErrorMode.LOG.process("test-log-3") 

108 

109 assert log == ["test-log", "test-log-2", "test-log-3"] 

110 

111 

112def test_custom_showwarning(): 

113 """Test custom_showwarning function with traceback handling and frame extraction.""" 

114 from muutils.errormode import custom_showwarning 

115 

116 # Capture warnings 

117 with warnings.catch_warnings(record=True) as w: 

118 warnings.simplefilter("always") 

119 

120 # Call custom_showwarning directly 

121 custom_showwarning("test warning message", UserWarning) 

122 

123 # Check that a warning was issued 

124 assert len(w) == 1 

125 assert issubclass(w[0].category, UserWarning) 

126 assert "test warning message" in str(w[0].message) 

127 

128 # Check that the warning has traceback information 

129 assert w[0].filename is not None 

130 assert w[0].lineno is not None 

131 

132 

133def test_custom_showwarning_with_category(): 

134 """Test custom_showwarning with different warning categories.""" 

135 from muutils.errormode import custom_showwarning 

136 

137 with warnings.catch_warnings(record=True) as w: 

138 warnings.simplefilter("always") 

139 

140 custom_showwarning("deprecation test", DeprecationWarning) 

141 

142 assert len(w) == 1 

143 assert issubclass(w[0].category, DeprecationWarning) 

144 

145 

146def test_custom_showwarning_default_category(): 

147 """Test custom_showwarning uses UserWarning as default.""" 

148 from muutils.errormode import custom_showwarning 

149 

150 with warnings.catch_warnings(record=True) as w: 

151 warnings.simplefilter("always") 

152 

153 # Call without specifying category 

154 custom_showwarning("default category test", category=None) 

155 

156 assert len(w) == 1 

157 assert issubclass(w[0].category, UserWarning) 

158 

159 

160def test_ErrorMode_process_except_from(): 

161 """Test exception chaining with except_from parameter.""" 

162 base_exception = ValueError("base error") 

163 

164 try: 

165 ErrorMode.EXCEPT.process( 

166 "chained error message", 

167 except_cls=RuntimeError, 

168 except_from=base_exception, 

169 ) 

170 except RuntimeError as e: 

171 # Check the exception message 

172 assert str(e) == "chained error message" 

173 # Check that __cause__ is set correctly 

174 assert e.__cause__ is base_exception 

175 assert isinstance(e.__cause__, ValueError) 

176 assert str(e.__cause__) == "base error" 

177 else: 

178 # TYPING: ty bug on python <= 3.9 

179 pytest.fail("Expected RuntimeError to be raised") # ty: ignore[arg-type,invalid-argument-type] 

180 

181 

182def test_ErrorMode_process_except_from_different_types(): 

183 """Test exception chaining with different exception types.""" 

184 # Test with KeyError -> TypeError 

185 base = KeyError("key not found") 

186 try: 

187 ErrorMode.EXCEPT.process("type error", except_cls=TypeError, except_from=base) 

188 except TypeError as e: 

189 assert e.__cause__ is base 

190 

191 # Test with AttributeError -> ValueError 

192 base2 = AttributeError("attribute missing") 

193 try: 

194 ErrorMode.EXCEPT.process( 

195 "value error", except_cls=ValueError, except_from=base2 

196 ) 

197 except ValueError as e: 

198 assert e.__cause__ is base2 

199 

200 

201def test_ErrorMode_process_custom_funcs(): 

202 """Test custom warn_func and log_func parameters.""" 

203 # Test custom warn_func 

204 warnings_captured = [] 

205 

206 def custom_warn(msg: str, category, source=None): 

207 warnings_captured.append({"msg": msg, "category": category, "source": source}) 

208 

209 ErrorMode.WARN.process( 

210 "custom warn test", warn_cls=UserWarning, warn_func=custom_warn 

211 ) 

212 

213 assert len(warnings_captured) == 1 

214 assert warnings_captured[0]["msg"] == "custom warn test" 

215 assert warnings_captured[0]["category"] == UserWarning # noqa: E721 

216 

217 # Test custom log_func 

218 logs_captured = [] 

219 

220 def custom_log(msg: str): 

221 logs_captured.append(msg) 

222 

223 ErrorMode.LOG.process("custom log test", log_func=custom_log) 

224 

225 assert len(logs_captured) == 1 

226 assert logs_captured[0] == "custom log test" 

227 

228 

229def test_ErrorMode_process_custom_warn_func_with_except_from(): 

230 """Test custom warn_func with except_from to augment message.""" 

231 warnings_captured = [] 

232 

233 def custom_warn(msg: str, category, source=None): 

234 warnings_captured.append(msg) 

235 

236 base_exception = ValueError("source exception") 

237 

238 ErrorMode.WARN.process( 

239 "warning message", 

240 warn_cls=UserWarning, 

241 warn_func=custom_warn, 

242 except_from=base_exception, 

243 ) 

244 

245 assert len(warnings_captured) == 1 

246 # Check that the message is augmented with source 

247 assert "warning message" in warnings_captured[0] 

248 assert "Source of warning" in warnings_captured[0] 

249 assert "source exception" in warnings_captured[0] 

250 

251 

252def test_ErrorMode_serialize_load(): 

253 """Test round-trip serialization and loading.""" 

254 # Test EXCEPT 

255 serialized = ErrorMode.EXCEPT.serialize() 

256 loaded = ErrorMode.load(serialized) 

257 assert loaded is ErrorMode.EXCEPT 

258 

259 # Test WARN 

260 serialized = ErrorMode.WARN.serialize() 

261 loaded = ErrorMode.load(serialized) 

262 assert loaded is ErrorMode.WARN 

263 

264 # Test LOG 

265 serialized = ErrorMode.LOG.serialize() 

266 loaded = ErrorMode.load(serialized) 

267 assert loaded is ErrorMode.LOG 

268 

269 # Test IGNORE 

270 serialized = ErrorMode.IGNORE.serialize() 

271 loaded = ErrorMode.load(serialized) 

272 assert loaded is ErrorMode.IGNORE 

273 

274 

275def test_ErrorMode_serialize_format(): 

276 """Test that serialize returns the expected format.""" 

277 assert ErrorMode.EXCEPT.serialize() == "ErrorMode.Except" 

278 assert ErrorMode.WARN.serialize() == "ErrorMode.Warn" 

279 assert ErrorMode.LOG.serialize() == "ErrorMode.Log" 

280 assert ErrorMode.IGNORE.serialize() == "ErrorMode.Ignore" 

281 

282 

283def test_ERROR_MODE_ALIASES(): 

284 """Test that all aliases resolve correctly.""" 

285 from muutils.errormode import ERROR_MODE_ALIASES 

286 

287 # Test EXCEPT aliases 

288 assert ERROR_MODE_ALIASES["except"] is ErrorMode.EXCEPT 

289 assert ERROR_MODE_ALIASES["e"] is ErrorMode.EXCEPT 

290 assert ERROR_MODE_ALIASES["error"] is ErrorMode.EXCEPT 

291 assert ERROR_MODE_ALIASES["err"] is ErrorMode.EXCEPT 

292 assert ERROR_MODE_ALIASES["raise"] is ErrorMode.EXCEPT 

293 

294 # Test WARN aliases 

295 assert ERROR_MODE_ALIASES["warn"] is ErrorMode.WARN 

296 assert ERROR_MODE_ALIASES["w"] is ErrorMode.WARN 

297 assert ERROR_MODE_ALIASES["warning"] is ErrorMode.WARN 

298 

299 # Test LOG aliases 

300 assert ERROR_MODE_ALIASES["log"] is ErrorMode.LOG 

301 assert ERROR_MODE_ALIASES["l"] is ErrorMode.LOG 

302 assert ERROR_MODE_ALIASES["print"] is ErrorMode.LOG 

303 assert ERROR_MODE_ALIASES["output"] is ErrorMode.LOG 

304 assert ERROR_MODE_ALIASES["show"] is ErrorMode.LOG 

305 assert ERROR_MODE_ALIASES["display"] is ErrorMode.LOG 

306 

307 # Test IGNORE aliases 

308 assert ERROR_MODE_ALIASES["ignore"] is ErrorMode.IGNORE 

309 assert ERROR_MODE_ALIASES["i"] is ErrorMode.IGNORE 

310 assert ERROR_MODE_ALIASES["silent"] is ErrorMode.IGNORE 

311 assert ERROR_MODE_ALIASES["quiet"] is ErrorMode.IGNORE 

312 assert ERROR_MODE_ALIASES["nothing"] is ErrorMode.IGNORE 

313 

314 

315def test_ErrorMode_from_any_with_string(): 

316 """Test from_any with string inputs.""" 

317 # Test base values 

318 assert ErrorMode.from_any("except") is ErrorMode.EXCEPT 

319 assert ErrorMode.from_any("warn") is ErrorMode.WARN 

320 assert ErrorMode.from_any("log") is ErrorMode.LOG 

321 assert ErrorMode.from_any("ignore") is ErrorMode.IGNORE 

322 

323 # Test with uppercase 

324 assert ErrorMode.from_any("EXCEPT") is ErrorMode.EXCEPT 

325 assert ErrorMode.from_any("WARN") is ErrorMode.WARN 

326 

327 # Test with whitespace 

328 assert ErrorMode.from_any(" except ") is ErrorMode.EXCEPT 

329 assert ErrorMode.from_any(" warn ") is ErrorMode.WARN 

330 

331 

332def test_ErrorMode_from_any_with_aliases(): 

333 """Test from_any with alias strings.""" 

334 # Test EXCEPT aliases 

335 assert ErrorMode.from_any("error") is ErrorMode.EXCEPT 

336 assert ErrorMode.from_any("e") is ErrorMode.EXCEPT 

337 assert ErrorMode.from_any("raise") is ErrorMode.EXCEPT 

338 

339 # Test WARN aliases 

340 assert ErrorMode.from_any("warning") is ErrorMode.WARN 

341 assert ErrorMode.from_any("w") is ErrorMode.WARN 

342 

343 # Test LOG aliases 

344 assert ErrorMode.from_any("print") is ErrorMode.LOG 

345 assert ErrorMode.from_any("l") is ErrorMode.LOG 

346 assert ErrorMode.from_any("output") is ErrorMode.LOG 

347 

348 # Test IGNORE aliases 

349 assert ErrorMode.from_any("silent") is ErrorMode.IGNORE 

350 assert ErrorMode.from_any("i") is ErrorMode.IGNORE 

351 assert ErrorMode.from_any("quiet") is ErrorMode.IGNORE 

352 

353 

354def test_ErrorMode_from_any_with_prefix(): 

355 """Test from_any with ErrorMode. prefix.""" 

356 assert ErrorMode.from_any("ErrorMode.except") is ErrorMode.EXCEPT 

357 assert ErrorMode.from_any("ErrorMode.warn") is ErrorMode.WARN 

358 assert ErrorMode.from_any("ErrorMode.log") is ErrorMode.LOG 

359 assert ErrorMode.from_any("ErrorMode.ignore") is ErrorMode.IGNORE 

360 

361 # Test with mixed case 

362 assert ErrorMode.from_any("ErrorMode.Except") is ErrorMode.EXCEPT 

363 assert ErrorMode.from_any("ErrorMode.WARN") is ErrorMode.WARN 

364 

365 

366def test_ErrorMode_from_any_with_ErrorMode_instance(): 

367 """Test from_any with ErrorMode instance.""" 

368 assert ErrorMode.from_any(ErrorMode.EXCEPT) is ErrorMode.EXCEPT 

369 assert ErrorMode.from_any(ErrorMode.WARN) is ErrorMode.WARN 

370 assert ErrorMode.from_any(ErrorMode.LOG) is ErrorMode.LOG 

371 assert ErrorMode.from_any(ErrorMode.IGNORE) is ErrorMode.IGNORE 

372 

373 

374def test_ErrorMode_from_any_without_aliases(): 

375 """Test from_any with allow_aliases=False.""" 

376 # Base values should still work 

377 assert ErrorMode.from_any("except", allow_aliases=False) is ErrorMode.EXCEPT 

378 

379 # Aliases should fail 

380 with pytest.raises(KeyError): 

381 ErrorMode.from_any("error", allow_aliases=False) 

382 

383 with pytest.raises(KeyError): 

384 ErrorMode.from_any("e", allow_aliases=False) 

385 

386 

387def test_ErrorMode_from_any_invalid_string(): 

388 """Test from_any with invalid string.""" 

389 with pytest.raises(KeyError): 

390 ErrorMode.from_any("invalid_mode") 

391 

392 with pytest.raises(KeyError): 

393 ErrorMode.from_any("not_a_mode") 

394 

395 

396def test_ErrorMode_from_any_invalid_type(): 

397 """Test from_any with invalid type.""" 

398 with pytest.raises(TypeError): 

399 ErrorMode.from_any(123) # type: ignore 

400 

401 with pytest.raises(TypeError): 

402 ErrorMode.from_any(None) # type: ignore 

403 

404 with pytest.raises(TypeError): 

405 ErrorMode.from_any([]) # type: ignore 

406 

407 

408def test_ErrorMode_str_repr(): 

409 """Test __str__ and __repr__ methods.""" 

410 assert str(ErrorMode.EXCEPT) == "ErrorMode.Except" 

411 assert str(ErrorMode.WARN) == "ErrorMode.Warn" 

412 assert str(ErrorMode.LOG) == "ErrorMode.Log" 

413 assert str(ErrorMode.IGNORE) == "ErrorMode.Ignore" 

414 

415 assert repr(ErrorMode.EXCEPT) == "ErrorMode.Except" 

416 assert repr(ErrorMode.WARN) == "ErrorMode.Warn" 

417 assert repr(ErrorMode.LOG) == "ErrorMode.Log" 

418 assert repr(ErrorMode.IGNORE) == "ErrorMode.Ignore" 

419 

420 

421def test_ErrorMode_process_unknown_mode(): 

422 """Test that an unknown error mode raises ValueError.""" 

423 # This is a edge case that shouldn't normally happen, but testing defensively 

424 # We can't easily create an invalid ErrorMode, so we test the else branch 

425 # by mocking or checking that all modes are handled 

426 # All enum values should be handled in process, so this is more of a sanity check 

427 pass 

428 

429 

430def test_warn_with_except_from_builtin(): 

431 """Test WARN mode with except_from using built-in warnings.warn.""" 

432 import muutils.errormode as errormode 

433 

434 # Make sure we're using the default warn function 

435 errormode.GLOBAL_WARN_FUNC = warnings.warn # type: ignore 

436 

437 with warnings.catch_warnings(record=True) as w: 

438 warnings.simplefilter("always") 

439 

440 base_exception = ValueError("base error") 

441 ErrorMode.WARN.process( 

442 "test warning", warn_cls=UserWarning, except_from=base_exception 

443 ) 

444 

445 assert len(w) == 1 

446 # Message should include source information 

447 message_str = str(w[0].message) 

448 assert "test warning" in message_str 

449 assert "Source of warning" in message_str 

450 assert "base error" in message_str 

451 

452 

453def test_custom_showwarning_with_warning_instance(): 

454 """Test custom_showwarning when passed a Warning instance instead of string.""" 

455 from muutils.errormode import custom_showwarning 

456 

457 with warnings.catch_warnings(record=True) as w: 

458 warnings.simplefilter("always") 

459 

460 # Create a warning instance 

461 warning_instance = UserWarning("instance warning") 

462 custom_showwarning(warning_instance, UserWarning) 

463 

464 assert len(w) == 1 

465 assert "instance warning" in str(w[0].message) 

466 

467 

468def test_log_with_custom_func(): 

469 """Test LOG mode with custom log function passed directly.""" 

470 logs = [] 

471 

472 def my_logger(msg: str): 

473 logs.append(f"LOGGED: {msg}") 

474 

475 ErrorMode.LOG.process("test message", log_func=my_logger) 

476 

477 assert len(logs) == 1 

478 assert logs[0] == "LOGGED: test message" 

479 

480 

481def test_multiple_log_functions(): 

482 """Test that different log functions can be used.""" 

483 log1 = [] 

484 log2 = [] 

485 

486 def logger1(msg: str): 

487 log1.append(msg) 

488 

489 def logger2(msg: str): 

490 log2.append(msg) 

491 

492 ErrorMode.LOG.process("message 1", log_func=logger1) 

493 ErrorMode.LOG.process("message 2", log_func=logger2) 

494 

495 assert log1 == ["message 1"] 

496 assert log2 == ["message 2"] 

497 

498 

499def test_warn_with_source_parameter(): 

500 """Test that warn_func receives proper parameters.""" 

501 calls = [] 

502 

503 def tracking_warn(msg: str, category, source=None): 

504 calls.append({"msg": msg, "category": category, "source": source}) 

505 

506 ErrorMode.WARN.process( 

507 "test message", warn_cls=DeprecationWarning, warn_func=tracking_warn 

508 ) 

509 

510 assert len(calls) == 1 

511 assert calls[0]["msg"] == "test message" 

512 assert calls[0]["category"] == DeprecationWarning # noqa: E721 

513 

514 

515def test_ErrorMode_enum_values(): 

516 """Test that ErrorMode has the expected enum values.""" 

517 assert ErrorMode.EXCEPT.value == "except" 

518 assert ErrorMode.WARN.value == "warn" 

519 assert ErrorMode.LOG.value == "log" 

520 assert ErrorMode.IGNORE.value == "ignore" 

521 

522 

523def test_from_any_without_prefix(): 

524 """Test from_any with allow_prefix=False.""" 

525 # Should still work with plain values 

526 assert ErrorMode.from_any("except", allow_prefix=False) is ErrorMode.EXCEPT 

527 

528 # Should fail with prefix 

529 with pytest.raises(KeyError): 

530 ErrorMode.from_any("ErrorMode.except", allow_prefix=False) 

531 

532 

533def test_GLOBAL_WARN_FUNC(): 

534 """Test that GLOBAL_WARN_FUNC is used when no warn_func is provided.""" 

535 import muutils.errormode as errormode 

536 

537 # Save original 

538 original_warn_func = errormode.GLOBAL_WARN_FUNC 

539 

540 try: 

541 # Set custom global warn function 

542 captured = [] 

543 

544 def global_warn(msg: str, category, source=None): 

545 captured.append(msg) 

546 

547 errormode.GLOBAL_WARN_FUNC = global_warn # type: ignore 

548 

549 # Use WARN mode without providing warn_func 

550 ErrorMode.WARN.process("test with global", warn_cls=UserWarning) 

551 

552 assert len(captured) == 1 

553 assert captured[0] == "test with global" 

554 

555 finally: 

556 # Restore original 

557 errormode.GLOBAL_WARN_FUNC = original_warn_func 

558 

559 

560def test_GLOBAL_LOG_FUNC(): 

561 """Test that GLOBAL_LOG_FUNC is used when no log_func is provided.""" 

562 import muutils.errormode as errormode 

563 

564 # Save original 

565 original_log_func = errormode.GLOBAL_LOG_FUNC 

566 

567 try: 

568 # Set custom global log function 

569 captured = [] 

570 

571 def global_log(msg: str): 

572 captured.append(msg) 

573 

574 errormode.GLOBAL_LOG_FUNC = global_log 

575 

576 # Use LOG mode without providing log_func 

577 ErrorMode.LOG.process("test with global log") 

578 

579 assert len(captured) == 1 

580 assert captured[0] == "test with global log" 

581 

582 finally: 

583 # Restore original 

584 errormode.GLOBAL_LOG_FUNC = original_log_func 

585 

586 

587def test_custom_warn_func_signature(): 

588 """Test that custom warn_func follows the WarningFunc protocol.""" 

589 from muutils.errormode import WarningFunc 

590 

591 # Create a function that matches the protocol 

592 def my_warn(msg: str, category: type[Warning], source=None) -> None: 

593 pass 

594 

595 # This should work without errors 

596 warn_func: WarningFunc = my_warn # type: ignore 

597 

598 # Use it with ErrorMode 

599 ErrorMode.WARN.process("test", warn_cls=UserWarning, warn_func=warn_func) 

600 

601 

602def test_ErrorMode_all_enum_members(): 

603 """Test that all ErrorMode enum members are accessible.""" 

604 # Verify all enum members exist 

605 assert hasattr(ErrorMode, "EXCEPT") 

606 assert hasattr(ErrorMode, "WARN") 

607 assert hasattr(ErrorMode, "LOG") 

608 assert hasattr(ErrorMode, "IGNORE") 

609 

610 # Test that they are unique 

611 modes = [ErrorMode.EXCEPT, ErrorMode.WARN, ErrorMode.LOG, ErrorMode.IGNORE] 

612 assert len(set(modes)) == 4 

613 

614 

615def test_custom_showwarning_frame_extraction(): 

616 """Test that custom_showwarning correctly extracts frame information.""" 

617 import sys 

618 from muutils.errormode import custom_showwarning 

619 

620 with warnings.catch_warnings(record=True) as w: 

621 warnings.simplefilter("always") 

622 

623 # Call from this specific line so we can verify frame info 

624 line_number = 0 

625 

626 def call_showwarning(): 

627 nonlocal line_number 

628 line_number = sys._getframe().f_lineno + 1 

629 custom_showwarning("frame test", UserWarning) 

630 

631 call_showwarning() 

632 

633 assert len(w) == 1 

634 # The warning should have been issued with correct file and line info 

635 assert w[0].filename == __file__ 

636 # Line number should be close to where we called it 

637 assert isinstance(w[0].lineno, int) 

638 

639 

640def test_exception_traceback_attached(): 

641 """Test that raised exceptions have traceback attached.""" 

642 try: 

643 ErrorMode.EXCEPT.process("test traceback", except_cls=ValueError) 

644 except ValueError as e: 

645 # Check that exception has traceback 

646 assert e.__traceback__ is not None 

647 else: 

648 # TYPING: ty bug on python <= 3.9 

649 pytest.fail("Expected ValueError to be raised") # ty: ignore[arg-type,invalid-argument-type] 

650 

651 

652def test_exception_traceback_with_chaining(): 

653 """Test that chained exceptions have correct traceback.""" 

654 base = RuntimeError("base") 

655 

656 try: 

657 ErrorMode.EXCEPT.process("chained", except_cls=ValueError, except_from=base) 

658 except ValueError as e: 

659 # Check traceback exists 

660 assert e.__traceback__ is not None 

661 # Check cause is set 

662 assert e.__cause__ is base 

663 else: 

664 # TYPING: ty bug on python <= 3.9 

665 pytest.fail("Expected ValueError to be raised") # ty: ignore[arg-type,invalid-argument-type] 

666 

667 

668def test_warn_with_default_warn_func(): 

669 """Test WARN mode with default warnings.warn function.""" 

670 import muutils.errormode as errormode 

671 

672 # Ensure we're using default 

673 errormode.GLOBAL_WARN_FUNC = warnings.warn # type: ignore 

674 

675 with warnings.catch_warnings(record=True) as w: 

676 warnings.simplefilter("always") 

677 

678 ErrorMode.WARN.process("default warn func test", warn_cls=UserWarning) 

679 

680 assert len(w) == 1 

681 assert "default warn func test" in str(w[0].message) 

682 

683 

684def test_from_any_strip_whitespace(): 

685 """Test that from_any strips whitespace correctly.""" 

686 # Leading/trailing spaces 

687 assert ErrorMode.from_any(" except") is ErrorMode.EXCEPT 

688 assert ErrorMode.from_any("warn ") is ErrorMode.WARN 

689 assert ErrorMode.from_any(" log ") is ErrorMode.LOG 

690 

691 # Tabs and newlines 

692 assert ErrorMode.from_any("\texcept\t") is ErrorMode.EXCEPT 

693 assert ErrorMode.from_any("\nwarn\n") is ErrorMode.WARN 

694 

695 

696def test_load_with_prefix(): 

697 """Test load method with ErrorMode. prefix.""" 

698 # load uses allow_prefix=True 

699 loaded = ErrorMode.load("ErrorMode.Except") 

700 assert loaded is ErrorMode.EXCEPT 

701 

702 loaded = ErrorMode.load("ErrorMode.warn") 

703 assert loaded is ErrorMode.WARN 

704 

705 

706def test_load_without_aliases(): 

707 """Test that load does not accept aliases.""" 

708 # load uses allow_aliases=False 

709 with pytest.raises((KeyError, ValueError)): 

710 ErrorMode.load("error") # alias should not work 

711 

712 with pytest.raises((KeyError, ValueError)): 

713 ErrorMode.load("e") # alias should not work 

714 

715 

716def test_ERROR_MODE_ALIASES_completeness(): 

717 """Test that ERROR_MODE_ALIASES contains all expected aliases.""" 

718 from muutils.errormode import ERROR_MODE_ALIASES 

719 

720 # Count aliases per mode 

721 except_aliases = [k for k, v in ERROR_MODE_ALIASES.items() if v is ErrorMode.EXCEPT] 

722 warn_aliases = [k for k, v in ERROR_MODE_ALIASES.items() if v is ErrorMode.WARN] 

723 log_aliases = [k for k, v in ERROR_MODE_ALIASES.items() if v is ErrorMode.LOG] 

724 ignore_aliases = [k for k, v in ERROR_MODE_ALIASES.items() if v is ErrorMode.IGNORE] 

725 

726 # Verify we have multiple aliases for each mode 

727 assert len(except_aliases) >= 5 # except, e, error, err, raise 

728 assert len(warn_aliases) >= 3 # warn, w, warning 

729 assert len(log_aliases) >= 6 # log, l, print, output, show, display 

730 assert len(ignore_aliases) >= 5 # ignore, i, silent, quiet, nothing 

731 

732 

733def test_custom_exception_classes(): 

734 """Test process with various custom exception classes.""" 

735 

736 class CustomError(Exception): 

737 pass 

738 

739 class NestedCustomError(CustomError): 

740 pass 

741 

742 # Test with custom exception 

743 with pytest.raises(CustomError): 

744 ErrorMode.EXCEPT.process("custom", except_cls=CustomError) 

745 

746 # Test with nested custom exception 

747 with pytest.raises(NestedCustomError): 

748 ErrorMode.EXCEPT.process("nested custom", except_cls=NestedCustomError) 

749 

750 

751def test_custom_warning_classes(): 

752 """Test process with various custom warning classes.""" 

753 

754 class CustomWarning(UserWarning): 

755 pass 

756 

757 class NestedCustomWarning(CustomWarning): 

758 pass 

759 

760 # Test with custom warning 

761 with warnings.catch_warnings(record=True) as w: 

762 warnings.simplefilter("always") 

763 

764 def custom_warn(msg: str, category, source=None): 

765 warnings.warn(msg, category) 

766 

767 ErrorMode.WARN.process("custom", warn_cls=CustomWarning, warn_func=custom_warn) 

768 

769 assert len(w) == 1 

770 assert issubclass(w[0].category, CustomWarning) 

771 

772 

773def test_ignore_with_all_parameters(): 

774 """Test that IGNORE mode ignores all parameters.""" 

775 # None of these should raise or warn 

776 ErrorMode.IGNORE.process("ignored message") 

777 ErrorMode.IGNORE.process("ignored", except_cls=ValueError) 

778 ErrorMode.IGNORE.process("ignored", warn_cls=UserWarning) 

779 ErrorMode.IGNORE.process("ignored", except_from=ValueError("base")) 

780 

781 # Also test with custom functions (they should not be called) 

782 called = [] 

783 

784 def should_not_be_called(msg: str): 

785 called.append(msg) 

786 

787 ErrorMode.IGNORE.process("ignored", log_func=should_not_be_called) 

788 

789 # log_func should not have been called 

790 assert len(called) == 0 

791 

792 

793def test_from_any_case_insensitivity(): 

794 """Test that from_any is case insensitive.""" 

795 # Test various cases 

796 assert ErrorMode.from_any("EXCEPT") is ErrorMode.EXCEPT 

797 assert ErrorMode.from_any("Except") is ErrorMode.EXCEPT 

798 assert ErrorMode.from_any("eXcEpT") is ErrorMode.EXCEPT 

799 

800 assert ErrorMode.from_any("WARN") is ErrorMode.WARN 

801 assert ErrorMode.from_any("Warn") is ErrorMode.WARN 

802 

803 # Test with aliases 

804 assert ErrorMode.from_any("ERROR") is ErrorMode.EXCEPT 

805 assert ErrorMode.from_any("Error") is ErrorMode.EXCEPT 

806 assert ErrorMode.from_any("RAISE") is ErrorMode.EXCEPT 

807 

808 

809# def test_logging_pass(): 

810# errmode: ErrorMode = ErrorMode.LOG 

811 

812# log: list[str] = [] 

813# def log_func(msg: str): 

814# log.append(msg) 

815 

816# errmode.process( 

817# "test-log", 

818# log_func=log_func, 

819# ) 

820 

821# errmode.process( 

822# "test-log-2", 

823# log_func=log_func, 

824# ) 

825 

826# assert log == ["test-log", "test-log-2"] 

827 

828 

829# def test_logging_init(): 

830# errmode: ErrorMode = ErrorMode.LOG 

831 

832# log: list[str] = [] 

833# def log_func(msg: str): 

834# log.append(msg) 

835 

836# errmode.set_log_loc(log_func) 

837 

838# errmode.process("test-log") 

839# errmode.process("test-log-2") 

840 

841# assert log == ["test-log", "test-log-2"] 

842 

843# errmode_2: ErrorMode = ErrorMode.LOG 

844# log_2: list[str] = [] 

845# def log_func_2(msg: str): 

846# log_2.append(msg) 

847 

848# errmode_2.set_log_loc(log_func_2) 

849 

850# errmode_2.process("test-log-3") 

851# errmode_2.process("test-log-4") 

852 

853# assert log_2 == ["test-log-3", "test-log-4"] 

854# assert log == ["test-log", "test-log-2"] 

855 

856 

857# def test_logging_init_2(): 

858# log: list[str] = [] 

859# def log_func(msg: str): 

860# log.append(msg) 

861 

862# errmode: ErrorMode = ErrorMode.LOG.set_log_loc(log_func) 

863 

864# errmode.process("test-log") 

865# errmode.process("test-log-2") 

866 

867# assert log == ["test-log", "test-log-2"]