2323To use, simply 'import logging' and log away!
2424"""
2525
26- import sys , os , time , io , traceback , warnings , weakref , collections .abc
26+ import sys , os , time , io , re , traceback , warnings , weakref , collections .abc
2727
2828from string import Template
29+ from string import Formatter as StrFormatter
30+
2931
3032__all__ = ['BASIC_FORMAT' , 'BufferingFormatter' , 'CRITICAL' , 'DEBUG' , 'ERROR' ,
3133 'FATAL' , 'FileHandler' , 'Filter' , 'Formatter' , 'Handler' , 'INFO' ,
@@ -413,33 +415,71 @@ def makeLogRecord(dict):
413415 rv .__dict__ .update (dict )
414416 return rv
415417
418+
416419#---------------------------------------------------------------------------
417420# Formatter classes and functions
418421#---------------------------------------------------------------------------
422+ _str_formatter = StrFormatter ()
423+ del StrFormatter
424+
419425
420426class PercentStyle (object ):
421427
422428 default_format = '%(message)s'
423429 asctime_format = '%(asctime)s'
424430 asctime_search = '%(asctime)'
431+ validation_pattern = re .compile (r'%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]' , re .I )
425432
426433 def __init__ (self , fmt ):
427434 self ._fmt = fmt or self .default_format
428435
429436 def usesTime (self ):
430437 return self ._fmt .find (self .asctime_search ) >= 0
431438
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 ):
433445 return self ._fmt % record .__dict__
434446
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+
435454class StrFormatStyle (PercentStyle ):
436455 default_format = '{message}'
437456 asctime_format = '{asctime}'
438457 asctime_search = '{asctime'
439458
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 ):
441463 return self ._fmt .format (** record .__dict__ )
442464
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+
443483
444484class StringTemplateStyle (PercentStyle ):
445485 default_format = '${message}'
@@ -454,9 +494,24 @@ def usesTime(self):
454494 fmt = self ._fmt
455495 return fmt .find ('$asctime' ) >= 0 or fmt .find (self .asctime_format ) >= 0
456496
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 ):
458512 return self ._tpl .substitute (** record .__dict__ )
459513
514+
460515BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"
461516
462517_STYLES = {
@@ -510,7 +565,7 @@ class Formatter(object):
510565
511566 converter = time .localtime
512567
513- def __init__ (self , fmt = None , datefmt = None , style = '%' ):
568+ def __init__ (self , fmt = None , datefmt = None , style = '%' , validate = True ):
514569 """
515570 Initialize the formatter with specified format strings.
516571
@@ -530,6 +585,9 @@ def __init__(self, fmt=None, datefmt=None, style='%'):
530585 raise ValueError ('Style must be one of: %s' % ',' .join (
531586 _STYLES .keys ()))
532587 self ._style = _STYLES [style ][0 ](fmt )
588+ if validate :
589+ self ._style .validate ()
590+
533591 self ._fmt = self ._style ._fmt
534592 self .datefmt = datefmt
535593
0 commit comments