The autologging API — Autologging 0.4.0 documentation

# The `autologging` API

Release: | 0.4.0

`autologging.` `TRACE` = 1

  A custom tracing log level, lower in severity than `logging.DEBUG`. Autologging’s `traced()` and `TracedMethods()` create log records with this custom level.

`autologging.` `logged`(obj)

  Add a logger member to a decorated class or function.

  Parameters: | obj – the class or function object being decorated, or an optional `logging.Logger` object to be used as the parent logger (instead of the default module-named logger)
  Returns: | obj if obj is a class or function; otherwise, if obj is a logger, returns a lambda decorator that will in turn set the logger attribute and return obj

  If obj is a `class`, then `obj.__logger` will have the logger name “module-name.class-name”:

      >>> import sys
      >>> logging.basicConfig(
      ...     level=logging.DEBUG, stream=sys.stdout,
      ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
      >>> @logged
      ... class Sample:
      ...
      ...     def test(self):
      ...         self.__logger.debug("This is a test.")
      ...
      >>> Sample().test()
      DEBUG:autologging.Sample:test:This is a test.

  Note

  Autologging will prefer to use the class’s `__qualname__` when it is available (Python 3.3+). Otherwise, the class’s `__name__` is used. For example:

      class Outer:

         @logged
         class Nested: pass

  Under Python 3.3+, `Nested.__logger` will have the name “autologging.Outer.Nested”, while under Python 2.7 or 3.2, the logger name will be “autologging.Nested”.

  Changed in version 0.4.0: Functions decorated with `@logged` now use a single underscore in the logger variable name (e.g. `my_function._logger`) rather than a double underscore.

  If obj is a function, then `obj._logger` will have the logger name “module-name”:

      >>> import sys
      >>> logging.basicConfig(
      ...     level=logging.DEBUG, stream=sys.stdout,
      ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
      >>> @logged
      ... def test():
      ...     test._logger.debug("This is a test.")
      ...
      >>> test()
      DEBUG:autologging:test:This is a test.

  Note

  Within a logged function, the `_logger` attribute must be qualified by the function name.

  In the example above, note the call to `test._logger.debug`.

  If obj is a `logging.Logger` object, then that logger is used as the parent logger (instead of the default module-named logger):

      >>> import sys
      >>> logging.basicConfig(
      ...     level=logging.DEBUG, stream=sys.stdout,
      ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
      >>> _logger = logging.getLogger("test.parent")
      >>> @logged(_logger)
      ... class Sample:
      ...
      ...     def test(self):
      ...         self.__logger.debug("This is a test.")
      ...
      >>> Sample().test()
      DEBUG:test.parent.Sample:test:This is a test.

  Again, functions are similar:

      >>> import sys
      >>> logging.basicConfig(
      ...     level=logging.DEBUG, stream=sys.stdout,
      ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
      >>> _logger = logging.getLogger("test.parent")
      >>> @logged(_logger)
      ... def test():
      ...     test._logger.debug("This is a test.")
      ...
      >>> test()
      DEBUG:test.parent:test:This is a test.

  Note

  For classes, the logger member is made “private” (i.e. `__logger` with double underscore) to ensure that log messages that include the %(name)s format placeholder are written with the correct name.

  Consider a subclass of a `@logged`-decorated parent class. If the subclass were not decorated with `@logged` and could access the parent’s logger member directly to make logging calls, those log messages would display the name of the parent class, not the subclass.

  Therefore, subclasses of a `@logged`-decorated parent class that wish to use a provided `self.__logger` object must themselves be decorated with `@logged`.

  Warning

  Although the `@logged` and `@traced` decorators will “do the right thing” regardless of the order in which they are applied to the same function, it is recommended that `@logged` always be used as the innermost decorator:

      @traced
      @logged
      def my_function():
          my_function._logger.info("message")

  This is because `@logged` simply sets the `_logger` attribute and then returns the original function, making it “safe” to use in combination with any other decorator.

`autologging.` `traced`(obj)

  Add call and return tracing to an unbound function.

  Parameters: | obj – the function object being decorated, or an optional `logging.Logger` object to be used as the parent logger (instead of the default module-named logger)
  Returns: | if obj is a function, returns a proxy function that wraps obj to provide the call and return tracing; otherwise, if obj is a logger, returns another function decorator that creates and returns the tracing proxy function

  Note

  The original (proxied) function is available as the `__wrapped__` attribute of the returned proxy function. For example:

      >>> import sys
      >>> logging.basicConfig(
      ...     level=TRACE, stream=sys.stdout,
      ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
      >>> @traced
      ... def my_function(arg):
      ...     return arg.upper()
      ...
      >>> my_function.__wrapped__("test")
      'TEST'

  (Note that the tracing log records were not written because the original, non-traced function was called.)

  Warning

  This decorator will not quite work as expected (or may fail entirely) for class methods. To automatically trace class method call/return, please see the `TracedMethods` metaclass factory.

  In the following example, tracing log messages are written to a log whose name defaults to the function’s module name:

      >>> import sys
      >>> logging.basicConfig(
      ...     level=TRACE, stream=sys.stdout,
      ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
      >>> @traced
      ... def my_function(arg, keyword=None):
      ...     return "%s and %s" % (arg, keyword)
      ...
      >>> my_function("spam", keyword="eggs")
      TRACE:autologging:my_function:CALL *('spam',) **{'keyword': 'eggs'}
      TRACE:autologging:my_function:RETURN 'spam and eggs'
      'spam and eggs'

  In the following example, tracing log messages are written to a user-named log:

      >>> import sys
      >>> logging.basicConfig(
      ...     level=TRACE, stream=sys.stdout,
      ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
      >>> _logger = logging.getLogger("test.ing")
      >>> @traced(_logger)
      ... def my_function(arg, keyword=None):
      ...     return "%s and %s" % (arg, keyword)
      ...
      >>> my_function("spam", keyword="eggs")
      TRACE:test.ing:my_function:CALL *('spam',) **{'keyword': 'eggs'}
      TRACE:test.ing:my_function:RETURN 'spam and eggs'
      'spam and eggs'

  Warning

  Although the `@traced` and `@logged` decorators will “do the right thing” regardless of the order in which they are applied to the same function, it is recommended that `@logged` always be used as the innermost decorator:

      @traced
      @logged
      def my_function():
          my_function._logger.info("message")

  This is because `@logged` simply sets the `_logger` attribute and then returns the original function, making it “safe” to use in combination with any other decorator.

`autologging.` `TracedMethods`(*args, **options)

  Return a metaclass that provides call and return tracing for a class’s methods.

  The contents of args and options determine which methods will be traced, and which logger will be used for tracing messages:

  TracedMethods()

    In the simplest configuration, all “public”, “_nonpublic”, and “__private” methods that are defined in the class body (as well as the special `__init__` method, if defined) will be traced. The class `__dict__` will be inspected to identify these methods and replace them with tracing proxy methods. (Inherited methods are never traced. See the note below.) Tracing messages will be logged to the “module-name.class-name” logger at the `autologging.TRACE` level.

    Example:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> class Sample(metaclass=TracedMethods()):
        ...     @staticmethod
        ...     def static_method():
        ...         return "STATIC"
        ...     @classmethod
        ...     def class_method(cls):
        ...         return "CLASS"
        ...     def __init__(self):
        ...         self.public = "PUBLIC"
        ...     def method(self, arg, kw=None):
        ...         return "%s%s" % (self.public if kw is None else kw,
        ...                          arg)
        ...     def _nonpublic(self):
        ...         return "NON_PUBLIC"
        ...     def __private(self):
        ...         return "PRIVATE"
        ...     def __eq__(self, other):
        ...         return False
        ...
        >>> Sample.static_method()
        TRACE:autologging.Sample:static_method:CALL *() **{}
        TRACE:autologging.Sample:static_method:RETURN 'STATIC'
        'STATIC'
        >>> Sample.class_method()
        TRACE:autologging.Sample:class_method:CALL *() **{}
        TRACE:autologging.Sample:class_method:RETURN 'CLASS'
        'CLASS'
        >>> sample = Sample()
        TRACE:autologging.Sample:__init__:CALL *() **{}
        TRACE:autologging.Sample:__init__:RETURN None
        >>> sample.method(1)
        TRACE:autologging.Sample:method:CALL *(1,) **{}
        TRACE:autologging.Sample:method:RETURN 'PUBLIC1'
        'PUBLIC1'
        >>> sample.method(2, kw="METHOD")
        TRACE:autologging.Sample:method:CALL *(2,) **{'kw': 'METHOD'}
        TRACE:autologging.Sample:method:RETURN 'METHOD2'
        'METHOD2'
        >>> sample._nonpublic()
        TRACE:autologging.Sample:_nonpublic:CALL *() **{}
        TRACE:autologging.Sample:_nonpublic:RETURN 'NON_PUBLIC'
        'NON_PUBLIC'
        >>> sample._Sample__private()
        TRACE:autologging.Sample:__private:CALL *() **{}
        TRACE:autologging.Sample:__private:RETURN 'PRIVATE'
        'PRIVATE'
        >>> sample == 79  # calls __eq__ (not traced by default)
        False

  TracedMethods(logger)

    In this configuration, `args[0]` is a `logging.Logger` object. This configuration is identical to `TracedMethods()`, except that tracing messages will be logged to the “logger-name.class-name” logger.

    Example:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> _logger = logging.getLogger("my.named.logger")
        >>> class Sample(metaclass=TracedMethods(_logger)):
        ...     def method(self):
        ...         return "METHOD"
        ...
        >>> sample = Sample()  # inherited __init__ is not traced
        >>> sample.method()
        TRACE:my.named.logger.Sample:method:CALL *() **{}
        TRACE:my.named.logger.Sample:method:RETURN 'METHOD'
        'METHOD'

  TracedMethods(“method_1”, ... , “method_N”)

    In this configuration, only the explicitly-named methods will be traced.

    Example:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> class Sample(metaclass=TracedMethods("test", "_calc")):
        ...     def __init__(self, multiplicand):
        ...         self._multiplicand = multiplicand
        ...     def test(self, multiplier):
        ...         return "%.2f" % self._calc(abs(multiplier))
        ...     def _calc(self, multiplier):
        ...         return multiplier * self._multiplicand
        ...
        >>> sample = Sample(7)  # __init__ is not named, so not traced
        >>> sample.test(-9)
        TRACE:autologging.Sample:test:CALL *(-9,) **{}
        TRACE:autologging.Sample:_calc:CALL *(9,) **{}
        TRACE:autologging.Sample:_calc:RETURN 63
        TRACE:autologging.Sample:test:RETURN '63.00'
        '63.00'

  TracedMethods(logger, “method_1”, ... , “method_N”)

    This configuration is identical to `TracedMethods("method_1", ... , "method_N")`, except that tracing messages will be logged to the “logger-name.class-name” logger.

    Example:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> _logger = logging.getLogger("my.named.logger")
        >>> class Sample(
        ...         metaclass=TracedMethods(_logger, "test", "_calc")):
        ...     def __init__(self, multiplicand):
        ...         self._multiplicand = multiplicand
        ...     def test(self, multiplier):
        ...         return "%.2f" % self._calc(abs(multiplier))
        ...     def _calc(self, multiplier):
        ...         return multiplier * self._multiplicand
        ...
        >>> sample = Sample(7)  # __init__ is not named, so not traced
        >>> sample.test(-9)
        TRACE:my.named.logger.Sample:test:CALL *(-9,) **{}
        TRACE:my.named.logger.Sample:_calc:CALL *(9,) **{}
        TRACE:my.named.logger.Sample:_calc:RETURN 63
        TRACE:my.named.logger.Sample:test:RETURN '63.00'
        '63.00'

  TracedMethods(trace_special_methods=True)

    This configuration is identical to `TracedMethods()`, except that “__special__” methods (e.g. `__eq__`, `__iter__`, etc.) will also be traced.

    Example:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> class Sample(
        ...         metaclass=TracedMethods(trace_special_methods=True)):
        ...     def method(self):
        ...         return "METHOD"
        ...     def __eq__(self, other):
        ...         return False
        ...
        >>> sample = Sample()  # inherited __init__ is not traced
        >>> sample.method()
        TRACE:autologging.Sample:method:CALL *() **{}
        TRACE:autologging.Sample:method:RETURN 'METHOD'
        'METHOD'
        >>> sample == 79  # calls __eq__
        TRACE:autologging.Sample:__eq__:CALL *(79,) **{}
        TRACE:autologging.Sample:__eq__:RETURN False
        False

    Note

    The `trace_special_methods` option is ignored when methods are named explicitly in args.

  TracedMethods(logger, trace_special_methods=True)

    This configuration is identical to `TracedMethods(trace_special_methods=True)`, except that tracing messages are logged to the “logger-name.class-name” logger.

    Example:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> _logger = logging.getLogger("my.named.logger")
        >>> class Sample(
        ...         metaclass=TracedMethods(_logger,
        ...                                 trace_special_methods=True)):
        ...     def method(self):
        ...         return "METHOD"
        ...     def __eq__(self, other):
        ...         return False
        ...
        >>> sample = Sample()  # inherited __init__ is not traced
        >>> sample.method()
        TRACE:my.named.logger.Sample:method:CALL *() **{}
        TRACE:my.named.logger.Sample:method:RETURN 'METHOD'
        'METHOD'
        >>> sample == 79  # calls __eq__
        TRACE:my.named.logger.Sample:__eq__:CALL *(79,) **{}
        TRACE:my.named.logger.Sample:__eq__:RETURN False
        False

  Warning

  Inherited methods are never traced, even if they are named explicitly in args.

  If you wish to trace a method from a super class, you have two options:

  1. Use `TracedMethods` in the super class.

  2. Override the method in the subclass and trace it there. For example:

         class SubClass(SuperClass,
                        metaclass=TracedMethods("method_name")):
             ...
             def method_name(self):
                 return super().method_name()
             ...

  Warning

  When method names are specified explicitly via args, Autologging ensures that each method is actually defined in the body of the class being traced.

  If a defintion for any named method is not found in the class body, either because the method is inherited or because the name is misspelled, Autologging will issue a `UserWarning`.

  Note

  Regardless of whether a logger was explicitly passed in or not, the logger used by tracing methods is not available to the class or its instances.

  Note

  A class may be decorated by `logged()` and use `TracedMethods` as a metaclass without conflict.