autologging — Autologging 1.0.0 documentation

# Source code for autologging

    # -*- coding: utf-8 -*-

    # Copyright (c) 2013-2016 Matthew Zipay <mattz@ninthtest.net>
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    # SOFTWARE.

    __author__ = (
        "Matthew Zipay <mattz@ninthtest.net>, "
        "Simon Knopp <simon.knopp@pg.canterbury.ac.nz>")
    __version__ = "1.0.0"

    from functools import partial, wraps
    from inspect import isclass, isroutine
    import logging
    import sys
    from types import FunctionType, MethodType
    import warnings

    __all__ = [
        "TRACE",
        "logged",
        "traced",
    ]

    #: A custom tracing log level, lower in severity than :attr:`logging.DEBUG`.
    TRACE = 1
    logging.addLevelName(TRACE, "TRACE")

    def logged(obj):
        """Add a logger member to a decorated class or function.

        :param obj: the class or function object being decorated, or an
                    optional :class:`logging.Logger` object to be used as
                    the parent logger (instead of the default module-named
                    logger)
        :return: *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 :obj:`class`, then ``obj.__log`` 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.__log.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.__log`` will have the name
           "autologging.Outer.Nested", while under Python 2.7 or 3.2, the
           logger name will be "autologging.Nested".

        .. versionchanged:: 0.4.0
           Functions decorated with ``@logged`` use a *single* underscore
           in the logger variable name (e.g. ``my_function._log``) rather
           than a double underscore.

        If *obj* is a function, then ``obj._log`` 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._log.debug("This is a test.")
        ...
        >>> test()
        DEBUG:autologging:test:This is a test.

        .. note::
           Within a logged function, the ``_log`` attribute must be
           qualified by the function name.

        If *obj* is a :class:`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")
        >>> @logged(logging.getLogger("test.parent"))
        ... class Sample:
        ...     def test(self):
        ...         self.__log.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")
        >>> @logged(logging.getLogger("test.parent"))
        ... def test():
        ...     test._log.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. ``__log``
           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.__log`` 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._log.info("message")

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

        """
        if isinstance(obj, logging.Logger): # `@logged(logger)'
            return lambda class_or_fn: _add_logger_to(
                class_or_fn,
                logger_name=_generate_logger_name(
                    class_or_fn, parent_name=obj.name))
        else: # `@logged'
            return _add_logger_to(obj)

    def traced(*args):
        """Add call and return tracing to an unbound function or to the
        methods of a class.

        The arguments to ``traced`` differ depending on whether it is being
        used to trace an unbound function or the methods of a class:

        .. rubric:: Trace an unbound function using the default logger

        :param func: the unbound function to be traced

        By default, a logger named for the function's module is used:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> @traced
        ... def func(x, y):
        ...     return x + y
        ...
        >>> func(7, 9)
        TRACE:autologging:func:CALL *(7, 9) **{}
        TRACE:autologging:func:RETURN 16
        16

        .. rubric:: Trace an unbound function using a named logger

        :param logging.Logger logger: the parent logger used to trace the\
                                      unbound function

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> @traced(logging.getLogger("my.channel"))
        ... def func(x, y):
        ...     return x + y
        ...
        >>> func(7, 9)
        TRACE:my.channel:func:CALL *(7, 9) **{}
        TRACE:my.channel:func:RETURN 16
        16

        .. rubric:: Trace default methods using the default logger

        :param class_: the class whose methods will be traced

        By default, all "public", "_nonpublic", and "__internal" methods, as
        well as the special "__init__" method, will be traced. Tracing log
        entries will be written to a logger named for the module and class:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> @traced
        ... class Class:
        ...     def __init__(self, x):
        ...         self._x = x
        ...     def public(self, y):
        ...         return self._x + y
        ...     def _nonpublic(self, y):
        ...         return self._x - y
        ...     def __internal(self, y=2):
        ...         return self._x ** y
        ...     def __repr__(self):
        ...         return "Class(%r)" % self._x
        ...
        >>> obj = Class(7)
        TRACE:autologging.Class:__init__:CALL *(7,) **{}
        >>> obj.public(9)
        TRACE:autologging.Class:public:CALL *(9,) **{}
        TRACE:autologging.Class:public:RETURN 16
        16
        >>> obj._nonpublic(5)
        TRACE:autologging.Class:_nonpublic:CALL *(5,) **{}
        TRACE:autologging.Class:_nonpublic:RETURN 2
        2
        >>> obj._Class__internal(y=3)
        TRACE:autologging.Class:__internal:CALL *() **{'y': 3}
        TRACE:autologging.Class:__internal:RETURN 343
        343
        >>> repr(obj) # not traced by default
        'Class(7)'

        .. note::
           When the runtime Python version is >= 3.3, the *qualified* class
           name will be used to name the tracing logger (i.e. a nested class
           will write tracing log entries to a logger named
           "module.Parent.Nested").

        .. rubric:: Trace default methods using a named logger

        :param logging.Logger logger: the parent logger used to trace the
                                      methods of the class

        By default, all "public", "_nonpublic", and "__internal" methods, as
        well as the special "__init__" method, will be traced. Tracing log
        entries will be written to the specified logger:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> @traced(logging.getLogger("my.channel"))
        ... class Class:
        ...     def __init__(self, x):
        ...         self._x = x
        ...     def public(self, y):
        ...         return self._x + y
        ...     def _nonpublic(self, y):
        ...         return self._x - y
        ...     def __internal(self, y=2):
        ...         return self._x ** y
        ...     def __repr__(self):
        ...         return "Class(%r)" % self._x
        ...
        >>> obj = Class(7)
        TRACE:my.channel.Class:__init__:CALL *(7,) **{}
        >>> obj.public(9)
        TRACE:my.channel.Class:public:CALL *(9,) **{}
        TRACE:my.channel.Class:public:RETURN 16
        16
        >>> obj._nonpublic(5)
        TRACE:my.channel.Class:_nonpublic:CALL *(5,) **{}
        TRACE:my.channel.Class:_nonpublic:RETURN 2
        2
        >>> obj._Class__internal(y=3)
        TRACE:my.channel.Class:__internal:CALL *() **{'y': 3}
        TRACE:my.channel.Class:__internal:RETURN 343
        343
        >>> repr(obj) # not traced by default
        'Class(7)'

        .. rubric:: Trace specified methods using the default logger

        :param tuple method_names: the names of the methods that will be
                                   traced

        Tracing log entries will be written to a logger named for the
        module and class:

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> @traced("public", "__internal")
        ... class Class:
        ...     def __init__(self, x):
        ...         self._x = x
        ...     def public(self, y):
        ...         return self._x + y
        ...     def _nonpublic(self, y):
        ...         return self._x - y
        ...     def __internal(self, y=2):
        ...         return self._x ** y
        ...     def __repr__(self):
        ...         return "Class(%r)" % self._x
        ...
        >>> obj = Class(7)
        >>> obj.public(9)
        TRACE:autologging.Class:public:CALL *(9,) **{}
        TRACE:autologging.Class:public:RETURN 16
        16
        >>> obj._nonpublic(5)
        2
        >>> obj._Class__internal(y=3)
        TRACE:autologging.Class:__internal:CALL *() **{'y': 3}
        TRACE:autologging.Class:__internal:RETURN 343
        343
        >>> repr(obj)
        'Class(7)'

        .. 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. (This means that inherited
           methods that are not overridden are **never** traced, even if
           they are named explicitly in *args*.)

           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 :exc:`UserWarning`.

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

           1. Use ``traced`` to decorate the super class.
           2. Override the method and trace it in the subclass.

        .. note::
           When the runtime Python version is >= 3.3, the *qualified* class
           name will be used to name the tracig logger (i.e. a nested class
           will write tracing log entries to a logger named
           "module.Parent.Nested").

        .. rubric:: Trace specified methods using a named logger

        :param logging.Logger logger: the parent logger used to trace the
                                      methods of the class
        :param tuple method_names: the names of the methods that will be
                                   traced

        >>> import sys
        >>> logging.basicConfig(
        ...     level=TRACE, stream=sys.stdout,
        ...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
        >>> @traced(logging.getLogger("my.channel"), "public", "__internal")
        ... class Class:
        ...     def __init__(self, x):
        ...         self._x = x
        ...     def public(self, y):
        ...         return self._x + y
        ...     def _nonpublic(self, y):
        ...         return self._x - y
        ...     def __internal(self, y=2):
        ...         return self._x ** y
        ...     def __repr__(self):
        ...         return "Class(%r)" % self._x
        ...
        >>> obj = Class(7)
        >>> obj.public(9)
        TRACE:my.channel.Class:public:CALL *(9,) **{}
        TRACE:my.channel.Class:public:RETURN 16
        16
        >>> obj._nonpublic(5)
        2
        >>> obj._Class__internal(y=3)
        TRACE:my.channel.Class:__internal:CALL *() **{'y': 3}
        TRACE:my.channel.Class:__internal:RETURN 343
        343
        >>> repr(obj) # not traced by default
        'Class(7)'

        .. 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. (This means that inherited
           methods that are not overridden are **never** traced, even if
           they are named explicitly in *args*.)

           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 :exc:`UserWarning`.

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

           1. Use ``traced`` to decorate the super class.
           2. Override the method and trace it in the subclass.

        .. note::
           When tracing a class, if the default (class-named) logger is
           used **and** the runtime Python version is >= 3.3, then the
           *qualified* class name will be used to name the tracig logger
           (i.e. a nested class will write tracing log entries to a logger
           named "module.Parent.Nested").

        .. note::
           If method names are specified when decorating a function, a
           :exc:`UserWarning` is issued, but the methods names are ignored
           and the function is traced as though the method names had not
           been specified.

        """
        obj = args[0] if args else None
        if obj is None:
            # treat `@traced()' as equivalent to `@traced'
            return traced

        if isclass(obj): # `@traced' class
            return _install_traceable_methods(obj)
        elif isroutine(obj): # `@traced' function
            return _make_traceable_function(
                obj, logging.getLogger(_generate_logger_name(obj)))
        elif isinstance(obj, logging.Logger):
            # may be decorating a class OR a function
            method_names = args[1:]

            def traced_decorator(class_or_fn):
                if isclass(class_or_fn):
                    # `@traced(logger)' or `@traced(logger, "method_name1", ..)' class
                    return _install_traceable_methods(
                        class_or_fn, *method_names,
                        logger=logging.getLogger(
                            _generate_logger_name(
                                class_or_fn, parent_name=obj.name)))
                else: # `@traced(logger)' function
                    if method_names:
                        warnings.warn(
                            "ignoring method names for @traced function %s.%s" %
                                (class_or_fn.__module__, class_or_fn.__name__))
                    return _make_traceable_function(class_or_fn, obj)

            return traced_decorator
        else: # `@traced("method_name1", ..)' class
            method_names = args[:]
            return lambda class_: _install_traceable_methods(class_, *method_names)

    def _generate_logger_name(obj, parent_name=None):
        """Generate the logger name (channel) for a class or function.

        :param obj: a class or function
        :keyword str parent_name: the name of *obj*'s parent logger
        :rtype: str

        If *parent_name* is not specified, the default is to use *obj*'s
        module name.

        """
        parent_logger_name = parent_name if parent_name else obj.__module__
        return "%s.%s" % (
                parent_logger_name, getattr(obj, "__qualname__", obj.__name__)) \
            if isclass(obj) else parent_logger_name

    def _add_logger_to(obj, logger_name=None):
        """Set a :class:`logging.Logger` member on *obj*.

        :param obj: a class or function object
        :keyword str logger_name: the name (channel) of the logger for *obj*
        :return: *obj*

        If *obj* is a class, the member will be named "__log". If *obj* is a
        function, the member will be named "_log".

        """
        logger = logging.getLogger(
            logger_name if logger_name else _generate_logger_name(obj))

        if isclass(obj):
            setattr(obj, _mangle_name("__log", obj.__name__), logger)
        else: # function
            obj._log = logger

        return obj

    def _make_traceable_function(function, logger):
        """Return a tracing proxy function for *function*.

        :param function: an unbound, module-level (or nested) function
        :param logging.Logger logger: the logger for tracing messages
        :return: a proxy function that wraps *function* to provide the call
                 and return tracing

        """
        make_call_record, make_return_record = _create_log_record_factories(
            logger.name, function)

        @wraps(function)
        def autologging_traced_function_proxy(*args, **keywords):
            trace_log = autologging_traced_function_proxy._trace_log
            if trace_log.isEnabledFor(TRACE):
                trace_log.handle(make_call_record(args, keywords))
                return_value = function(*args, **keywords)
                trace_log.handle(make_return_record(return_value))
                return return_value
            else:
                return function(*args, **keywords)

        # we don't use @logged(logger) in this case because then a function that
        # was both @logged and @traced (in either order) would have the _log member
        # overwritten by the outermost decorator
        autologging_traced_function_proxy._trace_log = logger

        if not hasattr(autologging_traced_function_proxy, "__wrapped__"):
            # __wrapped__ is only set by functools.wraps() in Python 3.2+
            autologging_traced_function_proxy.__wrapped__ = function

        autologging_traced_function_proxy.__autologging_traced__ = True

        return autologging_traced_function_proxy

    # can't use option=<default> keywords with *args in Python 2.7 (see PEP-3102)
    def _install_traceable_methods(class_, *method_names, **keywords):
        """Substitute tracing proxy methods for the methods named in
        *method_names* in *class_*'s ``__dict__``.

        :param class_: a class being traced
        :param tuple method_names: the names of the methods to be traced
        :keyword logging.Logger logger: the logger to use for tracing

        If *method_names* is empty, all "public", "_nonpublic", and
        "__internal" methods, as well as the special "__init__" method, will
        be traced by default.

        If *logger* is unspecified, a default logger will be used to log
        tracing messages.

        """
        logger = keywords.get(
            "logger", logging.getLogger(_generate_logger_name(class_)))

        if method_names:
            traceable_method_names = _get_traceable_method_names(
                method_names, class_)
        else:
            traceable_method_names = _get_default_traceable_method_names(class_)

        # replace each named method with a tracing proxy method
        for method_name in traceable_method_names:
            descriptor = class_.__dict__[method_name]
            descriptor_type = type(descriptor)

            if descriptor_type is FunctionType:
                make_traceable_method = _make_traceable_instancemethod
            elif descriptor_type is classmethod:
                make_traceable_method = _make_traceable_classmethod
            elif descriptor_type is staticmethod:
                make_traceable_method = _make_traceable_staticmethod
            else:
                # should be unreachable, but issue a warning just in case
                warnings.warn("tracing not supported for %r" % descriptor_type)
                continue

            tracing_proxy_descriptor = make_traceable_method(
                class_, descriptor, logger)

            # class_.__dict__ is a mappingproxy; direct assignment not supported
            setattr(class_, method_name, tracing_proxy_descriptor)

        return class_

    def _get_traceable_method_names(method_names, class_):
        """Filter (and possibly mangle) *method_names* so that only method
        names actually defined as methods in *cls_dict* remain.

        :param method_names: a sequence of names that should identify
                             methods defined in *class_*
        :param class_: the class being traced
        :return: a sequence of names identifying methods that are defined in
                 *class_* that will be traced
        :rtype: list

        .. warning::
           A :exc:`UserWarning` is issued if any method name specified in
           *method_names* is not actually defined in *class_*.

        """
        traceable_method_names = []

        for name in method_names:
            mname = (
                name if not _is_internal_name(name) else
                _mangle_name(name, class_.__name__))

            if isroutine(class_.__dict__.get(mname)):
                traceable_method_names.append(mname)
            else:
                warnings.warn(
                    "%r does not identify a method defined in %s" %
                        (name, class_.__name__))

        return traceable_method_names

    def _get_default_traceable_method_names(class_):
        """Return all names in *cls_dict* that identify methods.

        :param class_: the class being traced
        :return: a sequence of names identifying methods of *class_* that
                 will be traced
        :rtype: list

        """
        default_traceable_method_names = []

        for (name, member) in class_.__dict__.items():
            if isroutine(member) and (
                    not _is_special_name(name) or
                    name == "__init__"):
                default_traceable_method_names.append(name)

        return default_traceable_method_names

    def _is_internal_name(name):
        """Determine whether or not *name* is an "__internal" name.

        :param str name: a name defined in a class ``__dict__``
        :return: ``True`` if *name* is an "__internal" name, else ``False``
        :rtype: bool

        """
        return name.startswith("__") and not name.endswith("__")

    def _mangle_name(internal_name, class_name):
        """Transform *name* (which is assumed to be an "__internal" name)
        into a "_ClassName__internal" name.

        :param str internal_name: the assumed-to-be-"__internal" member name
        :param str class_name: the name of the class where *name* is defined
        :return: the transformed "_ClassName__internal" name
        :rtype: str

        """
        return "_%s%s" % (class_name.lstrip('_'), internal_name)

    def _is_special_name(name):
        """Determine whether or not *name* is a "__special__" name.

        :param str name: a name defined in a class ``__dict__``
        :return: ``True`` if *name* is a "__special__" name, else ``False``
        :rtype: bool

        """
        return name.startswith("__") and name.endswith("__")

    def _make_traceable_instancemethod(class_, method_descriptor, logger):
        """Create a method descriptor that returns the tracing proxy
        function for the instance method described by *method_descriptor*.

        :param class_: the class that owns *method_descriptor*
        :param method_descriptor: the method descriptor of the instance
                                  method being traced (i.e. the function)
        :param logging.Logger logger: the logger that will be used for
                                      tracing call and return messages
        :return: a method descriptor that returns the tracing proxy function

        When *logger* is not enabled for the :attr:`autologging.TRACE`
        level, the tracing proxy function will delegate directly to the
        original instance method.

        The original unbound function is available from the proxy
        descriptor's ``__func__.__wrapped__`` attribute.

        """
        # functions have a __get__ method; they can act as descriptors
        function = method_descriptor

        make_call_record, make_return_record = _create_log_record_factories(
            logger.name, function)

        @wraps(function)
        @logged(logger)
        def autologging_traced_instancemethod_proxy(f_self, *args, **keywords):
            method = method_descriptor.__get__(f_self, f_self.__class__)
            log = autologging_traced_instancemethod_proxy._log
            if log.isEnabledFor(TRACE):
                log.handle(make_call_record(args, keywords))
                return_value = method(*args, **keywords)
                log.handle(make_return_record(return_value))
                return return_value
            else:
                return method(*args, **keywords)

        if not hasattr(autologging_traced_instancemethod_proxy, "__wrapped__"):
            # __wrapped__ is only set by functools.wraps() in Python 3.2+
            autologging_traced_instancemethod_proxy.__wrapped__ = function

        autologging_traced_instancemethod_proxy.__autologging_traced__ = True

        return autologging_traced_instancemethod_proxy

    def _make_traceable_classmethod(class_, method_descriptor, logger):
        """Create a method descriptor that returns the tracing proxy
        function for the class method described by *method_descriptor*.

        :param class_: the class that owns *method_descriptor*
        :param method_descriptor: the method descriptor of the instance
                                  method being traced
        :param logging.Logger logger: the logger that will be used for
                                      tracing call and return messages
        :return: a method descriptor that returns the tracing proxy function

        When *logger* is not enabled for the :attr:`autologging.TRACE`
        level, the tracing proxy function will delegate directly to the
        original class method.

        The original class method is available from the proxy descriptor's
        ``__func__.__wrapped__`` attribute.

        """
        function = method_descriptor.__func__

        make_call_record, make_return_record = _create_log_record_factories(
            logger.name, function)

        @wraps(function)
        @logged(logger)
        def autologging_traced_classmethod_proxy(f_cls, *args, **keywords):
            method = method_descriptor.__get__(None, f_cls)
            log = autologging_traced_classmethod_proxy._log
            if log.isEnabledFor(TRACE):
                log.handle(make_call_record(args, keywords))
                return_value = method(*args, **keywords)
                log.handle(make_return_record(return_value))
                return return_value
            else:
                return method(*args, **keywords)

        if not hasattr(autologging_traced_classmethod_proxy, "__wrapped__"):
            # __wrapped__ is only set by functools.wraps() in Python 3.2+
            autologging_traced_classmethod_proxy.__wrapped__ = function

        autologging_traced_classmethod_proxy.__autologging_traced__ = True

        return classmethod(autologging_traced_classmethod_proxy)

    def _make_traceable_staticmethod(class_, method_descriptor, logger):
        """Create a method descriptor that returns the tracing proxy
        function for the class method described by *method_descriptor*.

        :param class_: the class that owns *method_descriptor*
        :param method_descriptor: the method descriptor of the instance
                                  method being traced
        :param logging.Logger logger: the logger that will be used for
                                      tracing call and return messages
        :return: a method descriptor that returns the tracing proxy function

        When *logger* is not enabled for the :attr:`autologging.TRACE`
        level, the tracing proxy function will delegate directly to the
        original class method.

        The original class method is available from the proxy descriptor's
        ``__func__.__wrapped__`` attribute.

        """
        function = method_descriptor.__func__

        make_call_record, make_return_record = _create_log_record_factories(
            logger.name, function)

        @wraps(function)
        @logged(logger)
        def autologging_traced_staticmethod_proxy(*args, **keywords):
            log = autologging_traced_staticmethod_proxy._log
            if log.isEnabledFor(TRACE):
                log.handle(make_call_record(args, keywords))
                return_value = function(*args, **keywords)
                log.handle(make_return_record(return_value))
                return return_value
            else:
                return function(*args, **keywords)

        if not hasattr(autologging_traced_staticmethod_proxy, "__wrapped__"):
            # __wrapped__ is only set by functools.wraps() in Python 3.2+
            autologging_traced_staticmethod_proxy.__wrapped__ = function

        autologging_traced_staticmethod_proxy.__autologging_traced__ = True

        return staticmethod(autologging_traced_staticmethod_proxy)

    def _create_log_record_factories(channel, function):
        """Create factory functions that create :class:`logging.LogRecord`
        objects for call and return tracing.

        :param str channel: the logging channel to which tracing log entries
                            will be emitted
        :param function: the traced function
        :return: a 2-tuple containing the factory functions for creating
                 call and return log records for *function* (respectively)

        """
        f_code = function.__code__
        make_call_record = partial(
            _make_call_record, channel, function.__name__, f_code.co_filename,
            f_code.co_firstlineno)
        make_return_record = partial(
            _make_return_record, channel, function.__name__, f_code.co_filename,
            _find_last_line_number(f_code))
        return (make_call_record, make_return_record)

    def _make_call_record(
            channel, f_name, f_filename, f_lineno, f_args, f_keywords):
        """Create the log record for a function call.

        :param str channel: the logging channel to which tracing log entries
                            will be emitted
        :param str f_name: the name of the traced function
        :param str f_filename: the filename in which the traced function is
                               defined
        :param int f_lineno: the line number in *f_filename* where the
                             traced function is defined
        :param tuple f_args: the positional arguments that were sent to
                             the traced function
        :param dict f_keywords: the keyword arguments that were sent to
                                the traced function
        :rtype: logging.LogRecord

        """
        return logging.LogRecord(
            channel, TRACE, f_filename, f_lineno, "CALL *%r **%r",
            (f_args, f_keywords), None, func=f_name)

    _is_py3 = (sys.version_info[0] >= 3)

    def _find_last_line_number(f_code):
        """Return the last line number of a function.

        :param types.CodeType f_code: the bytecode object for a
                                      function, as obtained from
                                      ``function.__code__``
        :return: the last physical line number of the function
        :rtype: int

        """
        last_line_number = f_code.co_firstlineno
        # co_lnotab is a sequence of 2-byte offsets
        # (address offset, line number offset), each relative to the previous;
        # we only care about the line number offsets here, so start at index 1
        # and increment by 2
        i = 1
        while i < len(f_code.co_lnotab):
            # co_lnotab is bytes in Python 3, but str in Python 2
            last_line_number += (
                f_code.co_lnotab[i] if _is_py3
                else ord(f_code.co_lnotab[i]))
            i += 2
        return last_line_number

    def _make_return_record(channel, f_name, f_filename, f_lineno, f_return):
        """Emit the log record for a function return.

        :param str channel: the logging channel to which tracing log entries
                            will be emitted
        :param str f_name: the name of the traced function
        :param str f_filename: the filename in which the traced function is
                               defined
        :param int f_lineno: the line number in *f_filename* of the traced
                             function's last physical line
        :param f_return: the value that was returned by the traced function
        :rtype: logging.LogRecord

        """
        return logging.LogRecord(
            channel, TRACE, f_filename, f_lineno, "RETURN %r", (f_return,), None,
            func=f_name)