cs-logutils 20250306__py2.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.
cs/logutils.py ADDED
@@ -0,0 +1,808 @@
1
+ #!/usr/bin/python
2
+ #
3
+ # Convenience routines for logging.
4
+ # - Cameron Simpson <cs@cskk.id.au> 29aug2009
5
+ #
6
+
7
+ r'''
8
+ Logging convenience routines.
9
+
10
+ The logging package is very useful, but a little painful to use.
11
+ This package provides low impact logging setup and some extremely
12
+ useful if unconventional context hooks for logging.
13
+
14
+ The default logging verbosity output format has different defaults
15
+ based on whether an output log file is a tty
16
+ and whether the environment variable `$DEBUG` is set, and to what.
17
+
18
+ On terminals warnings and errors get ANSI colouring.
19
+
20
+ A mode is available that uses `cs.upd` for certain log levels.
21
+
22
+ Log messages dispatched via `warning` and friends from this module
23
+ are automatically prefixed with the current `cs.pfx` prefix string,
24
+ providing automatic message context.
25
+
26
+ Some examples:
27
+ --------------
28
+
29
+ Program initialisation:
30
+
31
+ from cs.logutils import setup_logging
32
+
33
+ def main(argv):
34
+ cmd = os.path.basename(argv.pop(0))
35
+ setup_logging(cmd)
36
+
37
+ Basic logging from anywhere:
38
+
39
+ from cs.logutils import info, warning, error
40
+ [...]
41
+ def some_function(...):
42
+ [...]
43
+ error("nastiness found! bad value=%r", bad_value)
44
+ '''
45
+
46
+ from __future__ import with_statement
47
+ import codecs
48
+ from contextlib import contextmanager
49
+ try:
50
+ import importlib
51
+ except ImportError:
52
+ importlib = None
53
+ import logging
54
+ from logging import Formatter, StreamHandler
55
+ import os
56
+ import os.path
57
+ from pprint import pformat
58
+ import stat
59
+ import sys
60
+ from threading import Lock
61
+ import time
62
+ import traceback
63
+ from types import SimpleNamespace as NS
64
+
65
+ from cs.ansi_colour import colourise, env_no_color
66
+ from cs.context import stackattrs
67
+ from cs.deco import fmtdoc, logging_wrapper
68
+ from cs.lex import is_dotted_identifier
69
+ import cs.pfx
70
+ from cs.pfx import Pfx, XP
71
+ from cs.py.func import funccite
72
+
73
+ __version__ = '20250306'
74
+
75
+ DISTINFO = {
76
+ 'keywords': ["python2", "python3"],
77
+ 'classifiers': [
78
+ "Programming Language :: Python",
79
+ "Programming Language :: Python :: 2",
80
+ "Programming Language :: Python :: 3",
81
+ ],
82
+ 'install_requires': [
83
+ 'cs.ansi_colour>=20200729',
84
+ 'cs.context>=stackable_state',
85
+ 'cs.deco',
86
+ 'cs.lex',
87
+ 'cs.pfx',
88
+ 'cs.py.func',
89
+ ],
90
+ }
91
+
92
+ DEFAULT_BASE_FORMAT = '%(asctime)s %(levelname)s %(message)s'
93
+ DEFAULT_PFX_FORMAT = '%(asctime)s %(levelname)s %(pfx)s: %(message)s'
94
+ DEFAULT_PFX_FORMAT_TTY = '%(pfx)s: %(message)s'
95
+
96
+ # High level action tracking, above INFO and below WARNING.
97
+ TRACK = logging.INFO + 5
98
+
99
+ # Quiet messaging, below TRACK and above the rest.
100
+ QUIET = TRACK - 1
101
+
102
+ # Special status line tracking, above INFO and below TRACK and WARNING
103
+ STATUS = QUIET - 1
104
+
105
+ # Special verbose value, below INFO but above DEBUG.
106
+ VERBOSE = logging.INFO - 1
107
+
108
+ # check the hierarchy
109
+ assert (
110
+ logging.DEBUG < VERBOSE < logging.INFO < STATUS < QUIET < TRACK <
111
+ logging.WARNING
112
+ )
113
+
114
+ loginfo = None
115
+ D_mode = False
116
+
117
+ def ifdebug():
118
+ ''' Test the `loginfo.level` against `logging.DEBUG`.
119
+ '''
120
+ global loginfo # pylint: disable=global-statement
121
+ if loginfo is None:
122
+ loginfo = setup_logging()
123
+ return loginfo.level is not None and loginfo.level <= logging.DEBUG
124
+
125
+ class LoggingState(NS):
126
+ ''' A logging setup arranged for conventional UNIX command line use.
127
+ '''
128
+
129
+ # pylint: disable=too-many-branches,too-many-statements,too-many-locals
130
+ # pylint: disable=too-many-arguments,redefined-builtin
131
+ def __init__(
132
+ self,
133
+ cmd=None,
134
+ main_log=None,
135
+ format=None,
136
+ level=None,
137
+ flags=None,
138
+ upd_mode=None,
139
+ ansi_mode=None,
140
+ trace_mode=None,
141
+ verbose=None,
142
+ supplant_root_logger=False,
143
+ ):
144
+ ''' Prepare the `LoggingState` for conventional UNIX command
145
+ line error messaging.
146
+
147
+ Amongst other things, the default logger now includes
148
+ the `cs.pfx` prefix in the message.
149
+
150
+ This function runs in two modes:
151
+ - if logging has not been set up, it sets up a root logger
152
+ - if the root logger already has handlers,
153
+ monkey patch the first handler's formatter to prefix the `cs.pfx` state
154
+
155
+ Parameters:
156
+ * `cmd`: program name, default from `basename(sys.argv[0])`.
157
+ * `main_log`: default logging system.
158
+ If `None`, the main log will go to `sys.stderr`;
159
+ if `main_log` is a string, is it used as a filename to
160
+ open in append mode;
161
+ otherwise main_log should be a stream suitable
162
+ for use with `logging.StreamHandler()`.
163
+ The resulting log handler is added to the `logging` root logger.
164
+ * `format`: the message format for `main_log`.
165
+ If `None`, use `DEFAULT_PFX_FORMAT_TTY`
166
+ when `main_log` is a tty or FIFO,
167
+ otherwise `DEFAULT_PFX_FORMAT`.
168
+ * `level`: `main_log` logging level.
169
+ If `None`, infer a level from the environment
170
+ using `infer_logging_level()`.
171
+ * `flags`: a string containing debugging flags separated by commas.
172
+ If `None`, infer the flags from the environment using
173
+ `infer_logging_level()`.
174
+ The following flags have meaning:
175
+ `D`: set cs.logutils.D_mode to True;
176
+ `TDUMP`: attach a signal handler to SIGHUP to do a thread stack dump;
177
+ `TRACE`: enable various noisy tracing facilities;
178
+ `UPD`, `NOUPD`: set the default for `upd_mode` to True or False respectively.
179
+ * `upd_mode`: a Boolean to activate cs.upd as the `main_log` method;
180
+ if `None`, set it to `True` if `flags` contains 'UPD',
181
+ otherwise to `False` if `flags` contains 'NOUPD',
182
+ otherwise set it from `main_log.isatty()`.
183
+ A true value causes the root logger to use `cs.upd` for logging.
184
+ * `ansi_mode`: if `None`,
185
+ set it from `main_log.isatty() and not cs.colourise.env_no_color()`,
186
+ which thus honours the `$NO_COLOR` environment variable
187
+ (see https://no-color.org/ for the convention).
188
+ A true value causes the root logger to colour certain logging levels
189
+ using ANSI terminal sequences (currently only if `cs.upd` is used).
190
+ * `trace_mode`: if `None`, set it according to the presence of
191
+ 'TRACE' in flags. Otherwise if `trace_mode` is true, set the
192
+ global `loginfo.trace_level` to `loginfo.level`; otherwise it defaults
193
+ to `logging.DEBUG`.
194
+ * `verbose`: if `None`, then if stderr is a tty then the log
195
+ level is `INFO` otherwise `WARNING`. Otherwise, if `verbose` is
196
+ true then the log level is `INFO` otherwise `WARNING`.
197
+ '''
198
+ global D_mode, loginfo # pylint: disable=global-statement
199
+
200
+ # infer logging modes, these are the initial defaults
201
+ inferred = infer_logging_level(verbose=verbose)
202
+ if level is None:
203
+ level = inferred.level
204
+ if flags is None:
205
+ flags = inferred.flags
206
+
207
+ if cmd is None:
208
+ cmd = os.path.basename(sys.argv[0])
209
+
210
+ if main_log is None:
211
+ main_log = sys.stderr
212
+ elif isinstance(main_log, str):
213
+ # pylint: disable=consider-using-with
214
+ main_log = open(main_log, "a", encoding='utf-8')
215
+
216
+ # determine some attributes of main_log
217
+ try:
218
+ fd = main_log.fileno()
219
+ except (AttributeError, IOError):
220
+ is_fifo = False
221
+ ##is_reg = False # unused
222
+ is_tty = False
223
+ else:
224
+ st = os.fstat(fd)
225
+ is_fifo = stat.S_ISFIFO(st.st_mode)
226
+ ##is_reg = stat.S_ISREG(st.st_mode) # unused
227
+ is_tty = stat.S_ISCHR(st.st_mode)
228
+
229
+ if getattr(main_log, 'encoding', None) is None:
230
+ main_log = codecs.getwriter("utf-8")(main_log)
231
+
232
+ if trace_mode is None:
233
+ trace_mode = 'TRACE' in flags
234
+
235
+ if 'D' in flags:
236
+ D_mode = True
237
+
238
+ if upd_mode is None:
239
+ if 'UPD' in flags:
240
+ upd_mode = True
241
+ elif 'NOUPD' in flags:
242
+ upd_mode = False
243
+ else:
244
+ upd_mode = is_tty
245
+
246
+ if ansi_mode is None:
247
+ ansi_mode = is_tty and not env_no_color()
248
+
249
+ if format is None:
250
+ if is_tty or is_fifo:
251
+ format = DEFAULT_PFX_FORMAT_TTY
252
+ else:
253
+ format = DEFAULT_PFX_FORMAT
254
+
255
+ upd_ = None
256
+ if upd_mode:
257
+ from cs.upd import Upd # pylint: disable=import-outside-toplevel
258
+ upd_ = Upd()
259
+
260
+ if trace_mode:
261
+ # enable tracing in the thread that called setup_logging
262
+ Pfx._state.trace = info
263
+ trace_level = level
264
+ else:
265
+ trace_level = logging.DEBUG
266
+
267
+ NS.__init__(
268
+ self,
269
+ main_log=main_log,
270
+ level=level,
271
+ verbose=verbose,
272
+ trace_level=trace_level,
273
+ flags=flags,
274
+ cmd=cmd,
275
+ upd=upd_,
276
+ upd_mode=upd_mode,
277
+ ansi_mode=ansi_mode,
278
+ format=format,
279
+ supplant_root_logger=supplant_root_logger,
280
+ )
281
+
282
+ def apply(self):
283
+ ''' Apply this `LoggingState` to the current logging setup.
284
+ '''
285
+ global loginfo
286
+ root_logger = logging.getLogger()
287
+ if root_logger.handlers:
288
+ # The logging system is already set up.
289
+ # Just monkey patch the leading handler's formatter.
290
+ PfxFormatter.patch_formatter(root_logger.handlers[0].formatter)
291
+ else:
292
+ # Set up a handler etc.
293
+ main_handler = logging.StreamHandler(self.main_log)
294
+ if self.upd_mode:
295
+ main_handler = UpdHandler(
296
+ self.main_log, ansi_mode=self.ansi_mode, over_handler=main_handler
297
+ )
298
+ self.upd = main_handler.upd
299
+ root_logger.setLevel(self.level)
300
+ if loginfo is None:
301
+ # only do this the first time
302
+ # TODO: fix this clumsy hack, some kind of stackable state?
303
+ main_handler.setFormatter(PfxFormatter(self.format))
304
+ if self.supplant_root_logger:
305
+ root_logger.handlers.pop(0)
306
+ root_logger.addHandler(main_handler)
307
+
308
+ if 'TDUMP' in self.flags:
309
+ # do a thread dump to the main_log on SIGHUP
310
+ # pylint: disable=import-outside-toplevel
311
+ import signal
312
+ from cs.debug import thread_dump
313
+
314
+ # pylint: disable=unused-argument
315
+ def handler(sig, frame):
316
+ thread_dump(None, self.main_log)
317
+
318
+ signal.signal(signal.SIGHUP, handler)
319
+
320
+ def setup_logging(cmd_name=None, **kw):
321
+ ''' Prepare a `LoggingState` and return it.
322
+ It is also available as the global `cs.logutils.loginfo`.
323
+ Side-effect: sets `cs.pfx.cmd` to this value.
324
+ '''
325
+ global loginfo
326
+ if loginfo is None:
327
+ kw.setdefault('cmd', cmd_name or kw.get('cmd'))
328
+ logstate = LoggingState(**kw)
329
+ logstate.apply()
330
+ loginfo = logstate
331
+ cs.pfx.cmd = kw['cmd']
332
+ else:
333
+ # just amend the current LoggingState
334
+ loginfo.__dict__.update(**kw)
335
+ return loginfo
336
+
337
+ class PfxFormatter(Formatter):
338
+ ''' A Formatter subclass that has access to the program's `cmd` and `Pfx` state.
339
+ '''
340
+
341
+ @fmtdoc
342
+ def __init__(self, fmt=None, datefmt=None, cmd=None):
343
+ ''' Initialise the `PfxFormatter`.
344
+
345
+ Parameters:
346
+ * `fmt`: format template,
347
+ default from `DEFAULT_PFX_FORMAT` `{DEFAULT_PFX_FORMAT!r}`.
348
+ Passed through to `Formatter.__init__`.
349
+ * `datefmt`:
350
+ Passed through to `Formatter.__init__`.
351
+ * `cmd`: the "command prefix" made available to format strings.
352
+ If not set, `cs.pfx.cmd` is presented.
353
+ '''
354
+ if fmt is None:
355
+ fmt = DEFAULT_PFX_FORMAT
356
+ self.cmd = cmd
357
+ Formatter.__init__(self, fmt=fmt, datefmt=datefmt)
358
+
359
+ def format(self, record):
360
+ ''' Set `record.cmd` and `record.pfx`
361
+ to the global `cmd` and `Pfx` context prefix respectively,
362
+ then call `Formatter.format`.
363
+ '''
364
+ record.cmd = self.cmd if self.cmd else cs.pfx.cmd
365
+ record.pfx = Pfx._state.prefix
366
+ try:
367
+ s = Formatter.format(self, record)
368
+ except TypeError as e:
369
+ XP(
370
+ "cs.logutils: PfxFormatter.format: record=%r, self=%s: %s", record,
371
+ self, e
372
+ )
373
+ raise
374
+ record.message = s
375
+ return s
376
+
377
+ @staticmethod
378
+ def patch_formatter(formatter):
379
+ ''' Monkey patch an existing `Formatter` instance
380
+ with a `format` method which prepends the current `Pfx` prefix.
381
+ '''
382
+ if isinstance(formatter, PfxFormatter):
383
+ return
384
+ try:
385
+ formatter.PfxFormatter__monkey_patched
386
+ except AttributeError:
387
+ old_format = formatter.format
388
+
389
+ def new_format(record):
390
+ ''' Call the former `formatter.format` method
391
+ and prepend the current `Pfx` prefix to the start.
392
+ '''
393
+ ##msg0 = record.msg
394
+ ##args0 = record.args
395
+ cur_pfx = Pfx._state.prefix
396
+ if not cur_pfx:
397
+ return old_format(record)
398
+ if not isinstance(record.args, tuple):
399
+ # TODO: dict support
400
+ return old_format(record)
401
+ if record.args:
402
+ new_msg = '%s' + str(record.msg)
403
+ new_args = (cur_pfx + cs.pfx.DEFAULT_SEPARATOR,) + tuple(record.args)
404
+ else:
405
+ new_msg = cur_pfx + cs.pfx.DEFAULT_SEPARATOR + str(record.msg)
406
+ new_args = record.args
407
+ try:
408
+ with stackattrs(record, msg=new_msg, args=new_args):
409
+ return old_format(record)
410
+ except Exception: # pylint: disable=broad-except
411
+ # unsupported in some way, fall back to the original
412
+ # and lose the prefix
413
+ return old_format(record)
414
+
415
+ formatter.format = new_format
416
+ formatter.PfxFormatter__monkey_patched = True
417
+
418
+ # pylint: disable=too-many-branches,too-many-statements,redefined-outer-name
419
+ def infer_logging_level(env_debug=None, environ=None, verbose=None):
420
+ ''' Infer a logging level from the `env_debug`, which by default
421
+ comes from the environment variable `$DEBUG`.
422
+
423
+ Usually default to `logging.WARNING`, but if `sys.stderr` is a terminal,
424
+ default to `logging.INFO`.
425
+
426
+ Parse the environment variable `$DEBUG` as a comma separated
427
+ list of flags.
428
+
429
+ Examine the in sequence flags to affect the logging level:
430
+ * numeric < 1: `logging.WARNING`
431
+ * numeric >= 1 and < 2: `logging.INFO`
432
+ * numeric >= 2: `logging.DEBUG`
433
+ * `"DEBUG"`: `logging.DEBUG`
434
+ * `"STATUS"`: `STATUS`
435
+ * `"INFO"`: `logging.INFO`
436
+ * `"TRACK"`: `TRACK`
437
+ * `"WARNING"`: `logging.WARNING`
438
+ * `"ERROR"`: `logging.ERROR`
439
+
440
+ Return an object with the following attributes:
441
+ * `.level`: A logging level.
442
+ * `.flags`: All the words from `$DEBUG` as separated by commas and uppercased.
443
+ '''
444
+ if env_debug is None:
445
+ if environ is None:
446
+ environ = os.environ
447
+ env_debug = environ.get('DEBUG', '')
448
+ if verbose is None:
449
+ if sys.stderr.isatty():
450
+ level = TRACK
451
+ else:
452
+ level = logging.WARNING
453
+ elif verbose:
454
+ level = logging.VERBOSE
455
+ else:
456
+ level = logging.WARNING
457
+ flags = []
458
+ for flag in env_debug.split(','):
459
+ flag = flag.strip()
460
+ if not flag:
461
+ continue
462
+ if flag.isdigit():
463
+ flag_level = int(flag)
464
+ if flag_level < 1:
465
+ level = logging.WARNING
466
+ elif flag_level >= 2:
467
+ level = logging.DEBUG
468
+ else:
469
+ level = logging.INFO
470
+ elif flag[0].islower() and is_dotted_identifier(flag):
471
+ # modulename - now honoured by cs.debug, not this
472
+ pass
473
+ elif ':' in flag:
474
+ # module:funcname - now honoured by cs.debug, not this
475
+ pass
476
+ else:
477
+ uc_flag = flag.upper()
478
+ flags.append(uc_flag)
479
+ if uc_flag == 'DEBUG':
480
+ level = logging.DEBUG
481
+ elif uc_flag == 'STATUS':
482
+ level = STATUS
483
+ elif uc_flag == 'INFO':
484
+ level = logging.INFO
485
+ elif uc_flag == 'TRACK':
486
+ level = TRACK
487
+ # pylint: disable=consider-using-in
488
+ elif uc_flag == 'WARN' or uc_flag == 'WARNING':
489
+ level = logging.WARNING
490
+ elif uc_flag == 'ERROR':
491
+ level = logging.ERROR
492
+ return NS(
493
+ level=level,
494
+ flags=flags,
495
+ )
496
+
497
+ def D(msg, *args):
498
+ ''' Print formatted debug string straight to `sys.stderr` if
499
+ `D_mode` is true, bypassing the logging modules entirely.
500
+ A quick'n'dirty debug tool.
501
+ '''
502
+ # pylint: disable=global-statement
503
+ global D_mode
504
+ if D_mode:
505
+ XP(msg, *args)
506
+
507
+ # pylint: disable=too-many-arguments,redefined-builtin
508
+ def add_logfile(
509
+ filename,
510
+ logger=None,
511
+ mode='a',
512
+ encoding=None,
513
+ delay=False,
514
+ format=None,
515
+ no_prefix=False
516
+ ):
517
+ ''' Add a `FileHandler` logging to the specified `filename`;
518
+ return the chosen logger and the new handler.
519
+
520
+ Parameters:
521
+ * `logger`: if supplied and not `None`, add the `FileHandler` to that
522
+ `Logger`, otherwise to the root Logger. If `logger` is a string, call
523
+ `logging.getLogger(logger)` to obtain the logger.
524
+ * `mode`, `encoding` and `delay`: passed to the `FileHandler`
525
+ initialiser.
526
+ * `format`: used to override the handler's default format.
527
+ * `no_prefix`: if true, do not put the `Pfx` context onto the front of the message.
528
+ '''
529
+ if logger is None:
530
+ logger = logging.getLogger()
531
+ elif isinstance(logger, str):
532
+ logger = logging.getLogger(logger)
533
+ handler = logging.FileHandler(filename, mode, encoding, delay)
534
+ if no_prefix:
535
+ if format is None:
536
+ format = DEFAULT_BASE_FORMAT
537
+ formatter = Formatter(format)
538
+ else:
539
+ formatter = PfxFormatter(format)
540
+ handler.setFormatter(formatter)
541
+ logger.addHandler(handler)
542
+ return logger, handler
543
+
544
+ logTo = add_logfile
545
+
546
+ @contextmanager
547
+ def with_log(filename, **kw):
548
+ ''' Context manager to add a `Logger` to the output logs temporarily.
549
+ '''
550
+ logger, handler = add_logfile(filename, **kw)
551
+ try:
552
+ yield logger, handler
553
+ finally:
554
+ logger.removeHandler(handler)
555
+
556
+ class NullHandler(logging.Handler):
557
+ ''' A `Handler` which discards its requests.
558
+ '''
559
+
560
+ def emit(self, record):
561
+ ''' Discard the log record.
562
+ '''
563
+
564
+ __logExLock = Lock()
565
+
566
+ def logException(exc_type, exc_value, exc_tb):
567
+ ''' Replacement for `sys.excepthook` that reports via the `cs.logutils`
568
+ logging wrappers.
569
+ '''
570
+ with __logExLock:
571
+ curhook = sys.excepthook
572
+ sys.excepthook = sys.__excepthook__
573
+ exception("EXCEPTION: %s:%s" % (exc_type, exc_value))
574
+ for line in traceback.format_exception(exc_type, exc_value, exc_tb):
575
+ exception("EXCEPTION> " + line)
576
+ sys.excepthook = curhook
577
+
578
+ # Logger public functions
579
+ def exception(msg, *args, **kwargs):
580
+ ''' Emit an exception log with the current `Pfx` prefix.
581
+ '''
582
+ Pfx._state.cur.exception(msg, *args, **kwargs)
583
+
584
+ @logging_wrapper
585
+ def log(level, msg, *args, **kwargs):
586
+ ''' Emit a log at the specified level with the current `Pfx` prefix.
587
+ '''
588
+ Pfx._state.cur.log(level, msg, *args, **kwargs)
589
+
590
+ @logging_wrapper(stacklevel_increment=1)
591
+ def debug(msg, *args, **kwargs):
592
+ ''' Emit a log at `logging.DEBUG` level with the current `Pfx` prefix.
593
+ '''
594
+ log(logging.DEBUG, msg, *args, **kwargs)
595
+
596
+ @logging_wrapper(stacklevel_increment=1)
597
+ def info(msg, *args, **kwargs):
598
+ ''' Emit a log at `logging.INFO` level with the current `Pfx` prefix.
599
+ '''
600
+ log(logging.INFO, msg, *args, **kwargs)
601
+
602
+ @logging_wrapper(stacklevel_increment=1)
603
+ def status(msg, *args, **kwargs):
604
+ ''' Emit a log at `STATUS` level with the current `Pfx` prefix.
605
+ '''
606
+ log(STATUS, msg, *args, **kwargs)
607
+
608
+ @logging_wrapper(stacklevel_increment=1)
609
+ def track(msg, *args, **kwargs):
610
+ ''' Emit a log at `TRACK` level with the current `Pfx` prefix.
611
+ '''
612
+ log(TRACK, msg, *args, **kwargs)
613
+
614
+ @logging_wrapper(stacklevel_increment=1)
615
+ def quiet(msg, *args, **kwargs):
616
+ ''' Emit a log at `QUIET` level with the current `Pfx` prefix.
617
+ '''
618
+ log(QUIET, msg, *args, **kwargs)
619
+
620
+ @logging_wrapper(stacklevel_increment=1)
621
+ def verbose(msg, *args, **kwargs):
622
+ ''' Emit a log at `VERBOSE` level with the current `Pfx` prefix.
623
+ '''
624
+ log(VERBOSE, msg, *args, **kwargs)
625
+
626
+ @logging_wrapper(stacklevel_increment=1)
627
+ def ifverbose(is_verbose, msg, *args, **kwargs):
628
+ ''' Conditionally log a message.
629
+
630
+ If `is_verbose` is `None`, log at `VERBOSE` level and rely on the logging setup.
631
+ Otherwise, if `is_verbose` is true, log at `INFO` level.
632
+ '''
633
+ if is_verbose is None:
634
+ # emit at VERBOSE level, use the logging handler levels to emit or not
635
+ verbose(msg, *args, **kwargs)
636
+ elif is_verbose:
637
+ # emit at INFO level
638
+ info(msg, *args, **kwargs)
639
+
640
+ @logging_wrapper(stacklevel_increment=1)
641
+ def warning(msg, *args, **kwargs):
642
+ ''' Emit a log at `logging.WARNING` level with the current `Pfx` prefix.
643
+ '''
644
+ log(logging.WARNING, msg, *args, **kwargs)
645
+
646
+ @logging_wrapper(stacklevel_increment=1)
647
+ def error(msg, *args, **kwargs):
648
+ ''' Emit a log at `logging.ERROR` level with the current `Pfx` prefix.
649
+ '''
650
+ log(logging.ERROR, msg, *args, **kwargs)
651
+
652
+ @logging_wrapper(stacklevel_increment=1)
653
+ def critical(msg, *args, **kwargs):
654
+ ''' Emit a log at `logging.CRITICAL` level with the current `Pfx` prefix.
655
+ '''
656
+ log(logging.CRITICAL, msg, *args, **kwargs)
657
+
658
+ @logging_wrapper(stacklevel_increment=1)
659
+ def trace(msg, *args, **kwargs):
660
+ ''' Emit a log message at `loginfo.trace_level` with the current `Pfx` prefix.
661
+ '''
662
+ log(loginfo.trace_level, msg, *args, **kwargs)
663
+
664
+ @logging_wrapper(stacklevel_increment=1)
665
+ def upd(msg, *args, **kwargs):
666
+ ''' If we're using an `UpdHandler`,
667
+ update the status line otherwise write an info message.
668
+
669
+ Note that this calls `Upd.out` directly with `msg%args`
670
+ and thus does not include the current `Pfx` prefix.
671
+ You may well want to use the `status()` function instead.
672
+ '''
673
+ _upd = loginfo.upd
674
+ if _upd:
675
+ _upd.out(msg, *args)
676
+ else:
677
+ info(msg, *args, **kwargs)
678
+
679
+ # pylint: disable=too-many-instance-attributes
680
+ class LogTime(object):
681
+ ''' LogTime is a context manager that logs the elapsed time of the enclosed
682
+ code. After the run, the field .elapsed contains the elapsed time in
683
+ seconds.
684
+ '''
685
+
686
+ def __init__(self, tag, *args, **kwargs):
687
+ ''' Set up a LogTime.
688
+
689
+ Parameters:
690
+ * `tag`: label included at the start of the log entry
691
+ * `args`: optional array; if not empty `args` is applied to
692
+ `tag` with `%`
693
+ * `level`: keyword argument specifying a log level for a
694
+ default log entry, default `logging.INFO`
695
+ * `threshold`: keyword argument specifying minimum time to
696
+ cause a log, default None (no minimum)
697
+ * `warning_level`: keyword argument specifying the log level
698
+ for a warning log entry, default `logging.WARNING`
699
+ * `warning_threshold`: keyword argument specifying a time
700
+ which raises the log level to `warning_level`
701
+ '''
702
+ threshold = kwargs.pop('threshold', 1.0)
703
+ level = kwargs.pop('level', logging.INFO)
704
+ warning_threshold = kwargs.pop('warning_threshold', None)
705
+ warning_level = kwargs.pop('warning_level', logging.WARNING)
706
+ self.tag = tag
707
+ self.tag_args = args
708
+ self.threshold = threshold
709
+ self.level = level
710
+ self.warning_threshold = warning_threshold
711
+ self.warning_level = warning_level
712
+ self.start = None
713
+ self.end = None
714
+ self.elapsed = None
715
+
716
+ def __enter__(self):
717
+ self.start = time.time()
718
+ return self
719
+
720
+ def __exit__(self, *_):
721
+ now = self.end = time.time()
722
+ elapsed = self.elapsed = now - self.start
723
+ if self.threshold is not None and elapsed >= self.threshold:
724
+ level = self.level
725
+ if self.warning_threshold is not None and elapsed >= self.warning_threshold:
726
+ level = self.warning_level
727
+ tag = self.tag
728
+ if self.tag_args:
729
+ tag = tag % self.tag_args
730
+ log(level, "%s: ELAPSED %5.3fs" % (tag, elapsed))
731
+ return False
732
+
733
+ class UpdHandler(StreamHandler):
734
+ ''' A `StreamHandler` subclass whose `.emit` method
735
+ uses a `cs.upd.Upd` for transcription.
736
+ '''
737
+
738
+ def __init__(
739
+ self, strm=None, upd_level=None, ansi_mode=None, over_handler=None
740
+ ):
741
+ ''' Initialise the `UpdHandler`.
742
+
743
+ Parameters:
744
+ * `strm`: the output stream, default `sys.stderr`.
745
+ * `upd_level`: the magic logging level which updates the status line
746
+ via `Upd`. Default: `STATUS`.
747
+ * `ansi_mode`: if `None`, set from `strm.isatty()`.
748
+ A true value causes the handler to colour certain logging levels
749
+ using ANSI terminal sequences.
750
+ '''
751
+ from cs.upd import Upd # pylint: disable=import-outside-toplevel
752
+ if strm is None:
753
+ strm = sys.stderr
754
+ if upd_level is None:
755
+ upd_level = STATUS
756
+ if ansi_mode is None:
757
+ ansi_mode = strm.isatty()
758
+ StreamHandler.__init__(self, strm)
759
+ self.upd = Upd(strm)
760
+ self.upd_level = upd_level
761
+ self.ansi_mode = ansi_mode
762
+ self.over_handler = over_handler
763
+ self.__lock = Lock()
764
+
765
+ def emit(self, logrec):
766
+ ''' Emit a `LogRecord` `logrec`.
767
+
768
+ For the log level `self.upd_level` update the status line.
769
+ For other levels write a distinct line
770
+ to the output stream, possibly colourised.
771
+ '''
772
+ upd = self.upd
773
+ if logrec.levelno == self.upd_level:
774
+ line = self.format(logrec)
775
+ with self.__lock:
776
+ upd.out(line)
777
+ else:
778
+ if self.ansi_mode:
779
+ if logrec.levelno >= logging.ERROR:
780
+ logrec.msg = colourise(
781
+ colourise(logrec.msg, 'white'), 'redbg', 'blackbg'
782
+ )
783
+ elif logrec.levelno >= logging.WARNING:
784
+ logrec.msg = colourise(logrec.msg, 'yellow')
785
+ line = self.format(logrec)
786
+ with self.__lock:
787
+ if upd.disabled:
788
+ self.over_handler.emit(logrec)
789
+ else:
790
+ upd.nl(line)
791
+
792
+ def flush(self):
793
+ ''' Flush the update status.
794
+ '''
795
+ return self.upd.flush()
796
+
797
+ if __name__ == '__main__':
798
+
799
+ @logging_wrapper
800
+ def test_warning(msg, *a, **kw):
801
+ 'test function for warning'
802
+ warning(msg, *a, **kw)
803
+
804
+ setup_logging(
805
+ sys.argv[0],
806
+ format='%(pfx)s: from %(funcName)s:%(filename)s:%(lineno)d: %(message)s'
807
+ )
808
+ test_warning("test warning")
@@ -0,0 +1,479 @@
1
+ Metadata-Version: 2.4
2
+ Name: cs-logutils
3
+ Version: 20250306
4
+ Summary: Logging convenience routines.
5
+ Keywords: python2,python3
6
+ Author-email: Cameron Simpson <cs@cskk.id.au>
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 2
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
16
+ Requires-Dist: cs.ansi_colour>=20200729
17
+ Requires-Dist: cs.context>=20210306
18
+ Requires-Dist: cs.deco>=20250306
19
+ Requires-Dist: cs.lex>=20250103
20
+ Requires-Dist: cs.pfx>=20241208
21
+ Requires-Dist: cs.py.func>=20240630
22
+ Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
23
+ Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
24
+ Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
25
+ Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/logutils.py
26
+
27
+ Logging convenience routines.
28
+
29
+ *Latest release 20250306*:
30
+ ansi_mode: use white-on-red instead of red, easier to read - really needs some kind of palette from the environment and a light/dark mode.
31
+
32
+ The logging package is very useful, but a little painful to use.
33
+ This package provides low impact logging setup and some extremely
34
+ useful if unconventional context hooks for logging.
35
+
36
+ The default logging verbosity output format has different defaults
37
+ based on whether an output log file is a tty
38
+ and whether the environment variable `$DEBUG` is set, and to what.
39
+
40
+ On terminals warnings and errors get ANSI colouring.
41
+
42
+ A mode is available that uses `cs.upd` for certain log levels.
43
+
44
+ Log messages dispatched via `warning` and friends from this module
45
+ are automatically prefixed with the current `cs.pfx` prefix string,
46
+ providing automatic message context.
47
+
48
+ Some examples:
49
+ --------------
50
+
51
+ Program initialisation:
52
+
53
+ from cs.logutils import setup_logging
54
+
55
+ def main(argv):
56
+ cmd = os.path.basename(argv.pop(0))
57
+ setup_logging(cmd)
58
+
59
+ Basic logging from anywhere:
60
+
61
+ from cs.logutils import info, warning, error
62
+ [...]
63
+ def some_function(...):
64
+ [...]
65
+ error("nastiness found! bad value=%r", bad_value)
66
+
67
+ ## <a name="add_logfile"></a>`add_logfile(filename, logger=None, mode='a', encoding=None, delay=False, format=None, no_prefix=False)`
68
+
69
+ Add a `FileHandler` logging to the specified `filename`;
70
+ return the chosen logger and the new handler.
71
+
72
+ Parameters:
73
+ * `logger`: if supplied and not `None`, add the `FileHandler` to that
74
+ `Logger`, otherwise to the root Logger. If `logger` is a string, call
75
+ `logging.getLogger(logger)` to obtain the logger.
76
+ * `mode`, `encoding` and `delay`: passed to the `FileHandler`
77
+ initialiser.
78
+ * `format`: used to override the handler's default format.
79
+ * `no_prefix`: if true, do not put the `Pfx` context onto the front of the message.
80
+
81
+ ## <a name="critical"></a>`critical(msg, *args, **kwargs)`
82
+
83
+ Emit a log at `logging.CRITICAL` level with the current `Pfx` prefix.
84
+
85
+ ## <a name="D"></a>`D(msg, *args)`
86
+
87
+ Print formatted debug string straight to `sys.stderr` if
88
+ `D_mode` is true, bypassing the logging modules entirely.
89
+ A quick'n'dirty debug tool.
90
+
91
+ ## <a name="debug"></a>`debug(msg, *args, **kwargs)`
92
+
93
+ Emit a log at `logging.DEBUG` level with the current `Pfx` prefix.
94
+
95
+ ## <a name="error"></a>`error(msg, *args, **kwargs)`
96
+
97
+ Emit a log at `logging.ERROR` level with the current `Pfx` prefix.
98
+
99
+ ## <a name="exception"></a>`exception(msg, *args, **kwargs)`
100
+
101
+ Emit an exception log with the current `Pfx` prefix.
102
+
103
+ ## <a name="ifdebug"></a>`ifdebug()`
104
+
105
+ Test the `loginfo.level` against `logging.DEBUG`.
106
+
107
+ ## <a name="ifverbose"></a>`ifverbose(is_verbose, msg, *args, **kwargs)`
108
+
109
+ Conditionally log a message.
110
+
111
+ If `is_verbose` is `None`, log at `VERBOSE` level and rely on the logging setup.
112
+ Otherwise, if `is_verbose` is true, log at `INFO` level.
113
+
114
+ ## <a name="infer_logging_level"></a>`infer_logging_level(env_debug=None, environ=None, verbose=None)`
115
+
116
+ Infer a logging level from the `env_debug`, which by default
117
+ comes from the environment variable `$DEBUG`.
118
+
119
+ Usually default to `logging.WARNING`, but if `sys.stderr` is a terminal,
120
+ default to `logging.INFO`.
121
+
122
+ Parse the environment variable `$DEBUG` as a comma separated
123
+ list of flags.
124
+
125
+ Examine the in sequence flags to affect the logging level:
126
+ * numeric < 1: `logging.WARNING`
127
+ * numeric >= 1 and < 2: `logging.INFO`
128
+ * numeric >= 2: `logging.DEBUG`
129
+ * `"DEBUG"`: `logging.DEBUG`
130
+ * `"STATUS"`: `STATUS`
131
+ * `"INFO"`: `logging.INFO`
132
+ * `"TRACK"`: `TRACK`
133
+ * `"WARNING"`: `logging.WARNING`
134
+ * `"ERROR"`: `logging.ERROR`
135
+
136
+ Return an object with the following attributes:
137
+ * `.level`: A logging level.
138
+ * `.flags`: All the words from `$DEBUG` as separated by commas and uppercased.
139
+
140
+ ## <a name="info"></a>`info(msg, *args, **kwargs)`
141
+
142
+ Emit a log at `logging.INFO` level with the current `Pfx` prefix.
143
+
144
+ ## <a name="log"></a>`log(level, msg, *args, **kwargs)`
145
+
146
+ Emit a log at the specified level with the current `Pfx` prefix.
147
+
148
+ ## <a name="logException"></a>`logException(exc_type, exc_value, exc_tb)`
149
+
150
+ Replacement for `sys.excepthook` that reports via the `cs.logutils`
151
+ logging wrappers.
152
+
153
+ ## <a name="LoggingState"></a>Class `LoggingState(types.SimpleNamespace)`
154
+
155
+ A logging setup arranged for conventional UNIX command line use.
156
+
157
+ *`LoggingState.__init__(self, cmd=None, main_log=None, format=None, level=None, flags=None, upd_mode=None, ansi_mode=None, trace_mode=None, verbose=None, supplant_root_logger=False)`*:
158
+ Prepare the `LoggingState` for conventional UNIX command
159
+ line error messaging.
160
+
161
+ Amongst other things, the default logger now includes
162
+ the `cs.pfx` prefix in the message.
163
+
164
+ This function runs in two modes:
165
+ - if logging has not been set up, it sets up a root logger
166
+ - if the root logger already has handlers,
167
+ monkey patch the first handler's formatter to prefix the `cs.pfx` state
168
+
169
+ Parameters:
170
+ * `cmd`: program name, default from `basename(sys.argv[0])`.
171
+ * `main_log`: default logging system.
172
+ If `None`, the main log will go to `sys.stderr`;
173
+ if `main_log` is a string, is it used as a filename to
174
+ open in append mode;
175
+ otherwise main_log should be a stream suitable
176
+ for use with `logging.StreamHandler()`.
177
+ The resulting log handler is added to the `logging` root logger.
178
+ * `format`: the message format for `main_log`.
179
+ If `None`, use `DEFAULT_PFX_FORMAT_TTY`
180
+ when `main_log` is a tty or FIFO,
181
+ otherwise `DEFAULT_PFX_FORMAT`.
182
+ * `level`: `main_log` logging level.
183
+ If `None`, infer a level from the environment
184
+ using `infer_logging_level()`.
185
+ * `flags`: a string containing debugging flags separated by commas.
186
+ If `None`, infer the flags from the environment using
187
+ `infer_logging_level()`.
188
+ The following flags have meaning:
189
+ `D`: set cs.logutils.D_mode to True;
190
+ `TDUMP`: attach a signal handler to SIGHUP to do a thread stack dump;
191
+ `TRACE`: enable various noisy tracing facilities;
192
+ `UPD`, `NOUPD`: set the default for `upd_mode` to True or False respectively.
193
+ * `upd_mode`: a Boolean to activate cs.upd as the `main_log` method;
194
+ if `None`, set it to `True` if `flags` contains 'UPD',
195
+ otherwise to `False` if `flags` contains 'NOUPD',
196
+ otherwise set it from `main_log.isatty()`.
197
+ A true value causes the root logger to use `cs.upd` for logging.
198
+ * `ansi_mode`: if `None`,
199
+ set it from `main_log.isatty() and not cs.colourise.env_no_color()`,
200
+ which thus honours the `$NO_COLOR` environment variable
201
+ (see https://no-color.org/ for the convention).
202
+ A true value causes the root logger to colour certain logging levels
203
+ using ANSI terminal sequences (currently only if `cs.upd` is used).
204
+ * `trace_mode`: if `None`, set it according to the presence of
205
+ 'TRACE' in flags. Otherwise if `trace_mode` is true, set the
206
+ global `loginfo.trace_level` to `loginfo.level`; otherwise it defaults
207
+ to `logging.DEBUG`.
208
+ * `verbose`: if `None`, then if stderr is a tty then the log
209
+ level is `INFO` otherwise `WARNING`. Otherwise, if `verbose` is
210
+ true then the log level is `INFO` otherwise `WARNING`.
211
+
212
+ *`LoggingState.apply(self)`*:
213
+ Apply this `LoggingState` to the current logging setup.
214
+
215
+ ## <a name="loginfo"></a>`loginfo = LoggingState(main_log=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, level=25, verbose=None, trace_level=10, flags=[], cmd='cs-release', upd=<cs.upd.Upd object at 0x1061f03b0>, upd_mode=True, ansi_mode=True, format='%(pfx)s: %(message)s', supplant_root_logger=False)`
216
+
217
+ A logging setup arranged for conventional UNIX command line use.
218
+
219
+ ## <a name="LogTime"></a>Class `LogTime`
220
+
221
+ LogTime is a context manager that logs the elapsed time of the enclosed
222
+ code. After the run, the field .elapsed contains the elapsed time in
223
+ seconds.
224
+
225
+ *`LogTime.__init__(self, tag, *args, **kwargs)`*:
226
+ Set up a LogTime.
227
+
228
+ Parameters:
229
+ * `tag`: label included at the start of the log entry
230
+ * `args`: optional array; if not empty `args` is applied to
231
+ `tag` with `%`
232
+ * `level`: keyword argument specifying a log level for a
233
+ default log entry, default `logging.INFO`
234
+ * `threshold`: keyword argument specifying minimum time to
235
+ cause a log, default None (no minimum)
236
+ * `warning_level`: keyword argument specifying the log level
237
+ for a warning log entry, default `logging.WARNING`
238
+ * `warning_threshold`: keyword argument specifying a time
239
+ which raises the log level to `warning_level`
240
+
241
+ ## <a name="logTo"></a>`logTo(filename, logger=None, mode='a', encoding=None, delay=False, format=None, no_prefix=False)`
242
+
243
+ Add a `FileHandler` logging to the specified `filename`;
244
+ return the chosen logger and the new handler.
245
+
246
+ Parameters:
247
+ * `logger`: if supplied and not `None`, add the `FileHandler` to that
248
+ `Logger`, otherwise to the root Logger. If `logger` is a string, call
249
+ `logging.getLogger(logger)` to obtain the logger.
250
+ * `mode`, `encoding` and `delay`: passed to the `FileHandler`
251
+ initialiser.
252
+ * `format`: used to override the handler's default format.
253
+ * `no_prefix`: if true, do not put the `Pfx` context onto the front of the message.
254
+
255
+ ## <a name="NullHandler"></a>Class `NullHandler(logging.Handler)`
256
+
257
+ A `Handler` which discards its requests.
258
+
259
+ *`NullHandler.emit(self, record)`*:
260
+ Discard the log record.
261
+
262
+ ## <a name="PfxFormatter"></a>Class `PfxFormatter(logging.Formatter)`
263
+
264
+ A Formatter subclass that has access to the program's `cmd` and `Pfx` state.
265
+
266
+ *`PfxFormatter.__init__(self, fmt=None, datefmt=None, cmd=None)`*:
267
+ Initialise the `PfxFormatter`.
268
+
269
+ Parameters:
270
+ * `fmt`: format template,
271
+ default from `DEFAULT_PFX_FORMAT` `'%(asctime)s %(levelname)s %(pfx)s: %(message)s'`.
272
+ Passed through to `Formatter.__init__`.
273
+ * `datefmt`:
274
+ Passed through to `Formatter.__init__`.
275
+ * `cmd`: the "command prefix" made available to format strings.
276
+ If not set, `cs.pfx.cmd` is presented.
277
+
278
+ *`PfxFormatter.format(self, record)`*:
279
+ Set `record.cmd` and `record.pfx`
280
+ to the global `cmd` and `Pfx` context prefix respectively,
281
+ then call `Formatter.format`.
282
+
283
+ *`PfxFormatter.patch_formatter(formatter)`*:
284
+ Monkey patch an existing `Formatter` instance
285
+ with a `format` method which prepends the current `Pfx` prefix.
286
+
287
+ ## <a name="quiet"></a>`quiet(msg, *args, **kwargs)`
288
+
289
+ Emit a log at `QUIET` level with the current `Pfx` prefix.
290
+
291
+ ## <a name="setup_logging"></a>`setup_logging(cmd_name=None, **kw)`
292
+
293
+ Prepare a `LoggingState` and return it.
294
+ It is also available as the global `cs.logutils.loginfo`.
295
+ Side-effect: sets `cs.pfx.cmd` to this value.
296
+
297
+ ## <a name="status"></a>`status(msg, *args, **kwargs)`
298
+
299
+ Emit a log at `STATUS` level with the current `Pfx` prefix.
300
+
301
+ ## <a name="trace"></a>`trace(msg, *args, **kwargs)`
302
+
303
+ Emit a log message at `loginfo.trace_level` with the current `Pfx` prefix.
304
+
305
+ ## <a name="track"></a>`track(msg, *args, **kwargs)`
306
+
307
+ Emit a log at `TRACK` level with the current `Pfx` prefix.
308
+
309
+ ## <a name="upd"></a>`upd(msg, *args, **kwargs)`
310
+
311
+ If we're using an `UpdHandler`,
312
+ update the status line otherwise write an info message.
313
+
314
+ Note that this calls `Upd.out` directly with `msg%args`
315
+ and thus does not include the current `Pfx` prefix.
316
+ You may well want to use the `status()` function instead.
317
+
318
+ ## <a name="UpdHandler"></a>Class `UpdHandler(logging.StreamHandler)`
319
+
320
+ A `StreamHandler` subclass whose `.emit` method
321
+ uses a `cs.upd.Upd` for transcription.
322
+
323
+ *`UpdHandler.__init__(self, strm=None, upd_level=None, ansi_mode=None, over_handler=None)`*:
324
+ Initialise the `UpdHandler`.
325
+
326
+ Parameters:
327
+ * `strm`: the output stream, default `sys.stderr`.
328
+ * `upd_level`: the magic logging level which updates the status line
329
+ via `Upd`. Default: `STATUS`.
330
+ * `ansi_mode`: if `None`, set from `strm.isatty()`.
331
+ A true value causes the handler to colour certain logging levels
332
+ using ANSI terminal sequences.
333
+
334
+ *`UpdHandler.emit(self, logrec)`*:
335
+ Emit a `LogRecord` `logrec`.
336
+
337
+ For the log level `self.upd_level` update the status line.
338
+ For other levels write a distinct line
339
+ to the output stream, possibly colourised.
340
+
341
+ *`UpdHandler.flush(self)`*:
342
+ Flush the update status.
343
+
344
+ ## <a name="verbose"></a>`verbose(msg, *args, **kwargs)`
345
+
346
+ Emit a log at `VERBOSE` level with the current `Pfx` prefix.
347
+
348
+ ## <a name="warning"></a>`warning(msg, *args, **kwargs)`
349
+
350
+ Emit a log at `logging.WARNING` level with the current `Pfx` prefix.
351
+
352
+ ## <a name="with_log"></a>`with_log(filename, **kw)`
353
+
354
+ Context manager to add a `Logger` to the output logs temporarily.
355
+
356
+ # Release Log
357
+
358
+
359
+
360
+ *Release 20250306*:
361
+ ansi_mode: use white-on-red instead of red, easier to read - really needs some kind of palette from the environment and a light/dark mode.
362
+
363
+ *Release 20241109*:
364
+ * setup_logging: make it work with no arguments.
365
+ * ifdebug: handle loginfo.level=None.
366
+
367
+ *Release 20241007*:
368
+ * setup_logging: just amend the existing loginfo if already set up.
369
+ * Remove the cs.pfx.cmd side effect from LogState.__init__, now in setup_logging.
370
+
371
+ *Release 20240923*:
372
+ setup_logging: accept leading `cmd_name` for backwards compatibility, reported by Lance Cohen.
373
+
374
+ *Release 20240630*:
375
+ * New LoggingState class for the computed log state, split out setup_logging() as a little stub.
376
+ * Drop func_wrap and _ftrace, superceded by cs.debug.trace.
377
+ * infer_logging_level: ignore the module.name and module:function_name $DEBUG values, now done by importing cs.debug.
378
+
379
+ *Release 20230212*:
380
+ Late import of cs.upd at need to avoid import loop.
381
+
382
+ *Release 20220531*:
383
+ PfxFormatter.patch_formatter: notice if record.args is not a tuple and do not try to prefix it (for now).
384
+
385
+ *Release 20220530*:
386
+ * New QUIET log level between TRACK and STATUS, add new quiet() logging call.
387
+ * PfxFormatter.patch_formatter: bugfix handling of record.msg,record.args.
388
+
389
+ *Release 20220315*:
390
+ A bit of a hack to prevent double patching a formatter, as when BaseCommand calls a BaseCommand and other circumstances where setup_logging() gets called more than once.
391
+
392
+ *Release 20220227*:
393
+ * PfxFormatter: new patch_formatter() static method to modify an existing Formatter.
394
+ * setup_logging: just use PfxFormatter.patch_formatter on the first handler's formatter if logging is already set up.
395
+
396
+ *Release 20211208*:
397
+ Docstring update.
398
+
399
+ *Release 20210721*:
400
+ UpdHandler.emit: for newline-emitting messages, fall back to new .over_handler if the Upd is disabled.
401
+
402
+ *Release 20210718*:
403
+ setup_logging: new supplant_root_logger=False parameter to pop the existing handler, typical use supplant_root_logger=sys.stderr.isatty().
404
+
405
+ *Release 20210306*:
406
+ * Default logging level for ttys is now INFO, not STATUS.
407
+ * New VERBOSE level below INFO but above DEBUG.
408
+ * infer_logging_level: if verbose unspecified, logging=WARNING on a tty and TRACK otherwise, else if verbose, level=VERBOSE, otherwise WARNING.
409
+ * Include .verbose in the loginfo.
410
+ * New verbose() and ifverbose().
411
+
412
+ *Release 20201021*:
413
+ * setup_logging: always provide loginfo.upd, being either main_handler.upd if upd_mode otherwise Upd().
414
+ * exception(): plumb keyword arguments.
415
+
416
+ *Release 20200729*:
417
+ setup_logging: honour $NO_COLOR if ansi_mode not specified, per https://no-color.org/
418
+
419
+ *Release 20200613*:
420
+ * LogTime: set .end on exit.
421
+ * UpdHandle.emit: fix message colouring logic.
422
+
423
+ *Release 20200521*:
424
+ setup_logging: include the logger in loginfo (presently always the root logger).
425
+
426
+ *Release 20200519*:
427
+ bugfix setup_logging: apparently a LoggingProxy does not have an encoding
428
+
429
+ *Release 20200518*:
430
+ * Sweeping removal of cs.obj.O, universally supplanted by types.SimpleNamespace.
431
+ * Default to logging level TRACK if stderr is a tty instead of logging.INFO.
432
+ * New ifverbose function with leading `verbose` parameter: if None, log at INFO otherwise if true, log at TRACK, otherwise do not log.
433
+ * BREAKING: remove global logging_level and trace_level variables, put it all in the global loginfo.
434
+ * Make STATUS just below TRACK so that it is above INFO instead of below.
435
+ * New status() function for cs.upd messages.
436
+ * UpdHandler: treat status_level as special, going directly to Upd.out.
437
+ * Improved source line recitation on modern Python.
438
+ * Default level if sys.stderr.isatty() now STATUS, not TRACK.
439
+ * Some fixes for loginfo initialisation and setting cs.pfx.cmd.
440
+
441
+ *Release 20200229*:
442
+ * Update for new Upd.without context manager.
443
+ * setup_logging: default `upd_mode` to `main_log.isatty()`, was previously False.
444
+ * Drop UpdHandler.upd method, shadowed by instance attribute, never used.
445
+
446
+ *Release 20190923*:
447
+ * New `TRACK` constant equal to `logging.INFO+5` to provide a level higher than `INFO`
448
+ * (which seems unreasonably noisy) and lower than `WARNING`
449
+ * warning for tracking salient events.
450
+ * New `track()` function to match.
451
+
452
+ *Release 20190220*:
453
+ Improvements to upd_mode.
454
+
455
+ *Release 20190103*:
456
+ Documentation updates.
457
+
458
+ *Release 20190101*:
459
+ Bugfix for @contextmanager usage.
460
+
461
+ *Release 20171030*:
462
+ Assorted fixes from recent module reshuffle. Other small features and cleanups. Drop a couple of unused functions.
463
+
464
+ *Release 20160828*:
465
+ Use "install_requires" instead of "requires" in DISTINFO.
466
+
467
+ *Release 20160827*:
468
+ * Pfx: import __exit__ handler
469
+ * Preliminary per-module and per-function syntax accepted in $DEBUG envvar.
470
+ * Improvements to X(), add DP() and XP() prefixed flavours.
471
+ * status() function to update terminal status line.
472
+ * New X_via_tty global flag: directs X() to tty instead of sys.stderr.
473
+ * Assorted other minor improvements.
474
+
475
+ *Release 20150118*:
476
+ metadata updates
477
+
478
+ *Release 20150110*:
479
+ Initial PyPI release.
@@ -0,0 +1,4 @@
1
+ cs/logutils.py,sha256=WRTXQw1hvVsmCskdG8IKnZ-1a9WkAgkn3YV-5unqUTU,25566
2
+ cs_logutils-20250306.dist-info/WHEEL,sha256=BXjIu84EnBiZ4HkNUBN93Hamt5EPQMQ6VkF7-VZ_Pu0,100
3
+ cs_logutils-20250306.dist-info/METADATA,sha256=Wsg9cKp77me7LH7zl2Cj5TMGHTXfHAYBWEGhaht4LDw,18399
4
+ cs_logutils-20250306.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.11.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any