23
23
To use, simply 'import logging' and log away!
24
24
"""
25
25
26
- import sys , os , time , io , traceback , warnings , weakref , collections .abc
26
+ import sys , os , time , io , re , traceback , warnings , weakref , collections .abc
27
27
28
28
from string import Template
29
+ from string import Formatter as StrFormatter
30
+
29
31
30
32
__all__ = ['BASIC_FORMAT' , 'BufferingFormatter' , 'CRITICAL' , 'DEBUG' , 'ERROR' ,
31
33
'FATAL' , 'FileHandler' , 'Filter' , 'Formatter' , 'Handler' , 'INFO' ,
@@ -413,33 +415,71 @@ def makeLogRecord(dict):
413
415
rv .__dict__ .update (dict )
414
416
return rv
415
417
418
+
416
419
#---------------------------------------------------------------------------
417
420
# Formatter classes and functions
418
421
#---------------------------------------------------------------------------
422
+ _str_formatter = StrFormatter ()
423
+ del StrFormatter
424
+
419
425
420
426
class PercentStyle (object ):
421
427
422
428
default_format = '%(message)s'
423
429
asctime_format = '%(asctime)s'
424
430
asctime_search = '%(asctime)'
431
+ validation_pattern = re .compile (r'%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]' , re .I )
425
432
426
433
def __init__ (self , fmt ):
427
434
self ._fmt = fmt or self .default_format
428
435
429
436
def usesTime (self ):
430
437
return self ._fmt .find (self .asctime_search ) >= 0
431
438
432
- def format (self , record ):
439
+ def validate (self ):
440
+ """Validate the input format, ensure it matches the correct style"""
441
+ if not self .validation_pattern .search (self ._fmt ):
442
+ raise ValueError ("Invalid format '%s' for '%s' style" % (self ._fmt , self .default_format [0 ]))
443
+
444
+ def _format (self , record ):
433
445
return self ._fmt % record .__dict__
434
446
447
+ def format (self , record ):
448
+ try :
449
+ return self ._format (record )
450
+ except KeyError as e :
451
+ raise ValueError ('Formatting field not found in record: %s' % e )
452
+
453
+
435
454
class StrFormatStyle (PercentStyle ):
436
455
default_format = '{message}'
437
456
asctime_format = '{asctime}'
438
457
asctime_search = '{asctime'
439
458
440
- def format (self , record ):
459
+ fmt_spec = re .compile (r'^(.?[<>=^])?[+ -]?#?0?(\d+|{\w+})?[,_]?(\.(\d+|{\w+}))?[bcdefgnosx%]?$' , re .I )
460
+ field_spec = re .compile (r'^(\d+|\w+)(\.\w+|\[[^]]+\])*$' )
461
+
462
+ def _format (self , record ):
441
463
return self ._fmt .format (** record .__dict__ )
442
464
465
+ def validate (self ):
466
+ """Validate the input format, ensure it is the correct string formatting style"""
467
+ fields = set ()
468
+ try :
469
+ for _ , fieldname , spec , conversion in _str_formatter .parse (self ._fmt ):
470
+ if fieldname :
471
+ if not self .field_spec .match (fieldname ):
472
+ raise ValueError ('invalid field name/expression: %r' % fieldname )
473
+ fields .add (fieldname )
474
+ if conversion and conversion not in 'rsa' :
475
+ raise ValueError ('invalid conversion: %r' % conversion )
476
+ if spec and not self .fmt_spec .match (spec ):
477
+ raise ValueError ('bad specifier: %r' % spec )
478
+ except ValueError as e :
479
+ raise ValueError ('invalid format: %s' % e )
480
+ if not fields :
481
+ raise ValueError ('invalid format: no fields' )
482
+
443
483
444
484
class StringTemplateStyle (PercentStyle ):
445
485
default_format = '${message}'
@@ -454,9 +494,24 @@ def usesTime(self):
454
494
fmt = self ._fmt
455
495
return fmt .find ('$asctime' ) >= 0 or fmt .find (self .asctime_format ) >= 0
456
496
457
- def format (self , record ):
497
+ def validate (self ):
498
+ pattern = Template .pattern
499
+ fields = set ()
500
+ for m in pattern .finditer (self ._fmt ):
501
+ d = m .groupdict ()
502
+ if d ['named' ]:
503
+ fields .add (d ['named' ])
504
+ elif d ['braced' ]:
505
+ fields .add (d ['braced' ])
506
+ elif m .group (0 ) == '$' :
507
+ raise ValueError ('invalid format: bare \' $\' not allowed' )
508
+ if not fields :
509
+ raise ValueError ('invalid format: no fields' )
510
+
511
+ def _format (self , record ):
458
512
return self ._tpl .substitute (** record .__dict__ )
459
513
514
+
460
515
BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"
461
516
462
517
_STYLES = {
@@ -510,7 +565,7 @@ class Formatter(object):
510
565
511
566
converter = time .localtime
512
567
513
- def __init__ (self , fmt = None , datefmt = None , style = '%' ):
568
+ def __init__ (self , fmt = None , datefmt = None , style = '%' , validate = True ):
514
569
"""
515
570
Initialize the formatter with specified format strings.
516
571
@@ -530,6 +585,9 @@ def __init__(self, fmt=None, datefmt=None, style='%'):
530
585
raise ValueError ('Style must be one of: %s' % ',' .join (
531
586
_STYLES .keys ()))
532
587
self ._style = _STYLES [style ][0 ](fmt )
588
+ if validate :
589
+ self ._style .validate ()
590
+
533
591
self ._fmt = self ._style ._fmt
534
592
self .datefmt = datefmt
535
593
0 commit comments