pyplotutil 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyplotutil/__init__.py +70 -0
- pyplotutil/_typing.py +38 -0
- pyplotutil/datautil.py +1295 -0
- pyplotutil/loggingutil.py +743 -0
- pyplotutil/plotutil.py +846 -0
- pyplotutil/py.typed +0 -0
- pyplotutil-2.0.0.dist-info/METADATA +123 -0
- pyplotutil-2.0.0.dist-info/RECORD +10 -0
- pyplotutil-2.0.0.dist-info/WHEEL +4 -0
- pyplotutil-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
"""Event logging utility for streamlined log management.
|
|
2
|
+
|
|
3
|
+
This module provides a robust logging system that combines file and console logging with
|
|
4
|
+
configurable verbosity levels. It features:
|
|
5
|
+
|
|
6
|
+
- Unified interface for file and console logging
|
|
7
|
+
- FakeLogger implementation for testing scenarios
|
|
8
|
+
- Automatic log file management
|
|
9
|
+
- Verbosity control through command line arguments
|
|
10
|
+
|
|
11
|
+
Classes
|
|
12
|
+
-------
|
|
13
|
+
FakeLogger
|
|
14
|
+
Mock logger implementing standard logging interface
|
|
15
|
+
|
|
16
|
+
Functions
|
|
17
|
+
---------
|
|
18
|
+
start_event_logging
|
|
19
|
+
Configure and initialize logging system
|
|
20
|
+
get_logging_level_from_verbose_count
|
|
21
|
+
Convert verbosity flags to logging levels
|
|
22
|
+
start_logging
|
|
23
|
+
Simplified logging setup with verbosity control
|
|
24
|
+
event_logger, evlog
|
|
25
|
+
Convenient access to global logger instance
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
--------
|
|
29
|
+
>>> from pyplotutil.loggingutil import start_logging
|
|
30
|
+
>>> logger = start_event_logging(argv=["script.py"], output_dir="logs", logging_level="INFO")
|
|
31
|
+
>>> logger.info("Processing started")
|
|
32
|
+
|
|
33
|
+
Notes
|
|
34
|
+
-----
|
|
35
|
+
- Uses Python's built-in logging module as the foundation
|
|
36
|
+
- Automatically disables console output during pytest execution
|
|
37
|
+
- Supports both string and integer logging levels
|
|
38
|
+
- Maintains compatibility with standard logging interfaces
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import io
|
|
45
|
+
import logging
|
|
46
|
+
import sys
|
|
47
|
+
import tempfile
|
|
48
|
+
import traceback
|
|
49
|
+
import warnings
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from typing import TYPE_CHECKING, TextIO
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from collections.abc import Mapping
|
|
55
|
+
from types import FrameType
|
|
56
|
+
|
|
57
|
+
from pyplotutil._typing import Unknown
|
|
58
|
+
|
|
59
|
+
DEFAULT_FORMAT = "%(asctime)s (%(module)s:%(lineno)d) [%(levelname)s]: %(message)s"
|
|
60
|
+
_default_formatter = logging.Formatter(DEFAULT_FORMAT)
|
|
61
|
+
|
|
62
|
+
CRITICAL = 50
|
|
63
|
+
FATAL = CRITICAL
|
|
64
|
+
ERROR = 40
|
|
65
|
+
WARNING = 30
|
|
66
|
+
WARN = WARNING
|
|
67
|
+
INFO = 20
|
|
68
|
+
DEBUG = 10
|
|
69
|
+
NOTSET = 0
|
|
70
|
+
|
|
71
|
+
_LEVEL_TO_NAME = {
|
|
72
|
+
CRITICAL: "CRITICAL",
|
|
73
|
+
ERROR: "ERROR",
|
|
74
|
+
WARNING: "WARNING",
|
|
75
|
+
INFO: "INFO",
|
|
76
|
+
DEBUG: "DEBUG",
|
|
77
|
+
NOTSET: "NOTSET",
|
|
78
|
+
}
|
|
79
|
+
_NAME_TO_LEVEL = {
|
|
80
|
+
"CRITICAL": CRITICAL,
|
|
81
|
+
"FATAL": FATAL,
|
|
82
|
+
"ERROR": ERROR,
|
|
83
|
+
"WARN": WARNING,
|
|
84
|
+
"WARNING": WARNING,
|
|
85
|
+
"INFO": INFO,
|
|
86
|
+
"DEBUG": DEBUG,
|
|
87
|
+
"NOTSET": NOTSET,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _Error(Exception):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if hasattr(sys, "_getframe"):
|
|
96
|
+
|
|
97
|
+
def currentframe() -> FrameType | None:
|
|
98
|
+
"""Return the frame object for the caller's stack frame."""
|
|
99
|
+
return sys._getframe(1) # noqa: SLF001
|
|
100
|
+
else:
|
|
101
|
+
|
|
102
|
+
def currentframe() -> FrameType | None:
|
|
103
|
+
"""Return the frame object for the caller's stack frame."""
|
|
104
|
+
try:
|
|
105
|
+
raise _Error # noqa: TRY301
|
|
106
|
+
except _Error as exc:
|
|
107
|
+
if exc.__traceback__ is not None:
|
|
108
|
+
return exc.__traceback__.tb_frame.f_back
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def check_level(level: int | str) -> int:
|
|
113
|
+
"""Convert logging level string or int to corresponding integer level.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
level : int or str
|
|
118
|
+
Logging level as integer or string name (e.g. 'INFO', 'DEBUG')
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
int
|
|
123
|
+
Integer logging level value
|
|
124
|
+
|
|
125
|
+
Raises
|
|
126
|
+
------
|
|
127
|
+
ValueError
|
|
128
|
+
If level string is not recognized
|
|
129
|
+
TypeError
|
|
130
|
+
If level is not an integer or string
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
if isinstance(level, int):
|
|
134
|
+
rv = level
|
|
135
|
+
elif str(level) == level:
|
|
136
|
+
if level not in _NAME_TO_LEVEL:
|
|
137
|
+
msg = f"Unknown level: {level}"
|
|
138
|
+
raise ValueError(msg)
|
|
139
|
+
rv = _NAME_TO_LEVEL[level]
|
|
140
|
+
else:
|
|
141
|
+
msg = f"Level not an integer or a valid string: {level}"
|
|
142
|
+
raise TypeError(msg)
|
|
143
|
+
return rv
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class FakeLogger:
|
|
147
|
+
"""Mock logger implementing standard logging interface.
|
|
148
|
+
|
|
149
|
+
Provides logging functionality that mimics Python's standard Logger class, with output directed
|
|
150
|
+
to a specified stream.
|
|
151
|
+
|
|
152
|
+
Attributes
|
|
153
|
+
----------
|
|
154
|
+
level : int
|
|
155
|
+
Current logging level
|
|
156
|
+
disabled : bool
|
|
157
|
+
Whether logging is currently disabled
|
|
158
|
+
terminator : str
|
|
159
|
+
String appended after each log message
|
|
160
|
+
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
_level: int
|
|
164
|
+
_formatter: logging.Formatter
|
|
165
|
+
_stream: TextIO
|
|
166
|
+
_disabled: bool
|
|
167
|
+
terminator: str = "\n"
|
|
168
|
+
|
|
169
|
+
def __init__(self, level: int | str = NOTSET, stream: TextIO | None = None, *, disable: bool = False) -> None:
|
|
170
|
+
"""Initialize FakeLogger with specified level and output stream.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
level : int or str
|
|
175
|
+
The logging level to set, either as an integer or a string (default is WARNING)
|
|
176
|
+
stream : TextIO or None
|
|
177
|
+
Output stream for log messages, defaults to sys.stderr
|
|
178
|
+
disable : bool
|
|
179
|
+
Whether to start with logging disabled (default is False)
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
self.set_level(level)
|
|
183
|
+
self._formatter = _default_formatter
|
|
184
|
+
self._stream = stream if stream is not None else sys.stderr
|
|
185
|
+
self._disabled = disable
|
|
186
|
+
|
|
187
|
+
def set_level(self, level: int | str) -> None:
|
|
188
|
+
"""Set the logging level threshold.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
level : int or str
|
|
193
|
+
New logging level to set
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
self._level = check_level(level)
|
|
197
|
+
|
|
198
|
+
def setLevel(self, level: int | str) -> None: # noqa: N802
|
|
199
|
+
"""Legacy-style alias for set_level.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
level : int or str
|
|
204
|
+
New logging level to set
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
return self.set_level(level)
|
|
208
|
+
|
|
209
|
+
def is_enabled_for(self, level: int) -> bool:
|
|
210
|
+
"""Check if logger will process messages at given level.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
level : int
|
|
215
|
+
Logging level to check
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
bool
|
|
220
|
+
True if messages at this level will be processed
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
if self.disabled:
|
|
224
|
+
return False
|
|
225
|
+
return level >= self.level
|
|
226
|
+
|
|
227
|
+
def find_caller(self, *, stack_info: bool = False, stacklevel: int = 1) -> tuple[str, int, str, str | None]:
|
|
228
|
+
"""Identify the module and line number where logging was called.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
stack_info : bool
|
|
233
|
+
Whether to capture full stack information
|
|
234
|
+
stacklevel : int
|
|
235
|
+
Number of frames to skip when finding caller
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
tuple
|
|
240
|
+
(filename, line number, function name, stack info)
|
|
241
|
+
|
|
242
|
+
"""
|
|
243
|
+
f = currentframe()
|
|
244
|
+
if f is None:
|
|
245
|
+
return "(unknown file)", 0, "(unknown function)", None
|
|
246
|
+
while stacklevel > 0:
|
|
247
|
+
next_f = f.f_back
|
|
248
|
+
if next_f is None:
|
|
249
|
+
break
|
|
250
|
+
f = next_f
|
|
251
|
+
stacklevel -= 1
|
|
252
|
+
co = f.f_code
|
|
253
|
+
sinfo = None
|
|
254
|
+
if stack_info:
|
|
255
|
+
with io.StringIO() as sio:
|
|
256
|
+
sio.write("Stack (most recent call last):\n")
|
|
257
|
+
traceback.print_stack(f, file=sio)
|
|
258
|
+
sinfo = sio.getvalue()
|
|
259
|
+
if sinfo[-1] == "\n":
|
|
260
|
+
sinfo = sinfo[:-1]
|
|
261
|
+
return co.co_filename, f.f_lineno, co.co_name, sinfo
|
|
262
|
+
|
|
263
|
+
def _log(
|
|
264
|
+
self,
|
|
265
|
+
level: int,
|
|
266
|
+
msg: str,
|
|
267
|
+
args: tuple[object, ...],
|
|
268
|
+
*,
|
|
269
|
+
exc_info: tuple[None, None, None] | None = None,
|
|
270
|
+
extra: Mapping[str, object] | None = None,
|
|
271
|
+
stack_info: bool = False,
|
|
272
|
+
stacklevel: int = 1,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Core logging method that handles message creation and routing.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
level : int
|
|
279
|
+
Logging level for this message
|
|
280
|
+
msg : str
|
|
281
|
+
Message format string
|
|
282
|
+
args : tuple
|
|
283
|
+
Arguments for message formatting
|
|
284
|
+
**kwargs : dict
|
|
285
|
+
Additional logging context
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
fn, lno, func, sinfo = self.find_caller(stack_info=stack_info, stacklevel=stacklevel)
|
|
289
|
+
_ = extra
|
|
290
|
+
record = logging.LogRecord("root", level, fn, lno, msg, args, exc_info, func, sinfo)
|
|
291
|
+
self.handle(record)
|
|
292
|
+
|
|
293
|
+
def handle(self, record: logging.LogRecord) -> None:
|
|
294
|
+
"""Process a LogRecord by emitting it.
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
record : logging.LogRecord
|
|
299
|
+
The record to be processed
|
|
300
|
+
|
|
301
|
+
"""
|
|
302
|
+
self.emit(record)
|
|
303
|
+
|
|
304
|
+
def flush(self) -> None:
|
|
305
|
+
"""Ensure all logging output has been flushed to the stream."""
|
|
306
|
+
if self._stream and hasattr(self._stream, "flush"):
|
|
307
|
+
self._stream.flush()
|
|
308
|
+
|
|
309
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
310
|
+
"""Write formatted LogRecord to output stream.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
record : logging.LogRecord
|
|
315
|
+
The record to be emitted
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
msg = self.format(record)
|
|
319
|
+
self._stream.write(msg + self.terminator)
|
|
320
|
+
self.flush()
|
|
321
|
+
|
|
322
|
+
def set_formatter(self, fmt: logging.Formatter) -> None:
|
|
323
|
+
"""Set the formatter for log messages.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
fmt : logging.Formatter
|
|
328
|
+
Formatter to use for log messages
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
self._formatter = fmt
|
|
332
|
+
|
|
333
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
334
|
+
"""Apply formatter to LogRecord.
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
record : logging.LogRecord
|
|
339
|
+
The record to format
|
|
340
|
+
|
|
341
|
+
Returns
|
|
342
|
+
-------
|
|
343
|
+
str
|
|
344
|
+
Formatted log message
|
|
345
|
+
|
|
346
|
+
"""
|
|
347
|
+
return self._formatter.format(record)
|
|
348
|
+
|
|
349
|
+
def debug(self, msg: str, *args: object, **kwargs: Unknown) -> None:
|
|
350
|
+
"""Log message at DEBUG level.
|
|
351
|
+
|
|
352
|
+
Parameters
|
|
353
|
+
----------
|
|
354
|
+
msg : str
|
|
355
|
+
Message format string
|
|
356
|
+
*args : tuple
|
|
357
|
+
Arguments for message formatting
|
|
358
|
+
**kwargs : dict
|
|
359
|
+
Additional logging context
|
|
360
|
+
|
|
361
|
+
"""
|
|
362
|
+
if self.is_enabled_for(DEBUG):
|
|
363
|
+
self._log(DEBUG, msg, args, **kwargs)
|
|
364
|
+
|
|
365
|
+
def info(self, msg: str, *args: object, **kwargs: Unknown) -> None:
|
|
366
|
+
"""Log message at INFO level.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
msg : str
|
|
371
|
+
Message format string
|
|
372
|
+
*args : tuple
|
|
373
|
+
Arguments for message formatting
|
|
374
|
+
**kwargs : dict
|
|
375
|
+
Additional logging context
|
|
376
|
+
|
|
377
|
+
"""
|
|
378
|
+
if self.is_enabled_for(INFO):
|
|
379
|
+
self._log(INFO, msg, args, **kwargs)
|
|
380
|
+
|
|
381
|
+
def warning(self, msg: str, *args: object, **kwargs: Unknown) -> None:
|
|
382
|
+
"""Log message at WARNING level.
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
msg : str
|
|
387
|
+
Message format string
|
|
388
|
+
*args : tuple
|
|
389
|
+
Arguments for message formatting
|
|
390
|
+
**kwargs : dict
|
|
391
|
+
Additional logging context
|
|
392
|
+
|
|
393
|
+
"""
|
|
394
|
+
if self.is_enabled_for(WARNING):
|
|
395
|
+
self._log(WARNING, msg, args, **kwargs)
|
|
396
|
+
|
|
397
|
+
def error(self, msg: str, *args: object, **kwargs: Unknown) -> None:
|
|
398
|
+
"""Log message at ERROR level.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
msg : str
|
|
403
|
+
Message format string
|
|
404
|
+
*args : tuple
|
|
405
|
+
Arguments for message formatting
|
|
406
|
+
**kwargs : dict
|
|
407
|
+
Additional logging context
|
|
408
|
+
|
|
409
|
+
"""
|
|
410
|
+
if self.is_enabled_for(ERROR):
|
|
411
|
+
self._log(ERROR, msg, args, **kwargs)
|
|
412
|
+
|
|
413
|
+
def critical(self, msg: str, *args: object, **kwargs: Unknown) -> None:
|
|
414
|
+
"""Log message at CRITICAL level.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
msg : str
|
|
419
|
+
Message format string
|
|
420
|
+
*args : tuple
|
|
421
|
+
Arguments for message formatting
|
|
422
|
+
**kwargs : dict
|
|
423
|
+
Additional logging context
|
|
424
|
+
|
|
425
|
+
"""
|
|
426
|
+
if self.is_enabled_for(CRITICAL):
|
|
427
|
+
self._log(CRITICAL, msg, args, **kwargs)
|
|
428
|
+
|
|
429
|
+
def log(self, level: int, msg: str, *args: object, **kwargs: Unknown) -> None:
|
|
430
|
+
"""Log message at specified level.
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
level : int
|
|
435
|
+
Logging level to use
|
|
436
|
+
msg : str
|
|
437
|
+
Message format string
|
|
438
|
+
*args : tuple
|
|
439
|
+
Arguments for message formatting
|
|
440
|
+
**kwargs : dict
|
|
441
|
+
Additional logging context
|
|
442
|
+
|
|
443
|
+
"""
|
|
444
|
+
if self.is_enabled_for(level):
|
|
445
|
+
self._log(level, msg, args, **kwargs)
|
|
446
|
+
|
|
447
|
+
def toggle(self, *, disabled: bool | None = None) -> bool:
|
|
448
|
+
"""Toggle or set logger's disabled state.
|
|
449
|
+
|
|
450
|
+
Parameters
|
|
451
|
+
----------
|
|
452
|
+
disabled : bool or None
|
|
453
|
+
New disabled state, or None to toggle current state
|
|
454
|
+
|
|
455
|
+
Returns
|
|
456
|
+
-------
|
|
457
|
+
bool
|
|
458
|
+
New disabled state
|
|
459
|
+
|
|
460
|
+
"""
|
|
461
|
+
if disabled is None:
|
|
462
|
+
self._disabled = not self._disabled
|
|
463
|
+
else:
|
|
464
|
+
self._disabled = disabled
|
|
465
|
+
return self._disabled
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def level(self) -> int:
|
|
469
|
+
"""Get current logging level.
|
|
470
|
+
|
|
471
|
+
Returns
|
|
472
|
+
-------
|
|
473
|
+
int
|
|
474
|
+
Current logging level
|
|
475
|
+
|
|
476
|
+
"""
|
|
477
|
+
return self._level
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def disabled(self) -> bool:
|
|
481
|
+
"""Get logger's disabled state.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
bool
|
|
486
|
+
Whether logging is currently disabled
|
|
487
|
+
|
|
488
|
+
Return whether the console output is disabled or not.
|
|
489
|
+
|
|
490
|
+
"""
|
|
491
|
+
return self._disabled
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _running_in_pytest() -> bool:
|
|
495
|
+
"""Check if the code is running inside pytest.
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
-------
|
|
499
|
+
bool
|
|
500
|
+
True if running in pytest, False otherwise.
|
|
501
|
+
|
|
502
|
+
"""
|
|
503
|
+
return "pytest" in sys.modules
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
_event_logger: logging.Logger | FakeLogger = FakeLogger(disable=_running_in_pytest())
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _get_default_event_log_filename(
|
|
510
|
+
argv: list[str],
|
|
511
|
+
output_dir: str | Path | None,
|
|
512
|
+
given_log_filename: str | Path | None,
|
|
513
|
+
) -> Path:
|
|
514
|
+
"""Get the default event log filename.
|
|
515
|
+
|
|
516
|
+
Parameters
|
|
517
|
+
----------
|
|
518
|
+
argv : list of str
|
|
519
|
+
Command-line arguments.
|
|
520
|
+
output_dir : str or Path or None
|
|
521
|
+
Directory to save the log file, or None for the default temporary directory.
|
|
522
|
+
given_log_filename : str or Path or None
|
|
523
|
+
Specific log filename, or None to generate one.
|
|
524
|
+
|
|
525
|
+
Returns
|
|
526
|
+
-------
|
|
527
|
+
Path
|
|
528
|
+
The path to the log file.
|
|
529
|
+
|
|
530
|
+
"""
|
|
531
|
+
event_log_filename: Path
|
|
532
|
+
if given_log_filename is None:
|
|
533
|
+
if output_dir is not None:
|
|
534
|
+
event_log_filename = (Path(output_dir) / Path(argv[0]).name).with_suffix(".log")
|
|
535
|
+
else:
|
|
536
|
+
event_log_filename = (Path(tempfile.gettempdir()) / Path(argv[0]).name).with_suffix(".log")
|
|
537
|
+
else:
|
|
538
|
+
event_log_filename = Path(given_log_filename)
|
|
539
|
+
|
|
540
|
+
return event_log_filename
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def start_event_logging(
|
|
544
|
+
argv: list[str],
|
|
545
|
+
output_dir: str | Path | None = None,
|
|
546
|
+
log_filename: str | Path | None = None,
|
|
547
|
+
name: str | None = None,
|
|
548
|
+
logging_level: int | str = logging.WARNING,
|
|
549
|
+
logging_level_file: int | str = logging.DEBUG,
|
|
550
|
+
fmt: str | None = None,
|
|
551
|
+
) -> logging.Logger:
|
|
552
|
+
"""Configure and start event logging system.
|
|
553
|
+
|
|
554
|
+
Sets up both console and file handlers with specified logging levels.
|
|
555
|
+
|
|
556
|
+
Parameters
|
|
557
|
+
----------
|
|
558
|
+
argv : list[str]
|
|
559
|
+
Command line arguments used to generate default log filename
|
|
560
|
+
output_dir : str or Path or None
|
|
561
|
+
Directory for log file output
|
|
562
|
+
log_filename : str or Path or None
|
|
563
|
+
Custom log filename, generated from argv if None
|
|
564
|
+
name : str or None
|
|
565
|
+
Logger name, defaults to module name if None
|
|
566
|
+
logging_level : int or str
|
|
567
|
+
Console handler logging level
|
|
568
|
+
logging_level_file : int or str
|
|
569
|
+
File handler logging level
|
|
570
|
+
fmt : str or None
|
|
571
|
+
Custom log message format string
|
|
572
|
+
|
|
573
|
+
Returns
|
|
574
|
+
-------
|
|
575
|
+
logging.Logger
|
|
576
|
+
Configured logger instance ready for use
|
|
577
|
+
|
|
578
|
+
"""
|
|
579
|
+
if fmt is None:
|
|
580
|
+
fmt = DEFAULT_FORMAT
|
|
581
|
+
if name is None:
|
|
582
|
+
name = __name__
|
|
583
|
+
|
|
584
|
+
global _event_logger # noqa: PLW0603
|
|
585
|
+
if isinstance(_event_logger, logging.Logger) and _event_logger.name == name:
|
|
586
|
+
# Event logging has been started.
|
|
587
|
+
return _event_logger
|
|
588
|
+
|
|
589
|
+
# Create a new logger instance.
|
|
590
|
+
logger = logging.getLogger(name)
|
|
591
|
+
logger.setLevel(logging.DEBUG)
|
|
592
|
+
|
|
593
|
+
# Create a formatter
|
|
594
|
+
formatter = logging.Formatter(fmt)
|
|
595
|
+
|
|
596
|
+
# Setup a console handler
|
|
597
|
+
ch = logging.StreamHandler()
|
|
598
|
+
ch.setLevel(logging_level)
|
|
599
|
+
ch.setFormatter(formatter)
|
|
600
|
+
logger.addHandler(ch)
|
|
601
|
+
|
|
602
|
+
# Setup a file handler
|
|
603
|
+
event_log_filename = _get_default_event_log_filename(argv, output_dir, log_filename)
|
|
604
|
+
try:
|
|
605
|
+
fh = logging.FileHandler(event_log_filename)
|
|
606
|
+
except FileNotFoundError:
|
|
607
|
+
# Maybe, running in dry-run mode...
|
|
608
|
+
msg = f"Unable to save log file (if running in dry-run mode, ignore this): {event_log_filename}"
|
|
609
|
+
warnings.warn(msg, RuntimeWarning, stacklevel=2)
|
|
610
|
+
else:
|
|
611
|
+
fh.setLevel(logging_level_file)
|
|
612
|
+
fh.setFormatter(formatter)
|
|
613
|
+
logger.addHandler(fh)
|
|
614
|
+
|
|
615
|
+
# Always log command arguments.
|
|
616
|
+
logger.debug("Start event logging")
|
|
617
|
+
logger.debug("Logger name: %s", logger.name)
|
|
618
|
+
logger.debug("Log filename: %s", event_log_filename)
|
|
619
|
+
cmd = "python " + " ".join(argv)
|
|
620
|
+
logger.debug("Command: %s", cmd)
|
|
621
|
+
|
|
622
|
+
_event_logger = logger
|
|
623
|
+
return _event_logger
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def event_logger() -> logging.Logger | FakeLogger:
|
|
627
|
+
"""Get the global event logger instance.
|
|
628
|
+
|
|
629
|
+
Returns
|
|
630
|
+
-------
|
|
631
|
+
logging.Logger or FakeLogger
|
|
632
|
+
The global event logger instance.
|
|
633
|
+
|
|
634
|
+
"""
|
|
635
|
+
return _event_logger
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def evlog() -> logging.Logger | FakeLogger:
|
|
639
|
+
"""Alias for getting the global event logger instance.
|
|
640
|
+
|
|
641
|
+
Returns
|
|
642
|
+
-------
|
|
643
|
+
logging.Logger or FakeLogger
|
|
644
|
+
The global event logger instance.
|
|
645
|
+
|
|
646
|
+
"""
|
|
647
|
+
return event_logger()
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def get_event_logger_filename() -> Path | None:
|
|
651
|
+
"""Retrieve Path object of log file when the event logger is started.
|
|
652
|
+
|
|
653
|
+
Returns
|
|
654
|
+
-------
|
|
655
|
+
Path or None
|
|
656
|
+
Path object of log file, or None when the event logger is not started.
|
|
657
|
+
|
|
658
|
+
"""
|
|
659
|
+
logger = event_logger()
|
|
660
|
+
if isinstance(logger, FakeLogger):
|
|
661
|
+
return None
|
|
662
|
+
for handler in logger.handlers:
|
|
663
|
+
if isinstance(handler, logging.FileHandler):
|
|
664
|
+
return Path(handler.baseFilename)
|
|
665
|
+
return None
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def get_logging_level_from_verbose_count(verbose_count: int) -> str:
|
|
669
|
+
"""Convert verbosity count to logging level string.
|
|
670
|
+
|
|
671
|
+
Parameters
|
|
672
|
+
----------
|
|
673
|
+
verbose_count : int
|
|
674
|
+
Number of verbose flags (e.g. -v, -vv, -vvv)
|
|
675
|
+
0: WARNING
|
|
676
|
+
1: INFO
|
|
677
|
+
2+: DEBUG
|
|
678
|
+
|
|
679
|
+
Returns
|
|
680
|
+
-------
|
|
681
|
+
str
|
|
682
|
+
Corresponding logging level name
|
|
683
|
+
|
|
684
|
+
"""
|
|
685
|
+
match verbose_count:
|
|
686
|
+
case 0:
|
|
687
|
+
return "WARNING"
|
|
688
|
+
case 1:
|
|
689
|
+
return "INFO"
|
|
690
|
+
case _:
|
|
691
|
+
return "DEBUG"
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def start_logging(
|
|
695
|
+
argv: list[str],
|
|
696
|
+
output_dir: str | Path | None,
|
|
697
|
+
name: str,
|
|
698
|
+
verbose_count: int,
|
|
699
|
+
*,
|
|
700
|
+
dry_run: bool = False,
|
|
701
|
+
) -> logging.Logger:
|
|
702
|
+
"""Initialize logging system with verbosity control and output configuration.
|
|
703
|
+
|
|
704
|
+
Sets up a complete logging system with both file and console handlers based on the specified
|
|
705
|
+
verbosity level. Creates output directory if it doesn't exist (unless in dry-run mode).
|
|
706
|
+
|
|
707
|
+
Parameters
|
|
708
|
+
----------
|
|
709
|
+
argv : list[str]
|
|
710
|
+
Command line arguments used for log file naming
|
|
711
|
+
output_dir : Path or None
|
|
712
|
+
Directory where log files will be stored
|
|
713
|
+
name : str
|
|
714
|
+
Name for the logger instance
|
|
715
|
+
verbose_count : int
|
|
716
|
+
Verbosity level from command line:
|
|
717
|
+
- 0: WARNING level
|
|
718
|
+
- 1: INFO level
|
|
719
|
+
- 2+: DEBUG level
|
|
720
|
+
dry_run : bool, optional
|
|
721
|
+
When True, prevents creation of output directory and log files,
|
|
722
|
+
default is False
|
|
723
|
+
|
|
724
|
+
Returns
|
|
725
|
+
-------
|
|
726
|
+
logging.Logger
|
|
727
|
+
Configured logger instance ready for immediate use
|
|
728
|
+
|
|
729
|
+
Examples
|
|
730
|
+
--------
|
|
731
|
+
>>> logger = start_logging(argv=["script.py"], output_dir=Path("logs"), name="myapp", verbose_count=1)
|
|
732
|
+
>>> logger.info("Application started")
|
|
733
|
+
|
|
734
|
+
"""
|
|
735
|
+
logging_level = get_logging_level_from_verbose_count(verbose_count)
|
|
736
|
+
if not dry_run and output_dir is not None:
|
|
737
|
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
738
|
+
return start_event_logging(argv, output_dir, name=name, logging_level=logging_level)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
# Local Variables:
|
|
742
|
+
# jinx-local-words: "TextIO args argv asctime dir evlog fmt getframe kwargs levelname lineno loggingutil msg myapp noqa py pyplotutil pytest stacklevel stderr str sys vv vvv" # noqa: E501
|
|
743
|
+
# End:
|