# -*- 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")
[docs]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)
[docs]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)