Coverage for tests / unit / test_dbg.py: 99%

294 statements  

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

1import inspect 

2import tempfile 

3from pathlib import Path 

4import importlib 

5from typing import Any, Callable, Optional, List, Tuple 

6import re 

7 

8import pytest 

9 

10from muutils.dbg import ( 

11 dbg, 

12 _NoExpPassed, 

13 _process_path, 

14 _CWD, 

15 # we do use this as a global in `test_dbg_counter_increments` 

16 _COUNTER, 

17 dbg_auto, 

18 dbg_dict, # noqa: F401 

19 grep_repr, 

20 _normalize_for_loose, 

21 _compile_pattern, 

22) 

23 

24assert _COUNTER is not None 

25 

26 

27DBG_MODULE_NAME: str = "muutils.dbg" 

28 

29# ============================================================================ 

30# Dummy Tensor classes for testing tensor_info* functions 

31# ============================================================================ 

32 

33 

34class DummyTensor: 

35 """A dummy tensor whose sum is NaN.""" 

36 

37 shape: Tuple[int, ...] = (2, 3) 

38 dtype: str = "float32" 

39 device: str = "cpu" 

40 requires_grad: bool = False 

41 

42 def sum(self) -> float: 

43 return float("nan") 

44 

45 

46class DummyTensorNormal: 

47 """A dummy tensor with a normal sum.""" 

48 

49 shape: Tuple[int, ...] = (4, 5) 

50 dtype: str = "int32" 

51 device: str = "cuda" 

52 requires_grad: bool = True 

53 

54 def sum(self) -> float: 

55 return 20.0 

56 

57 

58class DummyTensorPartial: 

59 """A dummy tensor with only a shape attribute.""" 

60 

61 shape: Tuple[int, ...] = (7,) 

62 

63 

64# ============================================================================ 

65# Additional Tests for dbg and tensor_info functionality 

66# ============================================================================ 

67 

68 

69# --- Tests for _process_path (existing ones) --- 

70def test_process_path_absolute(monkeypatch: pytest.MonkeyPatch) -> None: 

71 monkeypatch.setattr( 

72 importlib.import_module(DBG_MODULE_NAME), "PATH_MODE", "absolute" 

73 ) 

74 test_path: Path = Path("somefile.txt") 

75 expected: str = test_path.absolute().as_posix() 

76 result: str = _process_path(test_path) 

77 assert result == expected 

78 

79 

80def test_process_path_relative_inside_common(monkeypatch: pytest.MonkeyPatch) -> None: 

81 monkeypatch.setattr( 

82 importlib.import_module(DBG_MODULE_NAME), "PATH_MODE", "relative" 

83 ) 

84 test_path: Path = _CWD / "file.txt" 

85 expected: str = "file.txt" 

86 result: str = _process_path(test_path) 

87 assert result == expected 

88 

89 

90def test_process_path_relative_outside_common(monkeypatch: pytest.MonkeyPatch) -> None: 

91 monkeypatch.setattr( 

92 importlib.import_module(DBG_MODULE_NAME), "PATH_MODE", "relative" 

93 ) 

94 with tempfile.TemporaryDirectory() as tmp_dir: 

95 test_path: Path = Path(tmp_dir) / "file.txt" 

96 expected: str = test_path.absolute().as_posix() 

97 result: str = _process_path(test_path) 

98 assert result == expected 

99 

100 

101def test_process_path_invalid_mode(monkeypatch: pytest.MonkeyPatch) -> None: 

102 monkeypatch.setattr( 

103 importlib.import_module(DBG_MODULE_NAME), "PATH_MODE", "invalid" 

104 ) 

105 with pytest.raises( 

106 ValueError, match="PATH_MODE must be either 'relative' or 'absolute" 

107 ): 

108 _process_path(Path("anything.txt")) 

109 

110 

111# --- Tests for dbg --- 

112def test_dbg_with_expression(capsys: pytest.CaptureFixture) -> None: 

113 result: int = dbg(1 + 2) 

114 captured: str = capsys.readouterr().err 

115 assert "= 3" in captured 

116 # check that the printed string includes some form of "1+2" 

117 assert "1+2" in captured.replace(" ", "") or "1 + 2" in captured 

118 assert result == 3 

119 

120 

121def test_dbg_without_expression( 

122 monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture 

123) -> None: 

124 monkeypatch.setattr(importlib.import_module(DBG_MODULE_NAME), "_COUNTER", 0) 

125 result: Any = dbg() 

126 captured: str = capsys.readouterr().err.strip() 

127 assert "<dbg 0>" in captured 

128 no_exp_passed: Any = _NoExpPassed 

129 assert result is no_exp_passed 

130 

131 

132def test_dbg_custom_formatter(capsys: pytest.CaptureFixture) -> None: 

133 custom_formatter: Callable[[Any], str] = lambda x: "custom" # noqa: E731 

134 result: str = dbg("anything", formatter=custom_formatter) 

135 captured: str = capsys.readouterr().err 

136 assert "custom" in captured 

137 assert result == "anything" 

138 

139 

140def test_dbg_complex_expression(capsys: pytest.CaptureFixture) -> None: 

141 # Test a complex expression (lambda invocation) 

142 result: int = dbg((lambda x: x * x)(5)) # pyright: ignore[reportCallIssue, reportArgumentType] 

143 captured: str = capsys.readouterr().err 

144 assert ( 

145 "lambda" in captured 

146 ) # expecting the extracted code snippet to include 'lambda' 

147 assert "25" in captured # evaluated result is 25 

148 assert result == 25 

149 

150 

151def test_dbg_multiline_code_context( 

152 monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture 

153) -> None: 

154 # Create a fake stack with two frames; the first frame does not contain "dbg", 

155 # but the second does. 

156 class FakeFrame: 

157 def __init__( 

158 self, code_context: Optional[List[str]], filename: str, lineno: int 

159 ) -> None: 

160 self.code_context = code_context 

161 self.filename = filename 

162 self.lineno = lineno 

163 

164 def fake_inspect_stack() -> List[Any]: 

165 return [ 

166 FakeFrame(["not line"], "frame1.py", 20), 

167 FakeFrame(["dbg(2+2)", "ignored line"], "frame2.py", 30), 

168 ] 

169 

170 monkeypatch.setattr(inspect, "stack", fake_inspect_stack) 

171 result: int = dbg(2 + 2) 

172 captured: str = capsys.readouterr().err 

173 print(captured) 

174 assert "2+2" in captured 

175 assert "4" in captured 

176 assert result == 4 

177 

178 

179def test_dbg_counter_increments(capsys: pytest.CaptureFixture) -> None: 

180 global _COUNTER 

181 _COUNTER = 0 

182 dbg() 

183 out1: str = capsys.readouterr().err 

184 dbg() 

185 out2: str = capsys.readouterr().err 

186 assert "<dbg 0>" in out1 

187 assert "<dbg 1>" in out2 

188 

189 

190def test_dbg_formatter_exception() -> None: 

191 def bad_formatter(x: Any) -> str: 

192 raise ValueError("formatter error") 

193 

194 with pytest.raises(ValueError, match="formatter error"): 

195 dbg(123, formatter=bad_formatter) 

196 

197 

198def test_dbg_incomplete_expression( 

199 monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture 

200) -> None: 

201 # Simulate a frame with an incomplete expression (no closing parenthesis) 

202 class FakeFrame: 

203 def __init__( 

204 self, code_context: Optional[List[str]], filename: str, lineno: int 

205 ) -> None: 

206 self.code_context = code_context 

207 self.filename = filename 

208 self.lineno = lineno 

209 

210 def fake_inspect_stack() -> List[Any]: 

211 return [FakeFrame(["dbg(42"], "fake_incomplete.py", 100)] 

212 

213 monkeypatch.setattr(inspect, "stack", fake_inspect_stack) 

214 result: int = dbg(42) 

215 captured: str = capsys.readouterr().err 

216 # The extracted expression should be "42" (since there's no closing parenthesis) 

217 assert "42" in captured 

218 assert result == 42 

219 

220 

221def test_dbg_non_callable_formatter() -> None: 

222 with pytest.raises(TypeError): 

223 dbg(42, formatter="not callable") # type: ignore 

224 

225 

226def test_misc() -> None: 

227 d1 = {"apple": 1, "banana": 2, "cherry": 3} 

228 dbg_dict(d1) 

229 dbg_auto(d1) 

230 l1 = [10, 20, 30] 

231 dbg_auto(l1) 

232 

233 

234# # --- Tests for tensor_info_dict and tensor_info --- 

235# def test_tensor_info_dict_with_nan() -> None: 

236# tensor: DummyTensor = DummyTensor() 

237# info: Dict[str, str] = tensor_info_dict(tensor) 

238# expected: Dict[str, str] = { 

239# "shape": repr((2, 3)), 

240# "sum": repr(float("nan")), 

241# "dtype": repr("float32"), 

242# "device": repr("cpu"), 

243# "requires_grad": repr(False), 

244# } 

245# assert info == expected 

246 

247 

248# def test_tensor_info_dict_normal() -> None: 

249# tensor: DummyTensorNormal = DummyTensorNormal() 

250# info: Dict[str, str] = tensor_info_dict(tensor) 

251# expected: Dict[str, str] = { 

252# "shape": repr((4, 5)), 

253# "dtype": repr("int32"), 

254# "device": repr("cuda"), 

255# "requires_grad": repr(True), 

256# } 

257# assert info == expected 

258 

259 

260# def test_tensor_info_dict_partial() -> None: 

261# tensor: DummyTensorPartial = DummyTensorPartial() 

262# info: Dict[str, str] = tensor_info_dict(tensor) 

263# expected: Dict[str, str] = {"shape": repr((7,))} 

264# assert info == expected 

265 

266 

267# def test_tensor_info() -> None: 

268# tensor: DummyTensorNormal = DummyTensorNormal() 

269# info_str: str = tensor_info(tensor) 

270# expected: str = ", ".join( 

271# [ 

272# f"shape={repr((4, 5))}", 

273# f"dtype={repr('int32')}", 

274# f"device={repr('cuda')}", 

275# f"requires_grad={repr(True)}", 

276# ] 

277# ) 

278# assert info_str == expected 

279 

280 

281# def test_tensor_info_dict_no_attributes() -> None: 

282# class DummyEmpty: 

283# pass 

284 

285# dummy = DummyEmpty() 

286# info: Dict[str, str] = tensor_info_dict(dummy) 

287# assert info == {} 

288 

289 

290# def test_tensor_info_no_attributes() -> None: 

291# class DummyEmpty: 

292# pass 

293 

294# dummy = DummyEmpty() 

295# info_str: str = tensor_info(dummy) 

296# assert info_str == "" 

297 

298 

299# def test_dbg_tensor(capsys: pytest.CaptureFixture) -> None: 

300# tensor: DummyTensorPartial = DummyTensorPartial() 

301# result: DummyTensorPartial = dbg_tensor(tensor) # type: ignore 

302# captured: str = capsys.readouterr().err 

303# assert "shape=(7,)" in captured 

304# assert result is tensor 

305 

306 

307# ============================================================================ 

308# Tests for grep_repr functionality 

309# ============================================================================ 

310 

311 

312def test_normalize_for_loose() -> None: 

313 assert _normalize_for_loose("hello_world") == "hello world" 

314 assert _normalize_for_loose("doThing") == "doThing" # camelCase preserved 

315 assert _normalize_for_loose("DO-THING") == "DO THING" 

316 assert _normalize_for_loose("a__b__c") == "a b c" 

317 assert _normalize_for_loose("test.method()") == "test method" 

318 assert _normalize_for_loose(" spaces ") == "spaces" 

319 

320 

321def test_compile_pattern_case_sensitivity() -> None: 

322 # String patterns default to case-insensitive 

323 pattern = _compile_pattern("hello") 

324 assert pattern.match("HELLO") is not None 

325 assert pattern.match("Hello") is not None 

326 

327 # With cased=True, string patterns are case-sensitive 

328 pattern_cased = _compile_pattern("hello", cased=True) 

329 assert pattern_cased.match("HELLO") is None 

330 assert pattern_cased.match("hello") is not None 

331 

332 

333def test_compile_pattern_loose() -> None: 

334 pattern = _compile_pattern("hello world", loose=True) 

335 # Pattern should be normalized 

336 assert pattern.pattern == "hello world" 

337 

338 

339def test_grep_repr_basic_match(capsys: pytest.CaptureFixture) -> None: 

340 test_list = [1, 2, 42, 3, 4] 

341 grep_repr(test_list, "42") 

342 captured = capsys.readouterr().out 

343 assert "42" in captured 

344 assert captured.count("42") >= 1 

345 

346 

347def test_grep_repr_no_match(capsys: pytest.CaptureFixture) -> None: 

348 test_list = [1, 2, 3, 4] 

349 grep_repr(test_list, "999") 

350 captured = capsys.readouterr().out 

351 assert captured.strip() == "" 

352 

353 

354def test_grep_repr_case_insensitive_default(capsys: pytest.CaptureFixture) -> None: 

355 test_dict = {"Hello": "World"} 

356 grep_repr(test_dict, "hello") # Should match "Hello" by default 

357 captured = capsys.readouterr().out 

358 assert "Hello" in captured 

359 

360 

361def test_grep_repr_case_sensitive(capsys: pytest.CaptureFixture) -> None: 

362 test_dict = {"Hello": "World"} 

363 grep_repr(test_dict, "hello", cased=True) # Should NOT match 

364 captured = capsys.readouterr().out 

365 assert captured.strip() == "" 

366 

367 

368def test_grep_repr_loose_matching(capsys: pytest.CaptureFixture) -> None: 

369 class TestObj: 

370 def __repr__(self) -> str: 

371 return "method_name(arg_value)" 

372 

373 obj = TestObj() 

374 grep_repr(obj, "method name", loose=True) 

375 captured = capsys.readouterr().out 

376 # With loose=True, both pattern and text are normalized, so we should see "method name" highlighted 

377 assert "method name" in captured 

378 

379 

380def test_grep_repr_char_context(capsys: pytest.CaptureFixture) -> None: 

381 test_string = "prefix_42_suffix" 

382 grep_repr(test_string, "42", char_context=3) 

383 captured = capsys.readouterr().out 

384 # Should show 3 chars before and after: "ix_42_su" 

385 assert "ix_" in captured and "_su" in captured 

386 

387 

388def test_grep_repr_char_context_zero(capsys: pytest.CaptureFixture) -> None: 

389 test_string = "prefix_42_suffix" 

390 grep_repr(test_string, "42", char_context=0) 

391 captured = capsys.readouterr().out 

392 # Should only show the match itself 

393 lines = captured.strip().split("\n") 

394 assert len(lines) == 1 

395 assert "42" in lines[0] 

396 

397 

398def test_grep_repr_line_context(capsys: pytest.CaptureFixture) -> None: 

399 test_obj = {"line1": 1, "line2": 2, "target": 42, "line4": 4} 

400 grep_repr(test_obj, "42", line_context=1) 

401 captured = capsys.readouterr().out 

402 assert "42" in captured 

403 

404 

405def test_grep_repr_before_after_context(capsys: pytest.CaptureFixture) -> None: 

406 class MultilineRepr: 

407 def __repr__(self) -> str: 

408 return "line1\nline2\ntarget_line\nline4\nline5" 

409 

410 multiline_obj = MultilineRepr() 

411 grep_repr(multiline_obj, "target", before_context=1, after_context=1) 

412 captured = capsys.readouterr().out 

413 assert "line2" in captured 

414 assert "target" in captured 

415 assert "line4" in captured 

416 

417 

418def test_grep_repr_context_shortcut(capsys: pytest.CaptureFixture) -> None: 

419 class MultilineRepr: 

420 def __repr__(self) -> str: 

421 return "line1\nline2\ntarget_line\nline4\nline5" 

422 

423 multiline_obj = MultilineRepr() 

424 grep_repr(multiline_obj, "target", context=1) 

425 captured = capsys.readouterr().out 

426 assert "line2" in captured 

427 assert "target" in captured 

428 assert "line4" in captured 

429 

430 

431def test_grep_repr_max_count(capsys: pytest.CaptureFixture) -> None: 

432 test_list = [42, 42, 42, 42, 42] # 5 matches in repr 

433 grep_repr( 

434 test_list, "42", max_count=2, char_context=0 

435 ) # No context to avoid duplicates 

436 captured = capsys.readouterr().out 

437 # Should only show 2 match blocks due to max_count=2 

438 lines = [line for line in captured.strip().split("\n") if line and line != "--"] 

439 assert len(lines) == 2 

440 

441 

442def test_grep_repr_line_numbers(capsys: pytest.CaptureFixture) -> None: 

443 # Create an object whose repr contains actual newlines 

444 class MultilineRepr: 

445 def __repr__(self) -> str: 

446 return "line1\nline2\ntarget_line\nline4" 

447 

448 multiline_obj = MultilineRepr() 

449 grep_repr(multiline_obj, "target", line_context=1, line_numbers=True) 

450 captured = capsys.readouterr().out 

451 assert "3:" in captured # Line number for target line 

452 assert "2:" in captured # Line number for context 

453 assert "4:" in captured # Line number for context 

454 

455 

456def test_grep_repr_no_highlight(capsys: pytest.CaptureFixture) -> None: 

457 test_list = [1, 2, 42, 3] 

458 grep_repr(test_list, "42", highlight=False) 

459 captured = capsys.readouterr().out 

460 # Should not contain ANSI escape sequences 

461 assert "\033[" not in captured 

462 assert "42" in captured 

463 

464 

465def test_grep_repr_custom_color(capsys: pytest.CaptureFixture) -> None: 

466 test_list = [1, 2, 42, 3] 

467 grep_repr(test_list, "42", color="32") # Green instead of red 

468 captured = capsys.readouterr().out 

469 assert "\033[1;32m" in captured # Green color code 

470 

471 

472def test_grep_repr_custom_separator(capsys: pytest.CaptureFixture) -> None: 

473 test_list = [42, 99, 42] # Multiple matches 

474 grep_repr(test_list, r"\d+", separator="---") 

475 captured = capsys.readouterr().out 

476 assert "---" in captured 

477 

478 

479def test_grep_repr_quiet_mode() -> None: 

480 test_list = [1, 2, 42, 3] 

481 result = grep_repr(test_list, "42", quiet=True) 

482 assert result is not None 

483 assert isinstance(result, list) 

484 assert len(result) >= 1 

485 assert any("42" in line for line in result) 

486 

487 

488def test_grep_repr_multiple_matches(capsys: pytest.CaptureFixture) -> None: 

489 test_dict = {"key1": 42, "key2": 24, "key3": 42} 

490 grep_repr(test_dict, "42") 

491 captured = capsys.readouterr().out 

492 # Should show multiple matches 

493 assert captured.count("42") >= 2 

494 

495 

496def test_grep_repr_regex_pattern(capsys: pytest.CaptureFixture) -> None: 

497 test_list = [1, 22, 333, 4444] 

498 grep_repr(test_list, r"\d{3}") # Exactly 3 digits 

499 captured = capsys.readouterr().out 

500 assert "333" in captured 

501 # Note: "4444" contains "444" which matches \d{3}, so it will be highlighted 

502 assert "444" in captured 

503 # The whole list repr is shown, so "22" will be in the output but not highlighted 

504 # Check that 333 and 444 are highlighted (contain ANSI codes) but 22 is not 

505 assert "\033[1;31m333\033[0m" in captured 

506 assert "\033[1;31m444\033[0m" in captured 

507 

508 

509def test_grep_repr_compiled_regex(capsys: pytest.CaptureFixture) -> None: 

510 import re 

511 

512 test_string = "Hello World" 

513 pattern = re.compile(r"hello", re.IGNORECASE) 

514 grep_repr(test_string, pattern) 

515 captured = capsys.readouterr().out 

516 assert "Hello" in captured 

517 

518 

519def test_grep_repr_empty_pattern(capsys: pytest.CaptureFixture) -> None: 

520 test_list = [1, 2, 3] 

521 # Empty pattern matches everything, should not raise an error but show all content 

522 grep_repr(test_list, "", char_context=0) 

523 captured = capsys.readouterr().out 

524 # Empty pattern should match at every position, but with char_context=0 should show minimal output 

525 assert len(captured.strip()) > 0 

526 

527 

528def test_grep_repr_invalid_regex() -> None: 

529 test_list = [1, 2, 3] 

530 with pytest.raises(re.error): 

531 grep_repr(test_list, "[invalid") 

532 

533 

534def test_grep_repr_large_object() -> None: 

535 large_dict = {f"key_{i}": i for i in range(1000)} 

536 large_dict["special_key"] = 42 

537 

538 # Should handle large objects without issues 

539 result = grep_repr(large_dict, "special_key", quiet=True) 

540 assert result is not None 

541 assert any("special_key" in line for line in result) 

542 

543 

544def test_grep_repr_nested_objects(capsys: pytest.CaptureFixture) -> None: 

545 nested = {"outer": {"inner": {"value": 42}}} 

546 grep_repr(nested, "42") 

547 captured = capsys.readouterr().out 

548 assert "42" in captured 

549 

550 

551def test_grep_repr_custom_objects(capsys: pytest.CaptureFixture) -> None: 

552 class CustomObject: 

553 def __repr__(self) -> str: 

554 return "CustomObject(special_value=123)" 

555 

556 obj = CustomObject() 

557 grep_repr(obj, "special_value") 

558 captured = capsys.readouterr().out 

559 assert "special_value" in captured