Skip to content

Commit d8dfcd9

Browse files
committed
Log failing cleanups as warnings
(See `fable-review-pr431-2026-07-05.md::P13` in #5) line_profiler/_child_process_profiling/_cache_logging.py TIMESTAMP_PATTERN Added formatting field `level` add_timestamp() Added optional argument `level` CacheLoggingEntry .__doc__ Updated instantiations in doctest .level New field (2nd item) .to_text(), .from_text(), .write() Added handling of the `.level` field .new() Added optional argument `level` line_profiler/_child_process_profiling/cache.py::LineProfilingCache _debug_output() - Added optional argument `level` in keeping with the base class - Moved implementation of `._make_debug_entry` (used nowhere else) inside _setup_in_child_process() Updated patching of `CuratedProfilerContext._debug_output` _make_debug_entry Removed line_profiler/cleanup.py::Cleanup _debug_output() Added optional argument `level` for setting the logging level _cleanup() If a callback fails, it is now logged with `Logger.warning()` instead of `Logger.debug()`
1 parent 14e9ee7 commit d8dfcd9

3 files changed

Lines changed: 86 additions & 33 deletions

File tree

line_profiler/_child_process_profiling/_cache_logging.py

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import re
88
from collections.abc import Generator
99
from datetime import datetime
10+
from enum import auto
1011
from itertools import pairwise
1112
from pathlib import Path
1213
from string import Formatter as StringParser
@@ -15,14 +16,13 @@
1516
from typing_extensions import Self
1617

1718
from .. import _diagnostics as diagnostics
18-
from ..line_profiler_utils import block_indent
19+
from ..line_profiler_utils import block_indent, StringEnum
1920

2021

2122
__all__ = ('CacheLoggingEntry',)
2223

23-
2424
FILENAME_PATTERN = 'debug_log_{main_pid}_{current_pid}.log'
25-
TIMESTAMP_PATTERN = '[cache-debug-log {timestamp} DEBUG]'
25+
TIMESTAMP_PATTERN = '[cache-debug-log {timestamp} {level}]'
2626
HEADER_PATTERN = 'PID {current_pid} ({main_pid}): Cache {obj_id:#x}'
2727

2828
TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S'
@@ -34,6 +34,14 @@
3434
HEADER_MAIN_INDICATOR = 'main process'
3535

3636

37+
class LogLevel(StringEnum):
38+
DEBUG = auto()
39+
INFO = auto()
40+
WARNING = auto()
41+
ERROR = auto()
42+
CRITICAL = auto()
43+
44+
3745
def get_logger_header(current_pid: int, main_pid: int, obj_id: int) -> str:
3846
"""
3947
Returns:
@@ -88,7 +96,11 @@ def parse_timestamp(ts: str) -> datetime:
8896
return datetime.strptime(ts, parse_format)
8997

9098

91-
def add_timestamp(msg: str, timestamp: datetime | None = None) -> str:
99+
def add_timestamp(
100+
msg: str,
101+
timestamp: datetime | None = None,
102+
level: str | LogLevel = LogLevel.DEBUG,
103+
) -> str:
92104
"""
93105
Returns:
94106
msg_with_timestamp (str):
@@ -99,6 +111,7 @@ def add_timestamp(msg: str, timestamp: datetime | None = None) -> str:
99111
timestamp = datetime.now()
100112
ts_formatted = TIMESTAMP_PATTERN.format(
101113
timestamp=format_timestamp(timestamp),
114+
level=str(level).upper(),
102115
)
103116
return block_indent(msg, ts_formatted + TIMESTAMP_SPACING)
104117

@@ -205,6 +218,7 @@ class CacheLoggingEntry(NamedTuple):
205218
>>>
206219
>>> entry = CacheLoggingEntry(
207220
... datetime(1900, 1, 1, 0, 0, 0, 0),
221+
... LogLevel.DEBUG,
208222
... 12345,
209223
... 12345,
210224
... 12345678,
@@ -217,13 +231,14 @@ class CacheLoggingEntry(NamedTuple):
217231
multiple lines
218232
>>> another_entry = CacheLoggingEntry(
219233
... datetime(2000, 12, 31, 12, 34, 56, 789000),
234+
... LogLevel.INFO,
220235
... 12345,
221236
... 54321,
222237
... 87654321,
223238
... 'FOO BAR BAZ',
224239
... )
225240
>>> print(another_entry.to_text())
226-
[cache-debug-log 2000-12-31 12:34:56,789 DEBUG] PID 54321 \
241+
[cache-debug-log 2000-12-31 12:34:56,789 INFO] PID 54321 \
227242
(12345): Cache 0x5397fb1: FOO BAR BAZ
228243
>>> log_text = '\\n'.join([
229244
... e.to_text() for e in [entry, another_entry]
@@ -233,30 +248,46 @@ class CacheLoggingEntry(NamedTuple):
233248
... ]
234249
"""
235250
timestamp: datetime
251+
level: LogLevel
236252
main_pid: int
237253
current_pid: int
238254
cache_id: int
239255
msg: str
240256

241257
def to_text(self) -> str:
242-
return add_timestamp(self._get_header() + self.msg, self.timestamp)
258+
return add_timestamp(
259+
self._get_header() + self.msg, self.timestamp, self.level,
260+
)
243261

244262
def _get_header(self) -> str:
245263
return get_logger_header(
246264
self.current_pid, self.main_pid, self.cache_id,
247265
) + HEADER_SEP
248266

249267
def write(self, tee: os.PathLike[str] | str | None = None) -> None:
268+
"""
269+
Write the log message using
270+
:py:mod:`line_profiler._diagnostics.log`. If ``tee`` is a path,
271+
also tee thereto with an appropriate timestamp.
272+
"""
250273
log_msg = self._get_header() + self.msg
251-
diagnostics.log.debug(log_msg)
274+
log_func = getattr(diagnostics.log, self.level.lower())
275+
log_func(log_msg)
252276
if tee is None:
253277
return
254278
with Path(tee).open(mode='a') as fobj:
255-
print(add_timestamp(log_msg, self.timestamp), file=fobj)
279+
full_msg = add_timestamp(log_msg, self.timestamp, self.level)
280+
print(full_msg, file=fobj)
256281

257282
@classmethod
258-
def new(cls, main_pid: int, cache_id: int, msg: str) -> Self:
259-
return cls(datetime.now(), main_pid, os.getpid(), cache_id, msg)
283+
def new(
284+
cls, main_pid: int, cache_id: int, msg: str,
285+
level: str | LogLevel = LogLevel.DEBUG,
286+
) -> Self:
287+
return cls(
288+
datetime.now(), LogLevel(level), main_pid,
289+
os.getpid(), cache_id, msg,
290+
)
260291

261292
@classmethod
262293
def from_file(cls, file: os.PathLike[str] | str | TextIO) -> list[Self]:
@@ -286,7 +317,7 @@ def gen_timestamps(text: str) -> Generator[re.Match, None, None]:
286317
return
287318

288319
def gen_message_blocks(text: str) -> Generator[
289-
tuple[datetime, re.Match, str], None, None
320+
tuple[datetime, LogLevel, re.Match, str], None, None
290321
]:
291322
timestamps = list(gen_timestamps(text))
292323
if not timestamps:
@@ -295,18 +326,22 @@ def gen_message_blocks(text: str) -> Generator[
295326
# Handle all the entries up till the 2nd-to-last one
296327
for this_match, next_match in pairwise(timestamps):
297328
ts = parse_timestamp(this_match.group('timestamp'))
329+
level = LogLevel(this_match.group('level'))
298330
text_block = text[this_match.start():next_match.start()]
299-
yield (ts, this_match, text_block.rstrip('\n'))
331+
yield (ts, level, this_match, text_block.rstrip('\n'))
300332
# Handle the last entry
301333
last_match = timestamps[-1]
302334
yield (
303335
parse_timestamp(last_match.group('timestamp')),
336+
LogLevel(last_match.group('level')),
304337
last_match,
305338
text[last_match.start():].rstrip('\n'),
306339
)
307340

308341
def get_entries(text: str) -> Generator[Self, None, None]:
309-
for timestamp, ts_match, text_block in gen_message_blocks(text):
342+
for (
343+
timestamp, level, ts_match, text_block,
344+
) in gen_message_blocks(text):
310345
# Strip the block indent
311346
ts_text = ts_match.group(0)
312347
assert text_block.startswith(ts_text), (
@@ -326,10 +361,14 @@ def get_entries(text: str) -> Generator[Self, None, None]:
326361
cache_id = parse_id(header_match.group('obj_id'))
327362
# The rest of the block is the message proper
328363
msg = text_block[header_match.end():]
329-
yield cls(timestamp, main_pid, current_pid, cache_id, msg)
364+
yield cls(
365+
timestamp, level, main_pid, current_pid, cache_id, msg,
366+
)
330367

331368
timestamp_pattern = fmt_to_regex(
332-
f'{TIMESTAMP_PATTERN}{TIMESTAMP_SPACING}', timestamp='.+?',
369+
f'{TIMESTAMP_PATTERN}{TIMESTAMP_SPACING}',
370+
timestamp='.+?',
371+
level='({})'.format('|'.join(LogLevel.__members__)),
333372
)
334373
timestamp_regex = re.compile('^' + timestamp_pattern, re.MULTILINE)
335374
header_regex = re.compile(fmt_to_regex(

line_profiler/_child_process_profiling/cache.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
from _line_profiler_hooks import INHERITED_PID_ENV_VARNAME, load_pth_hook
2828
from .. import _diagnostics as diagnostics
29-
from ..cleanup import Cleanup, _CALLBACK_REPR_HELPER
29+
from ..cleanup import Cleanup, LogLevel, _CALLBACK_REPR_HELPER
3030
from ..curated_profiling import CuratedProfilerContext
3131
from ..line_profiler import LineProfiler, LineStats
3232
from ..toml_config import ConfigSource
@@ -371,13 +371,14 @@ def get_pth_config() -> Mapping[str, Any]:
371371

372372
return fpath
373373

374-
def _debug_output(self, msg: str) -> None:
374+
def _debug_output(self, msg: str, /, level: LogLevel = 'debug') -> None:
375375
"""
376376
Beside writing to the logger, also write to the
377377
:py:attr:`~._debug_log`.
378378
"""
379+
entry = CacheLoggingEntry.new(self.main_pid, id(self), msg, level)
379380
try:
380-
self._make_debug_entry(msg).write(self._debug_log)
381+
entry.write(self._debug_log)
381382
except OSError: # Cache dir may have been rm-ed during cleanup
382383
pass
383384

@@ -446,9 +447,10 @@ def _setup_in_child_process(
446447
calling this function, true otherwise
447448
"""
448449
def wrap_ctx_debug(
449-
ctx: CuratedProfilerContext, msg: str,
450+
ctx: CuratedProfilerContext, msg: str, /,
451+
level: LogLevel = 'debug',
450452
) -> None:
451-
self._debug_output(f' Context {id(ctx):#x}: {msg}')
453+
self._debug_output(f' Context {id(ctx):#x}: {msg}', level)
452454

453455
if not context:
454456
context = '...'
@@ -820,10 +822,6 @@ def _debug_log(self) -> Path | None:
820822
)
821823
return Path(self.cache_dir) / fname
822824

823-
@cached_property
824-
def _make_debug_entry(self) -> Callable[[str], CacheLoggingEntry]:
825-
return partial(CacheLoggingEntry.new, self.main_pid, id(self))
826-
827825
@cached_property
828826
def _consistent_with_loaded_instance(self) -> bool:
829827
cls = type(self)

line_profiler/cleanup.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from inspect import getattr_static
1111
from operator import setitem
1212
from pathlib import Path
13-
from typing import Any, TypeVar, cast
13+
from typing import Any, Literal, Protocol, TypeVar, cast, overload
1414
from typing_extensions import Concatenate, ParamSpec, Self
1515

1616
from .line_profiler_utils import CallbackRepr, make_tempfile
@@ -24,12 +24,26 @@
2424
V = TypeVar('V')
2525
_Stacks = dict[float, list[Callable[[], Any]]]
2626
_StackContexts = list[_Stacks]
27+
LogLevel = Literal['debug', 'info', 'warning', 'error', 'critical']
2728

2829

2930
_CALLBACK_REPR_HELPER = CallbackRepr(maxother=cast(int, float('inf')))
3031
_CALLBACK_REPR = _CALLBACK_REPR_HELPER.repr
3132

3233

34+
class _LoggingCallback(Protocol):
35+
@overload
36+
def __call__(self, msg: str, /) -> Any:
37+
...
38+
39+
@overload
40+
def __call__(self, msg: str, /, level: LogLevel) -> Any:
41+
...
42+
43+
def __call__(self, *_, **__):
44+
...
45+
46+
3347
class Cleanup:
3448
"""
3549
Object which holds cleanup callbacks. Also provides convenience
@@ -124,7 +138,7 @@ def pop_n_levels_of_contexts(
124138

125139
@staticmethod
126140
def _cleanup(
127-
log: Callable[[str], Any], stacks: _Stacks, reason: str | None,
141+
log: _LoggingCallback, stacks: _Stacks, reason: str | None,
128142
) -> None:
129143
ncallbacks_total = sum(len(stack) for stack in stacks.values())
130144
note = f'{ncallbacks_total} callback(s)'
@@ -146,14 +160,15 @@ def _cleanup(
146160
try:
147161
callback()
148162
except Exception as e:
149-
state = 'failed'
163+
success, state = False, 'failed'
150164
msg = f'{callback_repr}: {type(e).__name__}: {e}'
151165
else:
152-
state, msg = 'succeeded', f'{callback_repr}'
153-
log(
166+
success, state, msg = True, 'succeeded', f'{callback_repr}'
167+
msg = (
154168
f'- Cleanup {state} '
155-
f'({ncallbacks_run}/{ncallbacks_total}): {msg}',
169+
f'({ncallbacks_run}/{ncallbacks_total}): {msg}'
156170
)
171+
log(msg, 'debug' if success else 'warning')
157172
log(f'... cleanup completed ({note})')
158173

159174
def add_cleanup(
@@ -399,15 +414,16 @@ def _get_name(obj: Any, /) -> str:
399414
name = f'{obj.__module__}.{name}'
400415
return str(name)
401416

402-
def _debug_output(self, msg: str, /) -> None:
417+
def _debug_output(self, msg: str, /, level: LogLevel = 'debug') -> None:
403418
"""
404419
Write debugging output.
405420
406421
Note:
407422
This default implementation just writes to the logger at the
408-
``DEBUG`` level.
423+
specified level.
409424
"""
410-
diagnostics.log.debug(msg)
425+
log_func = getattr(diagnostics.log, level)
426+
log_func(msg)
411427

412428
@property
413429
def _current_context(self) -> _Stacks:

0 commit comments

Comments
 (0)