Coverage for muutils/collect_warnings.py: 0%
38 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-07-07 20:15 -0700
« prev ^ index » next coverage.py v7.6.1, created at 2025-07-07 20:15 -0700
1from __future__ import annotations
3import sys
4import warnings
5from collections import Counter
6from contextlib import AbstractContextManager
7from types import TracebackType
8from typing import Any, Literal
11class CollateWarnings(AbstractContextManager["CollateWarnings"]):
12 """Capture every warning issued inside a `with` block and print a collated
13 summary when the block exits.
15 Internally this wraps `warnings.catch_warnings(record=True)` so that all
16 warnings raised in the block are recorded. When the context exits, identical
17 warnings are grouped and (optionally) printed with a user-defined format.
19 # Parameters:
20 - `print_on_exit : bool`
21 Whether to print the summary when the context exits
22 (defaults to `True`)
23 - `fmt : str`
24 Format string used for printing each line of the summary.
25 Available fields are:
27 * `{count}` : number of occurrences
28 * `{filename}` : file where the warning originated
29 * `{lineno}` : line number
30 * `{category}` : warning class name
31 * `{message}` : warning message text
33 (defaults to `"({count}x) {filename}:{lineno} {category}: {message}"`)
35 # Returns:
36 - `CollateWarnings`
37 The context-manager instance. After exit, the attribute
38 `counts` holds a mapping
40 ```python
41 {(filename, lineno, category, message): count}
42 ```
44 # Usage:
45 ```python
46 >>> import warnings
47 >>> with CollateWarnings() as cw:
48 ... warnings.warn("deprecated", DeprecationWarning)
49 ... warnings.warn("deprecated", DeprecationWarning)
50 ... warnings.warn("other", UserWarning)
51 (2x) /tmp/example.py:42 DeprecationWarning: deprecated
52 (1x) /tmp/example.py:43 UserWarning: other
53 >>> cw.counts
54 {('/tmp/example.py', 42, 'DeprecationWarning', 'deprecated'): 2,
55 ('/tmp/example.py', 43, 'UserWarning', 'other'): 1}
56 ```
57 """
59 _active: bool
60 _catcher: Any
61 _records: list[warnings.WarningMessage]
62 counts: Counter[
63 tuple[
64 str, # filename
65 int, # lineno
66 str, # category name
67 str, # message
68 ]
69 ]
70 print_on_exit: bool
71 fmt: str
73 def __init__(
74 self,
75 print_on_exit: bool = True,
76 fmt: str = "({count}x) {filename}:{lineno} {category}: {message}",
77 ) -> None:
78 self.print_on_exit = print_on_exit
79 self.fmt = fmt
80 self._active = False
81 self._records = []
82 self.counts = Counter()
84 def __enter__(self) -> CollateWarnings:
85 if self._active:
86 raise RuntimeError("CollateWarnings cannot be re-entered")
88 self._active = True
89 self._catcher = warnings.catch_warnings(record=True)
90 self._records = self._catcher.__enter__()
91 warnings.simplefilter("always") # capture every warning
92 return self
94 def __exit__(
95 self,
96 exc_type: type[BaseException] | None,
97 exc_val: BaseException | None,
98 exc_tb: TracebackType | None,
99 ) -> Literal[False]:
100 if not self._active:
101 raise RuntimeError("CollateWarnings exited twice")
103 self._active = False
104 # stop capturing
105 self._catcher.__exit__(exc_type, exc_val, exc_tb)
107 # collate
108 self.counts = Counter(
109 (
110 rec.filename,
111 rec.lineno,
112 rec.category.__name__,
113 str(rec.message),
114 )
115 for rec in self._records
116 )
118 if self.print_on_exit:
119 for (filename, lineno, category, message), count in self.counts.items():
120 print(
121 self.fmt.format(
122 count=count,
123 filename=filename,
124 lineno=lineno,
125 category=category,
126 message=message,
127 ),
128 file=sys.stderr,
129 )
131 # propagate any exception from the with-block
132 return False