# -*- 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)

