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
« 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
8import pytest
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)
24assert _COUNTER is not None
27DBG_MODULE_NAME: str = "muutils.dbg"
29# ============================================================================
30# Dummy Tensor classes for testing tensor_info* functions
31# ============================================================================
34class DummyTensor:
35 """A dummy tensor whose sum is NaN."""
37 shape: Tuple[int, ...] = (2, 3)
38 dtype: str = "float32"
39 device: str = "cpu"
40 requires_grad: bool = False
42 def sum(self) -> float:
43 return float("nan")
46class DummyTensorNormal:
47 """A dummy tensor with a normal sum."""
49 shape: Tuple[int, ...] = (4, 5)
50 dtype: str = "int32"
51 device: str = "cuda"
52 requires_grad: bool = True
54 def sum(self) -> float:
55 return 20.0
58class DummyTensorPartial:
59 """A dummy tensor with only a shape attribute."""
61 shape: Tuple[int, ...] = (7,)
64# ============================================================================
65# Additional Tests for dbg and tensor_info functionality
66# ============================================================================
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
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
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
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"))
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
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
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"
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
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
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 ]
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
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
190def test_dbg_formatter_exception() -> None:
191 def bad_formatter(x: Any) -> str:
192 raise ValueError("formatter error")
194 with pytest.raises(ValueError, match="formatter error"):
195 dbg(123, formatter=bad_formatter)
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
210 def fake_inspect_stack() -> List[Any]:
211 return [FakeFrame(["dbg(42"], "fake_incomplete.py", 100)]
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
221def test_dbg_non_callable_formatter() -> None:
222 with pytest.raises(TypeError):
223 dbg(42, formatter="not callable") # type: ignore
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)
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
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
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
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
281# def test_tensor_info_dict_no_attributes() -> None:
282# class DummyEmpty:
283# pass
285# dummy = DummyEmpty()
286# info: Dict[str, str] = tensor_info_dict(dummy)
287# assert info == {}
290# def test_tensor_info_no_attributes() -> None:
291# class DummyEmpty:
292# pass
294# dummy = DummyEmpty()
295# info_str: str = tensor_info(dummy)
296# assert info_str == ""
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
307# ============================================================================
308# Tests for grep_repr functionality
309# ============================================================================
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"
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
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
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"
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
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() == ""
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
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() == ""
368def test_grep_repr_loose_matching(capsys: pytest.CaptureFixture) -> None:
369 class TestObj:
370 def __repr__(self) -> str:
371 return "method_name(arg_value)"
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
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
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]
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
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"
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
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"
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
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
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"
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
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
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
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
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)
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
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
509def test_grep_repr_compiled_regex(capsys: pytest.CaptureFixture) -> None:
510 import re
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
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
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")
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
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)
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
551def test_grep_repr_custom_objects(capsys: pytest.CaptureFixture) -> None:
552 class CustomObject:
553 def __repr__(self) -> str:
554 return "CustomObject(special_value=123)"
556 obj = CustomObject()
557 grep_repr(obj, "special_value")
558 captured = capsys.readouterr().out
559 assert "special_value" in captured