~ubuntu-branches/ubuntu/utopic/python-logutils/utopic

« back to all changes in this revision

Viewing changes to logutils/dictconfig.py

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2014-01-09 14:31:16 UTC
  • Revision ID: package-import@ubuntu.com-20140109143116-khh927gczb4ewojh
Tags: upstream-0.3.3
ImportĀ upstreamĀ versionĀ 0.3.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# Copyright (C) 2009-2013 Vinay Sajip. See LICENSE.txt for details.
 
3
#
 
4
import logging.handlers
 
5
import re
 
6
import sys
 
7
import types
 
8
 
 
9
try:
 
10
    basestring
 
11
except NameError:
 
12
    basestring = str
 
13
try:
 
14
    StandardError
 
15
except NameError:
 
16
    StandardError = Exception
 
17
   
 
18
IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
 
19
 
 
20
def valid_ident(s):
 
21
    m = IDENTIFIER.match(s)
 
22
    if not m:
 
23
        raise ValueError('Not a valid Python identifier: %r' % s)
 
24
    return True
 
25
 
 
26
#
 
27
# This function is defined in logging only in recent versions of Python
 
28
#
 
29
try:
 
30
    from logging import _checkLevel
 
31
except ImportError:
 
32
    def _checkLevel(level):
 
33
        if isinstance(level, int):
 
34
            rv = level
 
35
        elif str(level) == level:
 
36
            if level not in logging._levelNames:
 
37
                raise ValueError('Unknown level: %r' % level)
 
38
            rv = logging._levelNames[level]
 
39
        else:
 
40
            raise TypeError('Level not an integer or a '
 
41
                            'valid string: %r' % level)
 
42
        return rv
 
43
 
 
44
# The ConvertingXXX classes are wrappers around standard Python containers,
 
45
# and they serve to convert any suitable values in the container. The
 
46
# conversion converts base dicts, lists and tuples to their wrapped
 
47
# equivalents, whereas strings which match a conversion format are converted
 
48
# appropriately.
 
49
#
 
50
# Each wrapper should have a configurator attribute holding the actual
 
51
# configurator to use for conversion.
 
52
 
 
53
class ConvertingDict(dict):
 
54
    """A converting dictionary wrapper."""
 
55
 
 
56
    def __getitem__(self, key):
 
57
        value = dict.__getitem__(self, key)
 
58
        result = self.configurator.convert(value)
 
59
        #If the converted value is different, save for next time
 
60
        if value is not result:
 
61
            self[key] = result
 
62
            if type(result) in (ConvertingDict, ConvertingList,
 
63
                                ConvertingTuple):
 
64
                result.parent = self
 
65
                result.key = key
 
66
        return result
 
67
        
 
68
    def get(self, key, default=None):
 
69
        value = dict.get(self, key, default)
 
70
        result = self.configurator.convert(value)
 
71
        #If the converted value is different, save for next time
 
72
        if value is not result:
 
73
            self[key] = result
 
74
            if type(result) in (ConvertingDict, ConvertingList,
 
75
                                ConvertingTuple):
 
76
                result.parent = self
 
77
                result.key = key
 
78
        return result
 
79
        
 
80
    def pop(self, key, default=None):
 
81
        value = dict.pop(self, key, default)
 
82
        result = self.configurator.convert(value)
 
83
        if value is not result:
 
84
            if type(result) in (ConvertingDict, ConvertingList,
 
85
                                ConvertingTuple):
 
86
                result.parent = self
 
87
                result.key = key
 
88
        return result
 
89
 
 
90
class ConvertingList(list):
 
91
    """A converting list wrapper."""
 
92
    def __getitem__(self, key):
 
93
        value = list.__getitem__(self, key)
 
94
        result = self.configurator.convert(value)
 
95
        #If the converted value is different, save for next time
 
96
        if value is not result:
 
97
            self[key] = result
 
98
            if type(result) in (ConvertingDict, ConvertingList,
 
99
                                ConvertingTuple):
 
100
                result.parent = self
 
101
                result.key = key
 
102
        return result
 
103
 
 
104
    def pop(self, idx=-1):
 
105
        value = list.pop(self, idx)
 
106
        result = self.configurator.convert(value)
 
107
        if value is not result:
 
108
            if type(result) in (ConvertingDict, ConvertingList,
 
109
                                ConvertingTuple):
 
110
                result.parent = self
 
111
        return result
 
112
 
 
113
class ConvertingTuple(tuple):
 
114
    """A converting tuple wrapper."""
 
115
    def __getitem__(self, key):
 
116
        value = tuple.__getitem__(self, key)
 
117
        result = self.configurator.convert(value)
 
118
        if value is not result:
 
119
            if type(result) in (ConvertingDict, ConvertingList,
 
120
                                ConvertingTuple):
 
121
                result.parent = self
 
122
                result.key = key
 
123
        return result
 
124
 
 
125
class BaseConfigurator(object):
 
126
    """
 
127
    The configurator base class which defines some useful defaults.
 
128
    """
 
129
    
 
130
    CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
 
131
 
 
132
    WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
 
133
    DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
 
134
    INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
 
135
    DIGIT_PATTERN = re.compile(r'^\d+$')
 
136
 
 
137
    value_converters = {
 
138
        'ext' : 'ext_convert',
 
139
        'cfg' : 'cfg_convert',
 
140
    }
 
141
 
 
142
    # We might want to use a different one, e.g. importlib
 
143
    importer = __import__
 
144
    "Allows the importer to be redefined."
 
145
 
 
146
    def __init__(self, config):
 
147
        """
 
148
        Initialise an instance with the specified configuration
 
149
        dictionary.
 
150
        """
 
151
        self.config = ConvertingDict(config)
 
152
        self.config.configurator = self
 
153
 
 
154
    def resolve(self, s):
 
155
        """
 
156
        Resolve strings to objects using standard import and attribute
 
157
        syntax.
 
158
        """
 
159
        name = s.split('.')
 
160
        used = name.pop(0)
 
161
        try:
 
162
            found = self.importer(used)
 
163
            for frag in name:
 
164
                used += '.' + frag
 
165
                try:
 
166
                    found = getattr(found, frag)
 
167
                except AttributeError:
 
168
                    self.importer(used)
 
169
                    found = getattr(found, frag)
 
170
            return found
 
171
        except ImportError:
 
172
            e, tb = sys.exc_info()[1:]
 
173
            v = ValueError('Cannot resolve %r: %s' % (s, e))
 
174
            v.__cause__, v.__traceback__ = e, tb
 
175
            raise v
 
176
 
 
177
    def ext_convert(self, value):
 
178
        """Default converter for the ext:// protocol."""
 
179
        return self.resolve(value)
 
180
    
 
181
    def cfg_convert(self, value):
 
182
        """Default converter for the cfg:// protocol."""
 
183
        rest = value
 
184
        m = self.WORD_PATTERN.match(rest)
 
185
        if m is None:
 
186
            raise ValueError("Unable to convert %r" % value)
 
187
        else:
 
188
            rest = rest[m.end():]
 
189
            d = self.config[m.groups()[0]]
 
190
            #print d, rest
 
191
            while rest:
 
192
                m = self.DOT_PATTERN.match(rest)
 
193
                if m:
 
194
                    d = d[m.groups()[0]]
 
195
                else:
 
196
                    m = self.INDEX_PATTERN.match(rest)
 
197
                    if m:
 
198
                        idx = m.groups()[0]
 
199
                        if not self.DIGIT_PATTERN.match(idx):
 
200
                            d = d[idx]
 
201
                        else:
 
202
                            try:
 
203
                                n = int(idx) # try as number first (most likely)
 
204
                                d = d[n]
 
205
                            except TypeError:
 
206
                                d = d[idx]
 
207
                if m:
 
208
                    rest = rest[m.end():]
 
209
                else:
 
210
                    raise ValueError('Unable to convert '
 
211
                                     '%r at %r' % (value, rest))
 
212
        #rest should be empty
 
213
        return d
 
214
 
 
215
    def convert(self, value):
 
216
        """
 
217
        Convert values to an appropriate type. dicts, lists and tuples are
 
218
        replaced by their converting alternatives. Strings are checked to
 
219
        see if they have a conversion format and are converted if they do.
 
220
        """
 
221
        if not isinstance(value, ConvertingDict) and isinstance(value, dict):
 
222
            value = ConvertingDict(value)
 
223
            value.configurator = self
 
224
        elif not isinstance(value, ConvertingList) and isinstance(value, list):
 
225
            value = ConvertingList(value)
 
226
            value.configurator = self
 
227
        elif not isinstance(value, ConvertingTuple) and\
 
228
                 isinstance(value, tuple):
 
229
            value = ConvertingTuple(value)
 
230
            value.configurator = self
 
231
        elif isinstance(value, basestring):
 
232
            m = self.CONVERT_PATTERN.match(value)
 
233
            if m:
 
234
                d = m.groupdict()
 
235
                prefix = d['prefix']
 
236
                converter = self.value_converters.get(prefix, None)
 
237
                if converter:
 
238
                    suffix = d['suffix']
 
239
                    converter = getattr(self, converter)
 
240
                    value = converter(suffix)
 
241
        return value
 
242
    
 
243
    def configure_custom(self, config):
 
244
        """Configure an object with a user-supplied factory."""
 
245
        c = config.pop('()')
 
246
        if isinstance(c, basestring):
 
247
            c = self.resolve(c)
 
248
        props = config.pop('.', None)
 
249
        # Check for valid identifiers
 
250
        kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
 
251
        result = c(**kwargs)
 
252
        if props:
 
253
            for name, value in props.items():
 
254
                setattr(result, name, value)
 
255
        return result
 
256
 
 
257
    def as_tuple(self, value):
 
258
        """Utility function which converts lists to tuples."""
 
259
        if isinstance(value, list):
 
260
            value = tuple(value)
 
261
        return value
 
262
 
 
263
def named_handlers_supported():
 
264
    major, minor = sys.version_info[:2]
 
265
    if major == 2:
 
266
        result = minor >= 7
 
267
    elif major == 3:
 
268
        result = minor >= 2
 
269
    else:
 
270
        result = (major > 3)
 
271
    return result
 
272
    
 
273
class DictConfigurator(BaseConfigurator):
 
274
    """
 
275
    Configure logging using a dictionary-like object to describe the
 
276
    configuration.
 
277
    """
 
278
 
 
279
    def configure(self):
 
280
        """Do the configuration."""
 
281
 
 
282
        config = self.config
 
283
        if 'version' not in config:
 
284
            raise ValueError("dictionary doesn't specify a version")
 
285
        if config['version'] != 1:
 
286
            raise ValueError("Unsupported version: %s" % config['version'])
 
287
        incremental = config.pop('incremental', False)
 
288
        EMPTY_DICT = {}
 
289
        logging._acquireLock()
 
290
        try:
 
291
            if incremental:
 
292
                handlers = config.get('handlers', EMPTY_DICT)
 
293
                # incremental handler config only if handler name
 
294
                # ties in to logging._handlers (Python 2.7, 3.2+)
 
295
                if named_handlers_supported():
 
296
                    for name in handlers:
 
297
                        if name not in logging._handlers:
 
298
                            raise ValueError('No handler found with '
 
299
                                             'name %r'  % name)
 
300
                        else:
 
301
                            try:
 
302
                                handler = logging._handlers[name]
 
303
                                handler_config = handlers[name]
 
304
                                level = handler_config.get('level', None)
 
305
                                if level:
 
306
                                    handler.setLevel(_checkLevel(level))
 
307
                            except StandardError:
 
308
                                e = sys.exc_info()[1]
 
309
                                raise ValueError('Unable to configure handler '
 
310
                                                 '%r: %s' % (name, e))
 
311
                loggers = config.get('loggers', EMPTY_DICT)
 
312
                for name in loggers:
 
313
                    try:
 
314
                        self.configure_logger(name, loggers[name], True)
 
315
                    except StandardError:
 
316
                        e = sys.exc_info()[1]
 
317
                        raise ValueError('Unable to configure logger '
 
318
                                         '%r: %s' % (name, e))
 
319
                root = config.get('root', None)
 
320
                if root:
 
321
                    try:
 
322
                        self.configure_root(root, True)
 
323
                    except StandardError:
 
324
                        e = sys.exc_info()[1]
 
325
                        raise ValueError('Unable to configure root '
 
326
                                         'logger: %s' % e)
 
327
            else:
 
328
                disable_existing = config.pop('disable_existing_loggers', True)
 
329
                
 
330
                logging._handlers.clear()
 
331
                del logging._handlerList[:]
 
332
                    
 
333
                # Do formatters first - they don't refer to anything else
 
334
                formatters = config.get('formatters', EMPTY_DICT)
 
335
                for name in formatters:
 
336
                    try:
 
337
                        formatters[name] = self.configure_formatter(
 
338
                                                            formatters[name])
 
339
                    except StandardError:
 
340
                        e = sys.exc_info()[1]
 
341
                        raise ValueError('Unable to configure '
 
342
                                         'formatter %r: %s' % (name, e))
 
343
                # Next, do filters - they don't refer to anything else, either
 
344
                filters = config.get('filters', EMPTY_DICT)
 
345
                for name in filters:
 
346
                    try:
 
347
                        filters[name] = self.configure_filter(filters[name])
 
348
                    except StandardError:
 
349
                        e = sys.exc_info()[1]
 
350
                        raise ValueError('Unable to configure '
 
351
                                         'filter %r: %s' % (name, e))
 
352
 
 
353
                # Next, do handlers - they refer to formatters and filters
 
354
                # As handlers can refer to other handlers, sort the keys
 
355
                # to allow a deterministic order of configuration
 
356
                handlers = config.get('handlers', EMPTY_DICT)
 
357
                for name in sorted(handlers):
 
358
                    try:
 
359
                        handler = self.configure_handler(handlers[name])
 
360
                        handler.name = name
 
361
                        handlers[name] = handler
 
362
                    except StandardError:
 
363
                        e = sys.exc_info()[1]
 
364
                        raise ValueError('Unable to configure handler '
 
365
                                         '%r: %s' % (name, e))
 
366
                # Next, do loggers - they refer to handlers and filters
 
367
                
 
368
                #we don't want to lose the existing loggers,
 
369
                #since other threads may have pointers to them.
 
370
                #existing is set to contain all existing loggers,
 
371
                #and as we go through the new configuration we
 
372
                #remove any which are configured. At the end,
 
373
                #what's left in existing is the set of loggers
 
374
                #which were in the previous configuration but
 
375
                #which are not in the new configuration.
 
376
                root = logging.root
 
377
                existing = sorted(root.manager.loggerDict.keys())
 
378
                #The list needs to be sorted so that we can
 
379
                #avoid disabling child loggers of explicitly
 
380
                #named loggers. With a sorted list it is easier
 
381
                #to find the child loggers.
 
382
                #We'll keep the list of existing loggers
 
383
                #which are children of named loggers here...
 
384
                child_loggers = []
 
385
                #now set up the new ones...
 
386
                loggers = config.get('loggers', EMPTY_DICT)
 
387
                for name in loggers:
 
388
                    if name in existing:
 
389
                        i = existing.index(name)
 
390
                        prefixed = name + "."
 
391
                        pflen = len(prefixed)
 
392
                        num_existing = len(existing)
 
393
                        i = i + 1 # look at the entry after name
 
394
                        while (i < num_existing) and\
 
395
                              (existing[i][:pflen] == prefixed):
 
396
                            child_loggers.append(existing[i])
 
397
                            i = i + 1
 
398
                        existing.remove(name)
 
399
                    try:
 
400
                        self.configure_logger(name, loggers[name])
 
401
                    except StandardError:
 
402
                        e = sys.exc_info()[1]
 
403
                        raise ValueError('Unable to configure logger '
 
404
                                         '%r: %s' % (name, e))
 
405
                    
 
406
                #Disable any old loggers. There's no point deleting
 
407
                #them as other threads may continue to hold references
 
408
                #and by disabling them, you stop them doing any logging.
 
409
                #However, don't disable children of named loggers, as that's
 
410
                #probably not what was intended by the user.
 
411
                for log in existing:
 
412
                    logger = root.manager.loggerDict[log]
 
413
                    if log in child_loggers:
 
414
                        logger.level = logging.NOTSET
 
415
                        logger.handlers = []
 
416
                        logger.propagate = True
 
417
                    elif disable_existing:
 
418
                        logger.disabled = True
 
419
    
 
420
                # And finally, do the root logger
 
421
                root = config.get('root', None)
 
422
                if root:
 
423
                    try:
 
424
                        self.configure_root(root)                        
 
425
                    except StandardError:
 
426
                        e = sys.exc_info()[1]
 
427
                        raise ValueError('Unable to configure root '
 
428
                                         'logger: %s' % e)
 
429
        finally:
 
430
            logging._releaseLock()
 
431
 
 
432
    def configure_formatter(self, config):
 
433
        """Configure a formatter from a dictionary."""
 
434
        if '()' in config:
 
435
            factory = config['()'] # for use in exception handler
 
436
            try:
 
437
                result = self.configure_custom(config)
 
438
            except TypeError:
 
439
                te = sys.exc_info()[1]
 
440
                if "'format'" not in str(te):
 
441
                    raise
 
442
                #Name of parameter changed from fmt to format.
 
443
                #Retry with old name.
 
444
                #This is so that code can be used with older Python versions
 
445
                #(e.g. by Django)
 
446
                config['fmt'] = config.pop('format')
 
447
                config['()'] = factory
 
448
                result = self.configure_custom(config)
 
449
        else:
 
450
            fmt = config.get('format', None)
 
451
            dfmt = config.get('datefmt', None)
 
452
            result = logging.Formatter(fmt, dfmt)
 
453
        return result
 
454
    
 
455
    def configure_filter(self, config):
 
456
        """Configure a filter from a dictionary."""
 
457
        if '()' in config:
 
458
            result = self.configure_custom(config)
 
459
        else:
 
460
            name = config.get('name', '')
 
461
            result = logging.Filter(name)
 
462
        return result
 
463
 
 
464
    def add_filters(self, filterer, filters):
 
465
        """Add filters to a filterer from a list of names."""
 
466
        for f in filters:
 
467
            try:
 
468
                filterer.addFilter(self.config['filters'][f])
 
469
            except StandardError:
 
470
                e = sys.exc_info()[1]
 
471
                raise ValueError('Unable to add filter %r: %s' % (f, e))
 
472
 
 
473
    def configure_handler(self, config):
 
474
        """Configure a handler from a dictionary."""
 
475
        formatter = config.pop('formatter', None)
 
476
        if formatter:
 
477
            try:
 
478
                formatter = self.config['formatters'][formatter]
 
479
            except StandardError:
 
480
                e = sys.exc_info()[1]
 
481
                raise ValueError('Unable to set formatter '
 
482
                                 '%r: %s' % (formatter, e))
 
483
        level = config.pop('level', None)
 
484
        filters = config.pop('filters', None)
 
485
        if '()' in config:
 
486
            c = config.pop('()')
 
487
            if isinstance(c, basestring):
 
488
                c = self.resolve(c)
 
489
            factory = c
 
490
        else:
 
491
            klass = self.resolve(config.pop('class'))
 
492
            #Special case for handler which refers to another handler
 
493
            if issubclass(klass, logging.handlers.MemoryHandler) and\
 
494
                'target' in config:
 
495
                try:
 
496
                    config['target'] = self.config['handlers'][config['target']]
 
497
                except StandardError:
 
498
                    e = sys.exc_info()[1]
 
499
                    raise ValueError('Unable to set target handler '
 
500
                                     '%r: %s' % (config['target'], e))
 
501
            elif issubclass(klass, logging.handlers.SMTPHandler) and\
 
502
                'mailhost' in config:
 
503
                config['mailhost'] = self.as_tuple(config['mailhost'])
 
504
            elif issubclass(klass, logging.handlers.SysLogHandler) and\
 
505
                'address' in config:
 
506
                config['address'] = self.as_tuple(config['address'])
 
507
            factory = klass
 
508
        kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
 
509
        try:
 
510
            result = factory(**kwargs)
 
511
        except TypeError:
 
512
            te = sys.exc_info()[1]
 
513
            if "'stream'" not in str(te):
 
514
                raise
 
515
            #The argument name changed from strm to stream
 
516
            #Retry with old name.
 
517
            #This is so that code can be used with older Python versions
 
518
            #(e.g. by Django)
 
519
            kwargs['strm'] = kwargs.pop('stream')
 
520
            result = factory(**kwargs)
 
521
        if formatter:
 
522
            result.setFormatter(formatter)
 
523
        if level is not None:
 
524
            result.setLevel(_checkLevel(level))
 
525
        if filters:
 
526
            self.add_filters(result, filters)
 
527
        return result
 
528
 
 
529
    def add_handlers(self, logger, handlers):
 
530
        """Add handlers to a logger from a list of names."""
 
531
        for h in handlers:
 
532
            try:
 
533
                logger.addHandler(self.config['handlers'][h])
 
534
            except StandardError:
 
535
                e = sys.exc_info()[1]
 
536
                raise ValueError('Unable to add handler %r: %s' % (h, e))
 
537
 
 
538
    def common_logger_config(self, logger, config, incremental=False):
 
539
        """
 
540
        Perform configuration which is common to root and non-root loggers.
 
541
        """
 
542
        level = config.get('level', None)
 
543
        if level is not None:
 
544
            logger.setLevel(_checkLevel(level))
 
545
        if not incremental:
 
546
            #Remove any existing handlers
 
547
            for h in logger.handlers[:]:
 
548
                logger.removeHandler(h)
 
549
            handlers = config.get('handlers', None)
 
550
            if handlers:
 
551
                self.add_handlers(logger, handlers)
 
552
            filters = config.get('filters', None)
 
553
            if filters:
 
554
                self.add_filters(logger, filters)
 
555
        
 
556
    def configure_logger(self, name, config, incremental=False):
 
557
        """Configure a non-root logger from a dictionary."""
 
558
        logger = logging.getLogger(name)
 
559
        self.common_logger_config(logger, config, incremental)
 
560
        propagate = config.get('propagate', None)
 
561
        if propagate is not None:
 
562
            logger.propagate = propagate
 
563
            
 
564
    def configure_root(self, config, incremental=False):
 
565
        """Configure a root logger from a dictionary."""
 
566
        root = logging.getLogger()
 
567
        self.common_logger_config(root, config, incremental)
 
568
 
 
569
dictConfigClass = DictConfigurator
 
570
 
 
571
def dictConfig(config):
 
572
    """Configure logging using a dictionary."""
 
573
    dictConfigClass(config).configure()