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

287 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-28 17:24 +0000

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, # noqa: F401 

17 grep_repr, 

18 _normalize_for_loose, 

19 _compile_pattern, 

20) 

21 

22 

23DBG_MODULE_NAME: str = "muutils.dbg" 

24 

25# ============================================================================ 

26# Dummy Tensor classes for testing tensor_info* functions 

27# ============================================================================ 

28 

29 

30class DummyTensor: 

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

32 

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

34 dtype: str = "float32" 

35 device: str = "cpu" 

36 requires_grad: bool = False 

37 

38 def sum(self) -> float: 

39 return float("nan") 

40 

41 

42class DummyTensorNormal: 

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

44 

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

46 dtype: str = "int32" 

47 device: str = "cuda" 

48 requires_grad: bool = True 

49 

50 def sum(self) -> float: 

51 return 20.0 

52 

53 

54class DummyTensorPartial: 

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

56 

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

58 

59 

60# ============================================================================ 

61# Additional Tests for dbg and tensor_info functionality 

62# ============================================================================ 

63 

64 

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

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

67 monkeypatch.setattr( 

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

69 ) 

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

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

72 result: str = _process_path(test_path) 

73 assert result == expected 

74 

75 

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

77 monkeypatch.setattr( 

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

79 ) 

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

81 expected: str = "file.txt" 

82 result: str = _process_path(test_path) 

83 assert result == expected 

84 

85 

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

87 monkeypatch.setattr( 

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

89 ) 

90 with tempfile.TemporaryDirectory() as tmp_dir: 

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

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

93 result: str = _process_path(test_path) 

94 assert result == expected 

95 

96 

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

98 monkeypatch.setattr( 

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

100 ) 

101 with pytest.raises( 

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

103 ): 

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

105 

106 

107# --- Tests for dbg --- 

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

109 result: int = dbg(1 + 2) 

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

111 assert "= 3" in captured 

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

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

114 assert result == 3 

115 

116 

117def test_dbg_without_expression( 

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

119) -> None: 

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

121 result: Any = dbg() 

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

123 assert "<dbg 0>" in captured 

124 no_exp_passed: Any = _NoExpPassed 

125 assert result is no_exp_passed 

126 

127 

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

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

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

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

132 assert "custom" in captured 

133 assert result == "anything" 

134 

135 

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

137 # Test a complex expression (lambda invocation) 

138 result: int = dbg((lambda x: x * x)(5)) 

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

140 assert ( 

141 "lambda" in captured 

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

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

144 assert result == 25 

145 

146 

147def test_dbg_multiline_code_context( 

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

149) -> None: 

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

151 # but the second does. 

152 class FakeFrame: 

153 def __init__( 

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

155 ) -> None: 

156 self.code_context = code_context 

157 self.filename = filename 

158 self.lineno = lineno 

159 

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

161 return [ 

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

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

164 ] 

165 

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

167 result: int = dbg(2 + 2) 

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

169 print(captured) 

170 assert "2+2" in captured 

171 assert "4" in captured 

172 assert result == 4 

173 

174 

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

176 global _COUNTER 

177 _COUNTER = 0 

178 dbg() 

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

180 dbg() 

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

182 assert "<dbg 0>" in out1 

183 assert "<dbg 1>" in out2 

184 

185 

186def test_dbg_formatter_exception() -> None: 

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

188 raise ValueError("formatter error") 

189 

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

191 dbg(123, formatter=bad_formatter) 

192 

193 

194def test_dbg_incomplete_expression( 

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

196) -> None: 

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

198 class FakeFrame: 

199 def __init__( 

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

201 ) -> None: 

202 self.code_context = code_context 

203 self.filename = filename 

204 self.lineno = lineno 

205 

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

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

208 

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

210 result: int = dbg(42) 

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

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

213 assert "42" in captured 

214 assert result == 42 

215 

216 

217def test_dbg_non_callable_formatter() -> None: 

218 with pytest.raises(TypeError): 

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

220 

221 

222# # --- Tests for tensor_info_dict and tensor_info --- 

223# def test_tensor_info_dict_with_nan() -> None: 

224# tensor: DummyTensor = DummyTensor() 

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

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

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

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

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

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

231# "requires_grad": repr(False), 

232# } 

233# assert info == expected 

234 

235 

236# def test_tensor_info_dict_normal() -> None: 

237# tensor: DummyTensorNormal = DummyTensorNormal() 

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

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

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

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

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

243# "requires_grad": repr(True), 

244# } 

245# assert info == expected 

246 

247 

248# def test_tensor_info_dict_partial() -> None: 

249# tensor: DummyTensorPartial = DummyTensorPartial() 

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

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

252# assert info == expected 

253 

254 

255# def test_tensor_info() -> None: 

256# tensor: DummyTensorNormal = DummyTensorNormal() 

257# info_str: str = tensor_info(tensor) 

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

259# [ 

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

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

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

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

264# ] 

265# ) 

266# assert info_str == expected 

267 

268 

269# def test_tensor_info_dict_no_attributes() -> None: 

270# class DummyEmpty: 

271# pass 

272 

273# dummy = DummyEmpty() 

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

275# assert info == {} 

276 

277 

278# def test_tensor_info_no_attributes() -> None: 

279# class DummyEmpty: 

280# pass 

281 

282# dummy = DummyEmpty() 

283# info_str: str = tensor_info(dummy) 

284# assert info_str == "" 

285 

286 

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

288# tensor: DummyTensorPartial = DummyTensorPartial() 

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

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

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

292# assert result is tensor 

293 

294 

295# ============================================================================ 

296# Tests for grep_repr functionality 

297# ============================================================================ 

298 

299 

300def test_normalize_for_loose() -> None: 

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

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

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

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

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

306 assert _normalize_for_loose(" spaces ") == "spaces" 

307 

308 

309def test_compile_pattern_case_sensitivity() -> None: 

310 # String patterns default to case-insensitive 

311 pattern = _compile_pattern("hello") 

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

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

314 

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

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

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

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

319 

320 

321def test_compile_pattern_loose() -> None: 

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

323 # Pattern should be normalized 

324 assert pattern.pattern == "hello world" 

325 

326 

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

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

329 grep_repr(test_list, "42") 

330 captured = capsys.readouterr().out 

331 assert "42" in captured 

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

333 

334 

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

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

337 grep_repr(test_list, "999") 

338 captured = capsys.readouterr().out 

339 assert captured.strip() == "" 

340 

341 

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

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

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

345 captured = capsys.readouterr().out 

346 assert "Hello" in captured 

347 

348 

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

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

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

352 captured = capsys.readouterr().out 

353 assert captured.strip() == "" 

354 

355 

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

357 class TestObj: 

358 def __repr__(self) -> str: 

359 return "method_name(arg_value)" 

360 

361 obj = TestObj() 

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

363 captured = capsys.readouterr().out 

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

365 assert "method name" in captured 

366 

367 

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

369 test_string = "prefix_42_suffix" 

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

371 captured = capsys.readouterr().out 

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

373 assert "ix_" in captured and "_su" in captured 

374 

375 

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

377 test_string = "prefix_42_suffix" 

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

379 captured = capsys.readouterr().out 

380 # Should only show the match itself 

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

382 assert len(lines) == 1 

383 assert "42" in lines[0] 

384 

385 

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

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

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

389 captured = capsys.readouterr().out 

390 assert "42" in captured 

391 

392 

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

394 class MultilineRepr: 

395 def __repr__(self) -> str: 

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

397 

398 multiline_obj = MultilineRepr() 

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

400 captured = capsys.readouterr().out 

401 assert "line2" in captured 

402 assert "target" in captured 

403 assert "line4" in captured 

404 

405 

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

407 class MultilineRepr: 

408 def __repr__(self) -> str: 

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

410 

411 multiline_obj = MultilineRepr() 

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

413 captured = capsys.readouterr().out 

414 assert "line2" in captured 

415 assert "target" in captured 

416 assert "line4" in captured 

417 

418 

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

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

421 grep_repr( 

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

423 ) # No context to avoid duplicates 

424 captured = capsys.readouterr().out 

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

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

427 assert len(lines) == 2 

428 

429 

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

431 # Create an object whose repr contains actual newlines 

432 class MultilineRepr: 

433 def __repr__(self) -> str: 

434 return "line1\nline2\ntarget_line\nline4" 

435 

436 multiline_obj = MultilineRepr() 

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

438 captured = capsys.readouterr().out 

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

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

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

442 

443 

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

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

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

447 captured = capsys.readouterr().out 

448 # Should not contain ANSI escape sequences 

449 assert "\033[" not in captured 

450 assert "42" in captured 

451 

452 

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

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

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

456 captured = capsys.readouterr().out 

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

458 

459 

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

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

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

463 captured = capsys.readouterr().out 

464 assert "---" in captured 

465 

466 

467def test_grep_repr_quiet_mode() -> None: 

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

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

470 assert result is not None 

471 assert isinstance(result, list) 

472 assert len(result) >= 1 

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

474 

475 

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

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

478 grep_repr(test_dict, "42") 

479 captured = capsys.readouterr().out 

480 # Should show multiple matches 

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

482 

483 

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

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

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

487 captured = capsys.readouterr().out 

488 assert "333" in captured 

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

490 assert "444" in captured 

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

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

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

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

495 

496 

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

498 import re 

499 

500 test_string = "Hello World" 

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

502 grep_repr(test_string, pattern) 

503 captured = capsys.readouterr().out 

504 assert "Hello" in captured 

505 

506 

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

508 test_list = [1, 2, 3] 

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

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

511 captured = capsys.readouterr().out 

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

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

514 

515 

516def test_grep_repr_invalid_regex() -> None: 

517 test_list = [1, 2, 3] 

518 with pytest.raises(re.error): 

519 grep_repr(test_list, "[invalid") 

520 

521 

522def test_grep_repr_large_object() -> None: 

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

524 large_dict["special_key"] = 42 

525 

526 # Should handle large objects without issues 

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

528 assert result is not None 

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

530 

531 

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

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

534 grep_repr(nested, "42") 

535 captured = capsys.readouterr().out 

536 assert "42" in captured 

537 

538 

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

540 class CustomObject: 

541 def __repr__(self) -> str: 

542 return "CustomObject(special_value=123)" 

543 

544 obj = CustomObject() 

545 grep_repr(obj, "special_value") 

546 captured = capsys.readouterr().out 

547 assert "special_value" in captured