-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathalogging.py
executable file
·1868 lines (1532 loc) · 70.2 KB
/
alogging.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
#
# alogging: messaging, logging, and statistics features from sjdUtils.py.
# Fairly close to Python's Logging package (and moving closer),
# but adds color and -v levels like many *nix utilities.
# 2011-12-09 : Written by Steven J. DeRose.
#
# pylint: disable=W1406
#
import sys
import os
import re
import logging
import inspect
from collections import defaultdict
from typing import Iterable, Dict, Any, Union
# Need this to do formatRec() right for it:
try:
from xml.dom.minidom import NamedNodeMap
except ImportError:
class NamedNodeMap(list): pass # Just get it out of the way...
__metadata__ = {
"title" : "alogging",
"description" : "logging additions, for -v levels, stats, formatting, traceback,...",
"rightsHolder" : "Steven J. DeRose",
"creator" : "http://viaf.org/viaf/50334488",
"type" : "http://purl.org/dc/dcmitype/Software",
"language" : "Python 3.7",
"created" : "2011-12-09",
"modified" : "2022-02-17",
"publisher" : "http://github.com/sderose",
"license" : "https://creativecommons.org/licenses/by-sa/3.0/"
}
__version__ = __metadata__["modified"]
descr = """
=Description=
A logging facility, largely compatible with Python 'logging.Logger'.
Derived from `sjdUtils.py`'s messaging methods.
=Extra features compared to `logging.Logger`=
* Traditional *nix-style `-v` verbosity levels. These are implemented by
occupying 'logging' levels from INFO (20) counting down. so no -v is 20;
-v is 19, -v -v is 18, etc. setVerbose(n) takes the -v count, and calls
logging.setLevel(20-n).setLevel.
* Better Unicode and non-printable char handling
* Definable named message types, with their own layouts
* Support for ANSI terminal color
* Controllable indentation levels
* Option to bump counters for any/all messages, making it easy
to keep and report error statistics
* `formatRec()` for nice layout of nested data structures.
=Usage=
import alogging
lg = alogging.ALogger()
...
parser.add_argument(
"--verbose", "-v", action="count", default=0,
help="Add more messages (repeatable).")
...
if (args.verbose): lg.setVerbose(args.verbose)
...
lg.warning(msg)
lg.vMsg(2, msg1, msg2, color="red")
`warning`, `info`, `error`, `fatal`, `exception`, and `critical` work
like in Python `logging.warn()`, except that there is only one context,
and they accept an optional `stat` parameter (see below).
To display a nicely-formatted rendition of a Python data structure:
print(lg.formatRec(myBigDataThing))
==vMsg() and its kin==
(deprecated in favor of info/warning/error/critical)
`vMsg()` takes an integer verbosity level, and only displays a message if the
current verbosity level is at least that high. This is typically controlled
by passing 0 or more `-v` options on a command line. It also allows a 2nd
message parameter, just for flexibility (probably should change to allow any
number).
`vMsg()` is meant for
verbose or informational messages `eMsg()` (now deprecated, use `error()`)
is for errors, while `hMsg` is for headings. They have different formatting
characteristics, which can be changed independently.
The `stat` option may be used to provide the name of a counter to increment.
This can be handy for tracking how many times various messages occur. Separate
messages may increment the same-named stat counter.
The named stats are created as needed.
Other `stat` methods include (see also the more detailed discussion below):
* `getStat("name")`
* `bumpStat("name")`
* `bumpStat("name", incrementAmount)`
* `setStat("name", newCounterValue)`
* `appendStat(self, stat, datum)` -- converts the stat to a list if it isn't
already, and appends `datum` to the list.
* `showStats()`
* `setOption("noMoreStats", True)` -- this will prevent any new stat names
from being accepted (causing an exception on any such attempt).
Verbosity can be set with `setOption("verbose", n)` or `setVerbose(n)`.
==formatRec(self, obj, options=None)==
This will fairly nicely format any (?) Python object as an annotated outline.
It knows about the basic scalar and collection datatypes,
as well as basic numpy and PyTorch vector types.
For objects, it knows to show their non-callable properties.
`options` should be a dict, which may include:
* `obj` -- The data to be displayed.
* `keyWidth` -- Room for dict keys, in trying to line things up. Default: 14.
* `maxDepth` -- Limit nesting of collections to display. Default: 3.
* `maxItems` -- Limit how many items of lists are displayed.
* `maxString` -- Limit number of characters to show for strings.
* `propWidth` -- Allow this much room for object property names.
* `quoteKeys` -- Put quotes around the keys of dicts. Default: False
* `showSize` -- Display len() for collection objects.
* `specials` -- Display callable and "__"-initial members of objects.
* `stopTypes` -- A dict of types or typenames, which are to be displayed
as just present, without all their (non-callable) properties. At the moment,
these are checked by == on the type and the typename, so subclasses must be
explicitly listed to be affected.
==formatPickle(self, path)==
This loads a pickle file, and then runs `formatRec()` on it and returns the
result.
===Layout conventions===
Lists and dictionaries get a header line indicating their type and length,
and the usual delimiters then enclose their members, each on a separate line.
Members that are themselves collections, do the same thing but indented one
more level. List items are prefixed by their index (like "#0:"), and dict
items by their keys (in quotes). Values in dicts and objects are aligned
vertically by padding their keys/names to `keyWidth` or `propWidth` columns.
Object are displayed pretty much like dicts, but using "<<" and ">>" to surround
their members, and putting "." before property names, unlike quoted dict keys.
Callables, and properties beginning with "__", are omitted unless the "specials"
option is set.
numpy and PyTorch vectors are not shown in their entirety, but do display their
dimensionality, length, and item datatype.
==getLoc(startLevel=0, endLevel=0)==
gets a decently-formatted partial or complete stack trace
(not counting internals of `alogging` itself).
==getCaller(self)==
Returns the name of the (direct) caller of `alogging`, using Python `inspect`.
==Notes on speed==
With either this package or Python `logging`,
calling a logging/messaging method does cost
time even if the message doesn't end up getting displayed or saved.
This can be very significant for messages in inner loops.
It can also be significant if evaluating the ''parameters'' being passed
is expensive, since that is done before the logging function is actually
called (so even if the logging function returns instantly the setup time
is still taken).
You can get reduce such overhead by:
* (a) removing or commenting out the calls when no longer needed,
especially in inner loops.
You may want to use a profiler to be sure which ones actually matter.
* (b) putting code under an "if", for example "if (False)",
"if (args.verbose)", or (even better)
"if (__debug__)" (which makes them go away when Python is run with the `-O`
option.
* (c) Use a preprocesser to remove some/all logging statements
for production. See the discussion at
[http://stackoverflow.com/questions/482014].
=General Methods=
* ''setVerbose(v)''
The initial verbosity level can be set as a parameter to the constructor.
This method lets you reset it later. It also calls the Pythong logging
instance's setLevel() method, for 20-v, which is the level used for
info messages by this package (where v should be the number of -v options given).
Otherwise, messages whose first argument's magnitude
is greater than ''v'' are discarded (verbosity levels are perhaps best
thought of as priorities). This means that verbosity runs like `-v` flags in
many *nix programs, but opposite from Python `logging` levels.
''setVerbose(v)'' is just shorthand for ''setOption("verbose", v)''.
* ''getVerbose''()
Return the current verbosity level (see ''setVerbose'').
This is just shorthand for ''getOption("verbose")''.
This is not the same as the Python logging .level value.
* '''findCaller(self)
Returns the name of the function or method that called ''findCaller''().
* ''pline(label, n, width=None, dotfill=0, fillchar='.', denom=None)''
Display a line consisting of ''label'' (padded to ''width''),
and then the data (''n'').
''width'' defaults to the value of the ''plinewidth'' option.
The data is displayed in a type-appropriate way after that.
If ''dotfill'' is greater than 0, then every ''dotfill'''th line will use
''fillchar'' instead of space for padding ''label''. For example,
''dotfill=3'' makes every third line be dot-filled.
If ''denom'' > 0, another column is displayed, showing ''n/denom''.
=='logging'-like messaging methods==
=The following methods are largely compatible with methods in the Python
`logging` package. However, there is no tree of instances, just
an ''ALogger'' object. The format string for each type is kept in
`msg.formats{typename}`, and defaults to ''%(msg}\\n''.
These methods are still experimental.
''kwargs'' can be used to provide a dictionary of names whose
values are filled in to such format strings via ''%(name)''.
This is only useful if you also set a format
string that includes references to them. These names are defined by default:
* '''info(self, msg, *args, **kwargs)'''
Issues an informational message, like Python `logging`.
* '''warning(self, msg, *args, **kwargs)'''
Issues a warning message, like Python `logging`.
* '''error(self, msg, *args, **kwargs)'''
Issues an error message, like Python `logging`.
* '''critical(self, msg, *args, **kwargs)'''
Issues an critical error message, like Python `logging`.
* '''debug(self, msg, *args, **kwargs)'''
Issues a debugging message, like Python `logging`.
* '''log(self, level, msg, *args, **kwargs)'''
Issues an otherwise unspecified log message, like Python `logging`.
* '''exception(self, msg, *args, **kwargs)'''
Issues an exception message, like Python `logging`.
==Additional messaging methods==
* ''eMsg'' ''(level, message1, message2, color, text, stat)''
Specifying a ''level'' less than 0 is fatal.
''Deprecated'', replaced by ''warning''(), ''error''(), and ''fatal''().
* ''hMsg'' ''(level, message1, message2, color, text, stat)''
''Deprecated''. Instead, start another kind of message with "====", which will
generate whitespace and a separator line.
* ''vMsg'' ''(level, message1, message2, color, text, stat)''
The levels of messages here are separate from Logger's implicit
levels for ''warning'', ''error'', etc.
===Treatment of message with `eMsg`, `vMsg`, `hMsg`, etc.===
The `ALogger`-specific messaging methods do the following:
** If the message is not high enough priority compared
to the ''verbose'' setting, the method simply returns.
''Important Note'': All ''vMsg'' messages are
''information''-level as far as ''Logger'' is concerned, and so ''none''
of them will show up unless you're showing such messages. The ''level''
argument to ''vMsg'' is a further filter, to support verbosity levels
as are very common for *nix utilities.
** Converts the message to UTF-8.
** Adds ''prefix'' before ''m1'',
''infix'' between ''m1'' and ''m2'', and/or ''suffix'' after ''m2''.
** If ''func'' is True, inserts the name of the calling function
before ''m1''.
** Colorizes the prefix (including any applicable preprefix, infix,
and indentation whitespace) and ''m1''.
** If ''escape'' is True, passes ''m1'' and ''m2'' through ''showInvisibles'',
which turns control characters, whitespace, etc. to visible representations.
** If ''nLevels'' ''' 0, appends that many levels of stack-trace information.
** Displays the resulting message. It goes to STDERR by default,
but to STDOUT if the ''stdout'' option is set.
These items can be filled in by a message format:
* 'function'
* 'filename'
* 'index'
* 'lineno'
* 'asctime'
* 'type'
* 'msg'
==Statistics-keeping methods==
[OBSOLETE, DEPRECATED]
This package maintains a list of named "statistics". Typically, each
one has just a counter, which you increment by passing the static name
to the `stat"` parameter of any of the messaging calls described in the
next section. You do not need to declare or initialize such statistics
(although you can set them all and then set the `noMoreStats` option
to prevent creating any more).
The statistics can all be printed out (for example, just before your
main program ends). For example:
recnum = 0
for rec in open(foo, 'r').readline():
recnum += 1
if (len(rec) > 999):
lg.error("Line %d too long (%d characters)." % (recnum, len(rec)),
stat="tooLong")
lg.setOption('plineWidth', 45) # Allow space for long stat names
lg.showStats()
An ''experimental'' feature allows you to use "/" in a stat name, in
order to group statistics for reporting. If used, such statistics will
be grouped (well, they would be anyway by alphabetization),
and indented under the common (pre-/) part, which is printed
with the total counts for all stats in the group. This is especially useful
for defining subtypes of errers, or attaching small data to them. The
example above could be modified to keep count of all the specific
excessive record-lengths, by changing the ''eMsg'' call to:
lg.error("Line %d too long (%d characters)." % (recnum, len(rec)),
stat="Too long/%d" % (len(rec))
In addition to the `stat` parameter on various messaging methods, there
are methods for manipulating the stats independently of messages:
* ''defineStat(self, stat)''
Initialize the given stat (to 0). This is optional (that is, you can just
mention a stat and it is created if necessary).
* ''bumpStat(self, stat, amount=1)''
Increment the given stat by the specified ''amount'' (default: 1).
* ''appendStat(self, stat, datum)''
Convert the given stat to a list if it is not one already, and ''append''
the ''datum'' to the list.
* ''setStat(self, stat, value)''
Set the given stat to the given ''value''.
* ''getStat(self, stat)''
Return the value of the given stat.
* '''showStats(self, zeros=False, descriptions=None, dotfill=0, fillchar='.', onlyMatching='', lists='len')
Display all the accumulated statistics,
each with its name and then its value.
Each statistic is printed used ''pline''(), so is subject to the formatting
described there.
Stats with a value of 0 are not shown unless
the ''zeros'' parameter is set to True.
If ''descriptions'' is specified, it must be a dict that maps (some or all) of
the stat names to strings to be printed in place of the name. This is useful
if you want short/compact names to pass to ''bumpStat'', messaging, or other
methods that take a stat name; and yet want long, more user-readable
error descriptions for the report.
''Note'': See above re. the (experimental) use of "/" in statistic names,
to group and accumulate more detailed statistics.
''dotfill'': Use dot-fills for readability (@see pline()).
''fillchar'': Use this char as the "dot" for dotfill.
''onlyMatching'': a regex. Only stats whose names match it are displayed.
''lists'': For stats that are lists (see ''appendStat''()), specifies
how to show those lists:
* 'omit': do not show at all
* 'len': just show the list name and length
* 'uniq': sort and uniqify, then show the list
* 'uniqc': sort and uniqify with count, then show the list
* otherwise: show the whole list
* ''clearStats(self)''
Remove all statistics.
==Additional messaging methods==
This package adds several more calls. The first is simply ''fatal'',
which issues a ''log''() message with level 60 and then raises an
exception (if uncaught, this will lead to a stack trace and termination).
The numbered message methods, such as ''info1'' check the verbosity level
(set via ''setVerbose()''), and discard the message if it is not at least
as high as the number at the end of the method name.
This supports the common *nix command-line usage, where the
amount of messaging is increased by using one or more ''-v'' options.
==Option control methods==
* '''setOption(name, value)'''
Sets the named option (see below) to the specified value.
* ''getOption(name)''
Returns the current value of the named option.
===List of available options===
These are stored in instances of `ALogger`, as .options['name'].
* Option: ''badChar''
Print this character in place of control or other unprintables.
Default: '?'.
* ''Option'':''color''
Use ANSI terminal colors? If not passed to the constructor,
this defaults to True if environment variable `CLI_COLOR` is set,
otherwise False. When set, the script tries to load my ColorManager package.
Failing that, it issues a message and falls back to using "*" before and
after colorized items.
* ''Option'':''controlPix''
When set, displays U+24xx for control characters. Those Unicode characters
should show up as tiny mnemonics for the corresponding control characters.
* ''Option'':''encoding''
Sets the name of the character encoding to be used (default `utf-8`).
* ''Option'':''filename''
This is just passed along to the like-named argument of the
Python ''logging.basicConfig''() method, and controls where messages go.
It defaults to `sys.stderr`.
* ''Option'':''noMoreStats''
When set, trying to increment a ''msgStats'' counter that is not already
known, results in an error message instead of the counter being quietly
created.
* ''Option'':''verbose'' (int)
''vMsg'', ''hMsg'', and ''Msg'' issue ''logging.info()'' messages.
Thus, they are at ''logging'' level ''20'', and so calling ''setVerbose''()
not only sets this option, but also force the ''logging'' level to 20.
The ''Msg'' calls take a first argument which is the minimum verbosity
required for the message to be shown.
This typically equals the number of times the ''-v'' option
was specified on the command line.
This is completely separate from ''logging'''s own 0-50 'levels'.
If the ''verbose'' option is not set at all,
then all such messages are displayed, regardless of their verbosity-level
(assuming that ''logging'''s message level is at most 20.
=Related commands=
My `ColorManager.py`: provides color support when requested.
=Known bugs and limitations=
If you don't call the constructor (or `setVerbose()`) with a non-zero argument,
nothing is printed.
This package is not integrated with the Python ''logging''
package's hierarchical loggers model.
There may be a problem with `maxItems`, and with suppressing callables in `formatRec()`.
`formatRec()` could use more options to shorten long displays, such as:
* suppress all object properties with value None
* suppress a given list of properties
* don't descend into certain object types
* use a caller-supplied displayer method
* output the info in HTML or XML rather than just as printable.
=To do=
* I don't really like that "stats" part, discard it.
setVerbose does:
logging.basicConfig(self.level)
lg.setLevel(20-args.verbose)
Then message levels 0...9 do the same translation.
* Option to get rid of "INFO:root:" prefix from logging package.
* Consider switching to something like:
logv = lambda lvl, msg: info(msg) if lvl<=args.verbose else None
* formatRec():
** Move out to be a separate library or at least class?
** Support more numpy, PyTorch classes (and have a way to extend? maybe just tostring()?)
Or have a dict of types, mapping to their formatter functions.
** Protect against circular structures (started 2022-02-17 via 'noDups' option).
** Option to only show first N of each class/type.
** Perhaps accept maxItems as a dict, mapping types to limits?
** For collections of length 1, put on same line.
** Coalesce short lists (and matrices?) of numbers.
** Option to make strings visible and/or ASCII.
** Implement key and property sorting options.
** Option to re-display item name after closing ">>".
** Let stopNames option take list, not just dict. When a dict, allow value to
say which sub-item to use as the key, and only show that.
=History=
* 2011-12-09: Port from Perl to Python by Steven J. DeRose.
(... see `sjdUtils.py`)
* 2015-10-13: Split messaging features from `sjdUtils` to new `ALogger`.
Integrate with Python `logging` package.
* 2015-12-31ff: Improve doc.
* 2018-04-02: Add `direct` option to make Anaconda Nav happier.
* 2018-07-29: Add `showSize` option to `formatRec`.
* 2018-08-07: Add `quoteKey` and `keyWidth` options to `formatRec()`.
* 2018-08-17: Add `maxItems` option to `formatRec()`.
* 2020-03-02: New layout, doc to MarkDown, document `formatRec()` better. Lint.
* 2020-03-06: Add `formatRec()` support for numpy and Torch vectors.
Move `formatRec()` options to a dict instead of individual parameters, and make
them inherit right. Add `propWidth`, `noExpand`, `disp_None`.
Improve formatting.
* 2020-08-19: Cleanup, lint, add some type-hinting. Improve messaging when
Linux command `dircolors` is not available. POD to MarkDown.
* 2020-09-23: Add forward for `colorize()`, and a fallback. lint.
* 2020-10-07: Add formatPickle().
* 2020-12-10ff: Drop 'direct' option, start dropping defineMsgType
and [evh]Msg() forms, in favor of warningN(), etc. Add verboseDefault. lint.
* 2021-07-09: formatRec(): Fix maxDepth limiting, clean up option handling.
Rename `noExpand` to `stopTypes`, add `stopNames`.
* 2022-02-17: formatRec: Fix numpy ndarray shape reporting. Make depth limit
message report type of next thing, and still report scalars. Add 'maxString'.
Drop headingN() methods, and make log() insert separator space and line if the
message starts with "====". Planning to lose hMsg(), defined msg types, etc.
Add 'noDups'. Drop rMsg(), MsgRule, defineMsgType(), Msg().
* 2022-07-27: Add indent option to pline. More type-hints.
* 2022-08-03: Drop remaining MsgType stuff, and MsgPush/MsgPop. Add indent
option to DirectMsg and pline. Add parseOptions in test main.
* 2022-08-03ff: Break formatRec into a class. Keep options on class instead
of passing down. Add FormatBuf class, to replace mongo string 'buf' (for the
moment, have both, but clean up how they're built). Drop remaining defineMsgType.
>>>>>>> 6f2e866 (logging updates. improve alogging.formatRec().)
* 2022-09-30: Fiddle with levels and level defaults. Drop MsgPush, MsgPop, etc.
* 2023-05-17: Move ALogger options out of dict into main object. Add .level for
regular Python 'logging' compatibility.
* 2023-09-20: Refactor too-large formatRec_R(). Support sortKeys for NamedNodeMaps.
Improve setLevel.
=Rights=
Copyright 2019 by Steven J. DeRose. This work is licensed under a Creative
Commons Attribution-Share Alike 3.0 Unported License. For further information
on this license, see [http://creativecommons.org/licenses/by-sa/3.0].
For the most recent version, see [http://www.derose.net/steve/utilities] or
[http://github.com/sderose].
This script was derived (almost entirely by extraction)
in Oct. 2015 from `sjdUtils.py`,
which in turn was ported from Perl version in Dec. 2011,
which was assembled from pieces in others of my scripts in Mar. 2011.
All those works are by the same author and are licensed under the same license
(Creative Commons Attribution-Sharealike 3.0 unported).
=Options=
"""
###############################################################################
#
class ALogger:
"""Some extensions to the Python logger, to make it more like my
old Perl logger, and to support repeatable -v options and color.
"""
verboseDefault = 0
optionTypes = {
"verbose" : int,
"color" : bool,
"filename" : str,
"encoding" : str,
"level" : int,
"badChar" : str,
"controlPix" : bool,
"indentString" : str,
"noMoreStats" : bool,
"plineWidth" : int,
"displayForm" : str, # Really an enum...
}
# For converting hex-escaped chars
unescapeExpr = re.compile(r'_[0-9a-f][0-9a-f]')
def __init__(self,
verbose:int = None, # Number of -v's in effect.
color:bool = None, # Try to use ANSI terminal color?
filename:str = None, # Write log somewhere?
encoding:str = 'utf-8',
options:dict = None # TODO: other options in here?
):
# First, set up the regular Python 'logging' package as desired
#
if (filename):
logging.basicConfig(filename=filename,
format='%(message)s')
else:
logging.basicConfig(format='%(message)s')
self.lg = logging.getLogger()
self.level = self.lg.level
self.filename = filename
# Now add all our own stuff
if (color is None):
color = ("CLI_COLOR" in os.environ)
if (verbose is None): verbose = ALogger.verboseDefault
self.options = {
"verbose" : verbose, # Verbosity level
"color" : color, # Using terminal color?
"filename" : filename, # For logging pkg
"encoding" : encoding, # Assumed char set
# Set a level that doesn't hide too much:
# CRITICAL 50, ERROR 40, WARNING 30, INFO 20, DEBUG 10, NOTSET 0
"level" : ALogger.verboseDefault, # For logging filtering
"badChar" : "?", # Substitute for undecodables
"controlPix" : True, # Show U+24xx for control chars?
"indentString" : " ", # For pretty-printing
"noMoreStats" : False, # Warn on new stat names
"plineWidth" : 30, # Columns for label for pline()
"displayForm" : "SIMPLE", # UNICODE, HTML, or SIMPLE
}
# Manage any options passed as **kwargs
if (options is not None):
for k, v in options.items():
if (k not in self.options):
raise KeyError("Unrecognized option '%s'." % (k))
self.options[k] = self.optionTypes[k](v)
if (filename):
logging.basicConfig(level=self.options["level"],
filename=self.options["filename"],
format='%(message)s')
else:
logging.basicConfig(level=self.options["level"],
format='%(message)s')
self.lg = logging.getLogger()
self.msgStats = {}
self.errorCount = 0 # Total number of errors logged
self.plineCount = 1 # pline() uses for colorizing
self.unescapeExpr = re.compile(r'_[0-9a-f][0-9a-f]')
# Also available via ColorManager.uncolorizer()
self.colorRegex = re.compile(r'\x1b\[\d+(;\d+)*m')
self.colorManager = None
if (self.options["color"]): self.setupColor()
return
def loggingError(self, msg):
"""Issue a message for an error generated here. By default this just goes
unconditionally to stderr, but callers can override this method as desired.
"""
sys.stderr.write(msg)
def setColors(self, activate:bool=True):
"""Obsolete but kept for backward compatibilit
"""
if (activate): self.setupColor()
def setupColor(self):
try:
import ColorManager
self.colorManager = ColorManager.ColorManager()
self.colorManager.setupColors()
self.colorStrings = self.colorManager.getColorStrings()
#print("Color count: %d" % (len(self.colorStrings)))
except ImportError as e:
self.loggingError("Cannot import ColorManager:\n %s" % (e))
self.colorStrings = { "bold": '*', "off":'*' }
def colorize(self, argColor:str="red", s:str="", endAs:str="off",
fg='', bg='', effect=''):
if (self.colorManager):
return self.colorManager.colorize(argColor=argColor, msg=s,
endAs=endAs, fg=fg, bg=bg, effect=effect)
return "*" + s + "*"
###########################################################################
# Options
#
def setALoggerOption(self, name:str, value:Any=1):
return(self.setOption(name, value))
def setOption(self, name:str, value:Any=1) -> bool:
"""Set option ''name'' to ''value''.
"""
if (name not in self.options):
return(None)
self.options[name] = self.optionTypes[name](value)
return(1)
def getALoggerOption(self, name:str) -> Any:
return(self.getOption(name))
def getOption(self, name:str) -> Any:
"""Return the current value of an option (see setOption()).
"""
return(self.options[name])
###########################################################################
# Handle verbosity level like *nix -- number of '-v' options.
# Translate this to logging's INFO range: descending from 20.
# For compatibility with Python 'logging' pkg, we also store that as .level.
#
def setVerbose(self, v:int) -> int:
"""Set the degree of verbosity for messages to be reported.
This is NOT the same as the logging package "level"!
"""
assert v >= 0 and v < 10
v = int(v)
if (v>0 and not self.lg.isEnabledFor(20)):
self.level = 20 - v
#logging.basicConfig(self.level)
self.lg.setLevel(self.level)
return(self.setALoggerOption("verbose", v))
def getVerbose(self) -> int:
"""Return the verbosity threshold.
"""
return(self.getALoggerOption("verbose"))
# Also provide setLevel() to act just like logging's, for compatibility.
#
_levelNames = { "CRITICAL":50, "ERROR":40, "WARNING":30,
"INFO":20, "DEBUG":10, "NOTSET":0,
"V1":19, "V2":18, "V3":17, "V4":16, "V5":15 }
def setLevel(self, pLevel:Union[int, str]) -> None:
if (pLevel in self._levelNames): pLevel = self._levelNames[pLevel]
self.level = pLevel
#logging.basicConfig(self.level)
self.lg.setLevel(pLevel)
###########################################################################
#
def showInvisibles(self, s):
"""Return "s" with non-ASCII and control characters replaced by
hexadecimal escapes such as \\uFFFF.
"""
cp = self.options["controlPix"]
try:
s = re.sub(r'([%[:ascii:]]|[[:cntrl:]])',
lambda x: (
chr(0x2400+ord(x.group(1))) if (cp and ord(x.group(1))<32)
else ("\\u%04x;" % (ord(x.group(1))))),
s)
except re.error as e:
self.loggingError("Regex error: %s\n" % (e))
return s
def getPickedColorString(self, argColor:str, msgType:str=None):
"""Return color escape codes (not name!) -- the requested color if any,
else the default color for the message type.
"""
assert not msgType
if (not self.options["color"]):
return("","")
if (argColor and argColor in self.colorStrings):
return(self.colorStrings[argColor], self.colorStrings["off"])
return("","")
###########################################################################
# Manage persistent indentation of xMsg calls.
#
# MsgPush, MsgPop, MsgSet, MsgGet have been deleted.
###########################################################################
# Python "Logger"-compatible calls:
#
# The type-specific calls handles statistic-updating, because that should
# happen whether or not the level filters out actual display.
#
def log(self, level, msg, **kwargs): # ARGS!
if (self.options["verbose"] < level): return
self.directMsg(msg, **kwargs)
def debug(self, msg, **kwargs): # level = 10
self.directMsg(msg, **kwargs)
def info(self, msg, **kwargs): # level = 20
self.directMsg(msg, **kwargs)
def warning(self, msg, **kwargs): # level = 30
self.directMsg(msg, **kwargs)
def error(self, msg, **kwargs): # level = 40
self.directMsg(msg, **kwargs)
def exception(self, msg, **kwargs): # level = 40
self.directMsg(msg, **kwargs)
def critical(self, msg, **kwargs): # level = 50
self.directMsg(msg, **kwargs)
###########################################################################
# Level-specific shorthand (replacing old log-style level param).
#
def info0(self, msg, **kwargs): self.log(0, msg, **kwargs)
def info1(self, msg, **kwargs): self.log(1, msg, **kwargs)
def info2(self, msg, **kwargs): self.log(2, msg, **kwargs)
def warning0(self, msg, **kwargs): self.log(0, msg, **kwargs)
def warning1(self, msg, **kwargs): self.log(1, msg, **kwargs)
def warning2(self, msg, **kwargs): self.log(2, msg, **kwargs)
def directMsg(self, msg, **kwargs) -> None:
"""Pretty much everything ends up here.
"stat" are should have been handled and removed by caller.
kwargs known:
"color" -- takes colorString style names
"end" -- like Python print()
"indent" -- indent the message this far.
"""
#assert ("stat" not in kwargs)
if (msg.startswith("====")):
msg = "\n%s\n%s" % ("=" * 79, msg.lstrip('='))
if ("indent" in kwargs):
msg = " " * kwargs["indent"] + msg
ender = kwargs["end"] if "end" in kwargs else "\n"
if (not msg.endswith(ender)): msg += ender
#msg2 = re.sub(r'\n', '*', msg)
if ("color" in kwargs):
if (not self.colorManager): msg += " (color not enabled)"
else: msg = self.colorManager.colorize(kwargs["color"])
sys.stderr.write(msg)
# Add fatal(): level 60, and raise exception after.
#
def fatal(self, msg, **kwargs) -> None:
self.log(60, msg, *kwargs)
raise SystemExit("*** Logged message is fatal ***")
# The following message methods and types are treated
# as subtypes of info() messages. So they only appear if you
# set logging level to a value <= 20.
# IN ADDITION, they take a "verbose" argument, which is a value
# from 0 to n, and is the number of '-v' flags (or equivalent)
# needed to make the message appare. So 0 will show up any time
# info() messages do; 1 must also have at least -v; two -v -v,....
#
def vMsg(
self,
verbose:int,
m1:str,
m2:str="",
color:str=None,
indent:int=0,
) -> None:
"""Issue a "verbose" (msgType "v") info message.
"""
if (self.options["verbose"] < verbose): return
m = self.assembleMsg(m1=m1, m2=m2, color=color, indent=indent)
self.info(m)
# Change called over to call error() directly, then delete eMsg().
def eMsg(
self,
verbose:int,
m1:str,
m2:str="",
color:str=None,
indent:int=0,
) -> None:
"""Issue an error message.
"""
self.errorCount += 1
if (self.options["verbose"] < verbose): return
m = self.assembleMsg(m1=m1, m2=m2, color=color, indent=indent)
self.info(m)
if (verbose<0):
raise AssertionError("alogging:eMsg with level %d." % (verbose))
def hMsg(
self,
verbose:int,
m1:str,
m2:str="",
color:str=None,
indent:int=0,
) -> None:
"""Issue a "heading" (msgType "h") message.
DEPRECATED in favor of prefixing "====" to other messages.
"""
if (self.options["verbose"] < verbose): return
m = self.assembleMsg(m1=m1, m2=m2, color=color, indent=indent)
self.info(m)
# Messages of various kinds (sync with Perl version)
def Msg(
self,
level:int, # This is a -v level: 0...9.
m1:str,
m2:str="",
color:str=None,
escape:str=None,
stat:str=None,
verbose:int=0
) -> None:
"""Issue a message.
See also the wrappers ''hMsg''(), ''vMsg''(), and ''hMsg''().
Checks ''verbose'' against the value of the 'verbose' option; if that
option has actually been set, ''and'' is lower, return without display.
Otherwise, call Logger's ''info''() to actually issue the message.