Source code for click_utils.logging

# -*- coding: utf-8 -*-
from __future__ import absolute_import

import logging
import logging.config

import click

from . import defaults


[docs]class LogLevelChoice(click.Choice): ''' A subclass of ``click.Choice`` class for specifying a logging level. Example usage:: import click import click_utils @click.command() @click.option('--loglevel', type=click_utils.LogLevelChoice()) def cli(loglevel): click.echo(loglevel) This option type returns an integer representation of a logging level:: $ cli --loglevel=error 40 By default available choices are the lowercased standard ``logging`` level names (i.e ``notset``, ``debug``, ``info``, ``warning``, ``error`` and ``critical``):: $ cli --help Usage: cli [OPTIONS] Options: --loglevel [notset|debug|info|warning|error|critical] ... But you can pass your additional logging levels like this:: logging.addLevelName(102, 'My custom level') @click.command() @click.option('--loglevel', type=click_utils.LogLevelChoice(101, 102)) def cli(loglevel): click.echo(loglevel) ... $ cli --help Usage: cli [OPTIONS] Options: --loglevel [notset|debug|info|warning|error|critical|level101|mycustomlevel] ... $ cli --loglevel=level101 101 You can also pass level name in the uppercased form:: $ cli --loglevel=WARNING 30 Finally, as a special case, user can provide any integer as a logging level:: $ cli --loglevel=123 123 ''' LOGGING_LEVELS = ( ('notset', logging.NOTSET), ('debug', logging.DEBUG), ('info', logging.INFO), ('warning', logging.WARNING), ('error', logging.ERROR), ('critical', logging.CRITICAL), ) @staticmethod def _convert_key(key): return key.lower().replace(' ', '') def __init__(self, *extra_levels): self.logging_levels_dict = dict(self.LOGGING_LEVELS) self.logging_level_keys = list(map(lambda x: x[0], self.LOGGING_LEVELS)) for extra_level in extra_levels: level_name = logging.getLevelName(extra_level) key = self._convert_key(level_name) self.logging_levels_dict.update({key: extra_level}) self.logging_level_keys.append(key) super(LogLevelChoice, self).__init__(self.logging_level_keys)
[docs] def convert(self, value, param, ctx): lower_val = self._convert_key(value) if not isinstance(value, int) else '{0}'.format(value) if lower_val.isdigit(): return int(lower_val) result = super(LogLevelChoice, self).convert(lower_val, param, ctx) return self.logging_levels_dict[result]
[docs]def loglevel_callback_factory(ctx_loglevel_attr=None): def inner(ctx, param, value): if not value or ctx.resilient_parsing: return if ctx_loglevel_attr: setattr(ctx, ctx_loglevel_attr, value) return value return inner
[docs]def loglevel_option(*param_decls, **attrs): '''Shortcut for logging level option type. This is equivalent to decorating a function with :func:`option` with the following parameters:: @click.command() @click.option('--loglevel', type=click_utils.LogLevelChoice()) def cli(loglevel): pass It also has a callback that stores a chosen loglevel on the context object. By default the loglevel is stored in attribute called ``click_utils_loglevel`` To change the attribute name you can pass an keyword arg to this option called ``ctx_loglevel_attr``. This is useful for other options to know what loglevel was set. If you want to disable storing the loglevel on context just pass:: @click.command() @click_utils.loglevel_option(ctx_loglevel_attr=False) def cli(loglevel): click.echo(loglevel) Also this option is eager by default. ''' def decorator(f): ctx_loglevel_attr = attrs.pop('ctx_loglevel_attr', defaults.CONTEXT_LOGLEVEL_ATTR) default_callback = loglevel_callback_factory(ctx_loglevel_attr=ctx_loglevel_attr) attrs.setdefault('callback', default_callback) attrs.setdefault('is_eager', True) attrs.setdefault('type', LogLevelChoice()) return click.option(*(param_decls or ('--loglevel',)), **attrs)(f) return decorator
[docs]def logconfig_callback_factory(defaults=None, disable_existing_loggers=False): ''' a factory for creating parametrized callbacks to invoke ``logging.config.fileConfig`` ''' def inner(ctx, param, value): if not value or ctx.resilient_parsing: return logging.config.fileConfig(value, defaults=defaults, disable_existing_loggers=disable_existing_loggers) return value return inner
logconfig_callback = logconfig_callback_factory() logconfig_callback.__doc__ = ''' a default callback for invoking:: logging.config.fileConfig(value, defaults=None, disable_existing_loggers=False) '''
[docs]def logconfig_option(*param_decls, **attrs): '''An option to easily configure logging via ``logging.config.fileConfig``. This one allows to specify a file that will be passed as an argument to ``logging.config.fileConfig`` function. Using this option like this:: @click.command() @click_utils.logconfig_option() def cli(): pass is equivalent to: .. code-block:: python def mycallback(ctx, param, value): if not value or ctx.resilient_parsing: return logging.config.fileConfig(value, defaults=None, disable_existing_loggers=False) @click.command() @click.option('--logconfig', expose_value=False, type=click.Path(exists=True, file_okay=True, dir_okay=False, writable=False, readable=True, resolve_path=True) callback=mycallback ) def cli(): pass This option accepts all the usual arguments and keyword arguments as ``click.option`` (be careful with passing a different ``callback`` though). Additionally it accepts two extra keyword arguments which are passed to ``logging.config.fileConfig``: * ``fileconfig_defaults`` is passed as ``defaults`` argument (default: ``None``) * ``disable_existing_loggers`` is passed as ``disable_existing_loggers`` argument (default: ``False``) So you can add logconfig option like this:: @click.command() @click_utils.logconfig_option(disable_existing_loggers=True) def cli(): pass ''' def decorator(f): path_type = click.Path(exists=True, file_okay=True, dir_okay=False, writable=False, readable=True, resolve_path=True) attrs.setdefault('type', path_type) attrs.setdefault('expose_value', False) disable_existing_loggers = attrs.pop('disable_existing_loggers', False) fileconfig_defaults = attrs.pop('fileconfig_defaults', None) callback_kwargs = dict(defaults=fileconfig_defaults, disable_existing_loggers=disable_existing_loggers) attrs.setdefault('callback', logconfig_callback_factory(**callback_kwargs)) return click.option(*(param_decls or ('--logconfig',)), **attrs)(f) return decorator
[docs]def logger_callback_factory(logger_name='', handler_cls=None, handler_attrs=None, formatter_cls=None, formatter_attrs=None, filters=None, level=None, ctx_loglevel_attr=None, ctx_key=None): handler_attrs = handler_attrs if handler_attrs else {} formatter_attrs = formatter_attrs if formatter_attrs else {} filters = filters if filters else () given_level = level ctx_loglevel_attr = ctx_loglevel_attr if ctx_loglevel_attr else defaults.CONTEXT_LOGLEVEL_ATTR def inner(ctx, param, value): if not value or ctx.resilient_parsing: return handler = handler_cls(value, **handler_attrs) if formatter_cls is not None: formatter = formatter_cls(**formatter_attrs) handler.setFormatter(formatter) level = given_level if level is None: level = getattr(ctx, ctx_loglevel_attr, defaults.LOGLEVEL) handler.setLevel(level) logger = logging.getLogger(logger_name) logger.addHandler(handler) logger.setLevel(level) for filter_function in filters: logger.addFilter(filter_function) # save it in context if ``ctx_key`` was passed if ctx_key: setattr(ctx, ctx_key, logger) return value return inner
[docs]def logger_option(*param_decls, **attrs): ''' An abstract option for configuring a specific handler to a given logger (root logger by default). This logger is then attached to a context for further usage. This option accepts all the :param handler_cls: **required** a logging Handler class. A value of this option will be passed as a first positional argument while creating an instance of the ``handler_cls``. :param handler_attrs: a dictionary of keyword arguments passed to ``handler_cls`` while instantiating :param formatter_cls: a Formatter class which instance will be added to a handler :param formatter_attrs: a dictionary of keyword arguments passed to ``formatter_cls`` while instantiating :param filters: an iterable of filters to add to the logger :param level: enforce a minimum logging level on a logger and handler. If this is ``None`` then a callback will try to retrieve the level from context (e.g. that was stored by ``loglevel_option``). If it fails to find it a default level is ``logging.WARNING`` :param ctx_loglevel_attr: an attribute name of the context to **find** a stored logging level (by ``loglevel_option`` for example) :param ctx_key: an attribute name of the context to **store** a logger. Pass a *falsy* value to disable storing (default: ``None``). :param logger_name: a name of a logger to configure (default: ``''`` i.e. a root logger) ''' def decorator(f): callback_kwargs = { 'handler_cls' : attrs.pop('handler_cls'), 'handler_attrs' : attrs.pop('handler_attrs', {}), 'formatter_cls' : attrs.pop('formatter_cls', None), 'formatter_attrs' : attrs.pop('formatter_attrs', {}), 'filters' : attrs.pop('filters', None), 'level' : attrs.pop('level', None), 'ctx_loglevel_attr': attrs.pop('ctx_loglevel_attr', None), 'ctx_key' : attrs.pop('ctx_key', None), 'logger_name' : attrs.pop('logger_name', ''), } default_callback = logger_callback_factory(**callback_kwargs) attrs.setdefault('callback', default_callback) attrs.setdefault('expose_value', False) return click.option(*(param_decls or ('--logger',)), **attrs)(f) return decorator
[docs]def logfile_option(*param_decls, **attrs): ''' a specific type of ``logger_option`` that configures a logging file handler (by default it's a ``logging.handlers.RotatingFileHandler``) on a logger (root logger by default). In addition to all ``click_utils.logger_option`` and ``click.option`` arguments it accepts four keyword arguments to control handler creation and format settings. :param fmt: a format (``fmt``) argument for ``logging.Formatter`` class :param datefmt: a date format (``datefmt``) argument for ``logging.Formatter`` class :param maxBytes: a ``maxBytes`` argument for ``RotatingFileHandler`` class :param backupCount: a ``backupCount`` argument for ``RotatingFileHandler`` class ''' def decorator(f): path_type = click.Path(exists=False, file_okay=True, dir_okay=False, writable=True, readable=True, resolve_path=True) attrs.setdefault('type', path_type) fmt = attrs.pop('fmt', defaults.FORMAT) datefmt = attrs.pop('datefmt', defaults.DATEFMT) maxBytes = attrs.pop('maxBytes', defaults.MAXBYTES) backupCount = attrs.pop('backupCount', defaults.BACKUPCOUNT) # logger_option attrs attrs.setdefault('handler_cls', logging.handlers.RotatingFileHandler), attrs.setdefault('handler_attrs', {'maxBytes': maxBytes, 'backupCount': backupCount}), attrs.setdefault('formatter_cls', logging.Formatter), attrs.setdefault('formatter_attrs', {'fmt': fmt, 'datefmt': datefmt}), attrs.setdefault('filters', None), attrs.setdefault('level', None), attrs.setdefault('ctx_loglevel_attr', None), attrs.setdefault('ctx_key', None), attrs.setdefault('logger_name', ''), return logger_option(*(param_decls or ('--logfile',)), **attrs)(f) return decorator