Source code for autologging

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

# Copyright 2013, 2015, 2016, 2018, 2019 Matthew Zipay.
#
# 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 (mattzATninthtestDOTinfo)"
__version__ = "1.3.2"

from functools import wraps
from inspect import isclass, isgenerator, isroutine
import logging
import os
import platform
import sys
from types import FunctionType
import warnings

# BEGIN Jython/IronPython detection
# (this needs to be implemented consistently w/r/t Aglyph's aglyph._compat)
try:
    _py_impl = platform.python_implementation()
except:
    _py_impl = "Python"

_is_jython = \
    _py_impl == "Jython" and getattr(sys, "JYTHON_JAR", None) is not None

try:
    import clr
    clr.AddReference("System")
    _has_clr = True
except:
    _has_clr = False

_is_ironpython = _py_impl == "IronPython" and _has_clr
# END Jython/IronPython detection

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

#: 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. :arg 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, return 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. .. note:: Both `Jython <http://www.jython.org/>`_ and `IronPython <http://ironpython.net/>`_ report an "internal" class name using its mangled form, which will be reflected in the default logger name. For example, in the sample code below, both Jython and IronPython will use the default logger name "autologging._Outer__Nested" (whereas CPython/PyPy/Stackless would use "autologging.__Nested" under Python 2 or "autologging.Outer.__Nested" under Python 3.3+) :: class Outer: @logged class __Nested: pass .. warning:: `IronPython <http://ironpython.net/>`_ does not fully support frames (even with the -X:FullFrames option), so you are likely to see things like misreported line numbers and "<unknown file>" in log records emitted when running under IronPython. """ 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, **keywords): """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 :arg 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 :arg 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 :arg class_: the class whose methods will be traced By default, all "public", "_nonpublic", and "__internal" methods, as well as the special "__init__" and "__call__" methods, 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 ... def __call__(self): ... return 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)' >>> obj() TRACE:autologging.Class:__call__:CALL *() **{} TRACE:autologging.Class:__call__:RETURN 7 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 :arg 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 ... def __call__(self): ... return 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)' >>> obj() TRACE:my.channel.Class:__call__:CALL *() **{} TRACE:my.channel.Class:__call__:RETURN 7 7 .. rubric:: Trace specified methods using the default logger :arg 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 ... def __call__(self): ... return 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)' >>> obj() 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 tracing 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 :arg logging.Logger logger: the parent logger used to trace the methods of the class :arg 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 ... def __call__(self): ... return 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)' >>> obj() 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. .. rubric:: Exclude specified methods from tracing .. versionadded:: 1.3.0 :arg tuple method_names: the names of the methods that will be excluded from tracing :keyword bool exclude: ``True`` to cause the method names list to be interpreted as an exclusion list (``False`` is the default, and causes the named methods to be **included** as described above) The example below demonstrates exclusions using the default logger. >>> import sys >>> logging.basicConfig( ... level=TRACE, stream=sys.stdout, ... format="%(levelname)s:%(name)s:%(funcName)s:%(message)s") >>> @traced("_nonpublic", "__internal", exclude=True) ... 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 ... def __call__(self): ... return 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) 343 >>> repr(obj) 'Class(7)' >>> obj() TRACE:autologging.Class:__call__:CALL *() **{} TRACE:autologging.Class:__call__:RETURN 7 7 When method names are excluded via *args* and the *exclude* keyword, Autologging **ignores** methods that are not actually defined in the body of the class being traced. .. warning:: If an exclusion list causes the list of traceable methods to resolve empty, then Autologging will issue a :exc:`UserWarning`. .. 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"). .. 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 tracing 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. .. note:: Both `Jython <http://www.jython.org/>`_ and `IronPython <http://ironpython.net/>`_ report an "internal" class name using its mangled form, which will be reflected in the default tracing logger name. For example, in the sample code below, both Jython and IronPython will use the default tracing logger name "autologging._Outer__Nested" (whereas CPython/PyPy/Stackless would use "autologging.__Nested" under Python 2 or "autologging.Outer.__Nested" under Python 3.3+):: class Outer: @traced class __Nested: pass .. warning:: Neither `Jython <http://www.jython.org/>`_ nor `IronPython <http://ironpython.net/>`_ currently implement the ``function.__code__.co_lnotab`` attribute, so the last line number of a function cannot be determined by Autologging. .. versionchanged:: 1.3.1 Due to unavoidable inconsistencies in line number tracking across Python variants (see `issues/6 <https://github.com/mzipay/Autologging/issues/6>`_, as of version 1.3.1 and until further notice Autologging will only record the first line number of the function being traced in all tracing CALL and RETURN records. (Note that YIELD tracing records for generator iterators will continue to record the correct line number on variants other than IronPython.) """ 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, exclude=keywords.get("exclude", False)) 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:] exclude = keywords.get("exclude", False) def traced_decorator(class_or_fn): if isclass(class_or_fn): # `@traced(logger)' or `@traced(logger, "method", ..)' class return _install_traceable_methods( class_or_fn, *method_names, exclude=exclude, 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[:] exclude = keywords.get("exclude", False) return lambda class_: _install_traceable_methods(class_, *method_names, exclude=exclude)
__traced_original = traced def _traced_noop(*args, **keywords): """Turn the ``@traced`` decorator into a no-op.""" obj = args[0] if args else None if obj is None: # treat `@traced()' as equivalent to `@traced' return _traced_noop if isclass(obj) or isroutine(obj): # `@traced' class or function return obj else: # `@traced(logger)' or `@traced("method_name1", ..)' def traced_noop_decorator(class_or_fn): return class_or_fn return traced_noop_decorator
[docs]def install_traced_noop(): """Replace the :func:`traced` decorator with an identity (no-op) decorator. Although the overhead of a ``@traced`` function or method is minimal when the :data:`TRACED` log level is disabled, there is still *some* overhead (the logging level check, an extra function call). If you would like to completely *eliminate* this overhead, call this function **before** any classes or functions in your application are decorated with ``@traced``. The :func:`traced` decorator will be replaced with a no-op decorator that simply returns the class or function unmodified. .. note:: The **recommended** way to install the no-op ``@traced`` decorator is to set the ``AUTOLOGGING_TRACED_NOOP`` environment variable to any non-empty value. If the ``AUTOLOGGING_TRACED_NOOP`` environment variable is set to a non-empty value when Autologging is loaded, the ``@traced`` no-op will be installed automatically. As an alternative to setting the ``AUTOLOGGING_TRACED_NOOP`` environment variable, you can also call this function directly in your application's bootstrap module. For example:: import autologging if running_in_production: autologging.install_traced_noop() .. warning:: This function **does not** "revert" any already-``@traced`` class or function! It simply replaces the ``autologging.traced`` module reference with a no-op. For this reason it is imperative that ``autologging.install_traced_noop()`` be called **before** the ``@traced`` decorator has been applied to any class or function in the application. (This is why the ``AUTOLOGGING_TRACED_NOOP`` environment variable is the recommended approach for installing the no-op - it allows Autologging itself to guarantee that the no-op is installed before any classes or functions are decorated.) """ global traced traced = _traced_noop logging.getLogger().info("autologging.traced no-op is installed")
if os.getenv("AUTOLOGGING_TRACED_NOOP"): install_traced_noop() def _generate_logger_name(obj, parent_name=None): """Generate the logger name (channel) for a class or function. :arg 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*. :arg 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): """Create a function that delegates to either a tracing proxy or the original *function*. :arg function: an unbound, module-level (or nested) function :arg logging.Logger logger: the tracing logger :return: a function that wraps *function* to provide the call and return tracing support If *logger* is not enabled for the :attr:`autologging.TRACE` level **at the time the returned delegator function is invoked**, then the original *function* is called instead of the tracing proxy. The overhead that a ``@traced`` function incurs when tracing is **disabled** is: * the delegator function call itself * the ``TRACE`` level check. The original *function* is available from the delegator function's ``__wrapped__`` attribute. """ proxy = _FunctionTracingProxy(function, logger) @wraps(function) def autologging_traced_function_delegator(*args, **keywords): if logger.isEnabledFor(TRACE): # don't access the proxy from closure (IronPython does not manage # co_freevars/__closure__ correctly for local vars) proxy = autologging_traced_function_delegator._tracing_proxy return proxy(function, args, keywords) else: return function(*args, **keywords) autologging_traced_function_delegator._tracing_proxy = proxy if not hasattr(autologging_traced_function_delegator, "__wrapped__"): # __wrapped__ is only set by functools.wraps() in Python 3.2+ autologging_traced_function_delegator.__wrapped__ = function autologging_traced_function_delegator.__autologging_traced__ = True return autologging_traced_function_delegator # 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__``. :arg class_: a class being traced :arg tuple method_names: the names of the methods to be traced :keyword logging.Logger logger: the logger to use for tracing :keyword bool exclude: ``True`` to interpret *method_names* as an **exclusion** list rather than an inclusion list If *method_names* is empty and the *exclude* keyword is ``False`` (the default), then all "public", "_nonpublic", and "__internal" methods, as well as the special "__init__" and "__call__" methods, will be traced by default. If the *exclude* keyword is ``True``, then the methods that will be traced are the default methods (as identified above) **MINUS** any methods named in *method_names*. 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_, exclude=keywords.get("exclude", False)) 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(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_, **keywords): """Filter (and possibly mangle) *method_names* so that only method names actually defined as methods in *cls_dict* remain. :arg method_names: a sequence of names that should identify methods defined in *class_* :arg class_: the class being traced :keyword bool exclude: ``True`` to interpret *method_names* as an **exclusion** list rather than an inclusion list :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 **included** method named in *method_names* is not actually defined in *class_*; or if the result of filtering **excluded** methods results in an empty list. """ exclude = keywords.get("exclude", False) traceable_method_names = [] if not keywords.get("exclude", False): 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__)) else: traceable_method_names = [ name for name in _get_default_traceable_method_names(class_) if _unmangle_name(name, class_.__name__) not in method_names] if not traceable_method_names: warnings.warn(( "exclude=True with the supplied method names results in NO " "traceable methods for %s") % class_.__name__) return traceable_method_names def _get_default_traceable_method_names(class_): """Return all names in *cls_dict* that identify methods. :arg 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 in ("__init__", "__call__")): 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. :arg 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 *internal_name* (which is assumed to be an "__internal" name) into a "_ClassName__internal" name. :arg str internal_name: the assumed-to-be-"__internal" member name :arg str class_name: name of the class where *internal_name* is defined :return: the transformed "_ClassName__internal" name :rtype: str """ return "_%s%s" % (class_name.lstrip('_'), internal_name) def _unmangle_name(mangled_name, class_name): """Transform *mangled_name* (which is assumed to be a "_ClassName__internal" name) into an "__internal" name. :arg str mangled_name: a mangled "_ClassName__internal" member name :arg str class_name: name of the class where the (unmangled) name is defined :return: the transformed "__internal" name :rtype: str """ return mangled_name.replace("_%s" % class_name.lstrip('_'), "") def _is_special_name(name): """Determine whether or not *name* is a "__special__" name. :arg 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(unbound_function, logger): """Create an unbound function that delegates to either a tracing proxy or the original *unbound_function*. :arg unbound_function: the unbound function for the instance method being traced :arg logging.Logger logger: the tracing logger :return: an unbound function that wraps *unbound_function* to provide the call and return tracing support If *logger* is not enabled for the :attr:`autologging.TRACE` level **at the time the returned delegator function is invoked**, then the method for the original *unbound_function* is called instead of the tracing proxy. The overhead that a ``@traced`` instance method incurs when tracing is **disabled** is: * the delegator function call itself * binding the original *unbound_function* to the instance * the ``TRACE`` level check The original *unbound_function* is available from the delegator function's ``__wrapped__`` attribute. """ # functions have a __get__ method; they can act as descriptors proxy = _FunctionTracingProxy(unbound_function, logger) @wraps(unbound_function) def autologging_traced_instancemethod_delegator(self_, *args, **keywords): method = unbound_function.__get__(self_, self_.__class__) if logger.isEnabledFor(TRACE): # don't access the proxy from closure (IronPython does not manage # co_freevars/__closure__ correctly for local vars) proxy = \ autologging_traced_instancemethod_delegator._tracing_proxy return proxy(method, args, keywords) else: return method(*args, **keywords) autologging_traced_instancemethod_delegator._tracing_proxy = proxy if not hasattr( autologging_traced_instancemethod_delegator, "__wrapped__"): # __wrapped__ is only set by functools.wraps() in Python 3.2+ autologging_traced_instancemethod_delegator.__wrapped__ = \ unbound_function autologging_traced_instancemethod_delegator.__autologging_traced__ = True return autologging_traced_instancemethod_delegator def _make_traceable_classmethod(method_descriptor, logger): """Create a method descriptor that delegates to either a tracing proxy or the original *method_descriptor*. :arg method_descriptor: the method descriptor for the class method being traced :arg logging.Logger logger: the tracing logger :return: a method descriptor that wraps the *method_descriptor* function to provide the call and return tracing support If *logger* is not enabled for the :attr:`autologging.TRACE` level **at the time the returned delegator method descriptor is invoked**, then the method for the original *method_descriptor* is called instead of the tracing proxy. The overhead that a ``@traced`` class method incurs when tracing is **disabled** is: * the delegator function call itself * binding the original *method_descriptor* to the class * the ``TRACE`` level check The original *method_descriptor* function is available from the delegator method descriptor's ``__func__.__wrapped__`` attribute. """ function = method_descriptor.__func__ proxy = _FunctionTracingProxy(function, logger) @wraps(function) def autologging_traced_classmethod_delegator(cls, *args, **keywords): method = method_descriptor.__get__(None, cls) if logger.isEnabledFor(TRACE): # don't access the proxy from closure (IronPython does not manage # co_freevars/__closure__ correctly for local vars) proxy = autologging_traced_classmethod_delegator._tracing_proxy return proxy(method, args, keywords) else: return method(*args, **keywords) autologging_traced_classmethod_delegator._tracing_proxy = proxy if not hasattr(autologging_traced_classmethod_delegator, "__wrapped__"): # __wrapped__ is only set by functools.wraps() in Python 3.2+ autologging_traced_classmethod_delegator.__wrapped__ = function autologging_traced_classmethod_delegator.__autologging_traced__ = True return classmethod(autologging_traced_classmethod_delegator) def _make_traceable_staticmethod(method_descriptor, logger): """Create a method descriptor that delegates to either a tracing proxy or the original *method_descriptor*. :arg method_descriptor: the method descriptor for the static method being traced :arg logging.Logger logger: the tracing logger :return: a method descriptor that wraps the *method_descriptor* function to provide the call and return tracing support If *logger* is not enabled for the :attr:`autologging.TRACE` level **at the time the returned delegator method descriptor is invoked**, then the method for the original *method_descriptor* is called instead of the tracing proxy. The overhead that a ``@traced`` static method incurs when tracing is **disabled** is: * the delegator function call itself * the ``TRACE`` level check The original *method_descriptor* function is available from the delegator method descriptor's ``__func__.__wrapped__`` attribute. """ autologging_traced_staticmethod_delegator = _make_traceable_function( method_descriptor.__func__, logger) return staticmethod(autologging_traced_staticmethod_delegator) class _FunctionTracingProxy(object): """Proxy a function invocation to capture and log the call arguments and return value. """ def __init__(self, function, logger): """ :arg function: the function being traced :arg logging.Logger logger: the tracing logger """ func_code = function.__code__ self._func_filename = func_code.co_filename self._func_lineno = func_code.co_firstlineno self._logger = logger @property def logger(self): """The tracing logger for the function.""" return self._logger def __call__(self, function, args, keywords): """Call *function*, tracing its arguments and return value. :arg tuple args: the positional arguments for *function* :arg dict keywords: the keyword arguments for *function* :return: the value returned by calling *function* with positional arguments *args* and keyword arguments *keywords* .. warning:: This method does **not** perform a level check, and delegates *directly* to :meth:`logging.Logger.handle`. The caller is expected to perform the level check prior to calling this method. .. note:: If the return value of *function* is a `generator iterator <https://docs.python.org/3/glossary.html#term-generator-iterator>`_, then this method returns *value* wrapped in a :class:`_GeneratorIteratorTracingProxy` object to provide the ``yield`` and ``StopIteration`` tracing support. """ self._logger.handle(logging.LogRecord( self._logger.name, # name TRACE, # level self._func_filename, # pathname self._func_lineno, # lineno "CALL *%r **%r", # msg (args, keywords), # args None, # exc_info func=function.__name__)) value = function(*args, **keywords) self._logger.handle(logging.LogRecord( self._logger.name, # name TRACE, # level self._func_filename, # pathname self._func_lineno, # lineno "RETURN %r", # msg (value,), # args None, # exc_info func=function.__name__)) return (_GeneratorIteratorTracingProxy(function, value, self._logger) if isgenerator(value) else value) class _GeneratorIteratorTracingProxy(object): """Proxy a generator iterator to capture and trace *YIELD*, *SEND*, *THROW*, *CLOSE* and *STOP* events. .. note:: Generator iterators cannot be "rewound." A generator iterator that has been exhausted will continue to raise ``StopIteration`` on all subsequent calls to ``next()``, and Autologging will dutifully trace each of those events. This behavior is by design; if a program is failing due to an unexpected ``StopIteration`` exception, then the (traced) program should be able to identify when/where the errant ``next()`` call was made. """ #: An easily-queriable marker. __autologging_traced__ = True def __init__(self, gfunc, giter, logger): """ :arg gfunc: the generator function that returned *giter* :arg types.GeneratorType iterator: the generator iterator returned by *gfunc* :arg logging.Logger logger: the tracing logger """ # this is a "fallback" line number for IronPython self._gfunc_lineno = gfunc.__code__.co_firstlineno self._giter = giter self._logger = logger #: The wrapped generator iterator. @property def __wrapped__(self): return self._giter #: The name of the wrapped generator iterator. @property def __name__(self): return self._giter.__name__ #: The current line number of the wrapped generator iterator. @property def _lineno(self): # NOTE: IronPython does not track gi_frame.f_lineno correctly (always # reported as 1). return (getattr(self._giter.gi_frame, "f_lineno", self._gfunc_lineno) if not _is_ironpython else self._gfunc_lineno) def __iter__(self): """Return a self-reference. This method (along with :meth:`__next__`) implements the iterator protocol for the proxy object. """ return self def _trace(self, message, *message_args): giter = self._giter self._logger.handle(logging.LogRecord( self._logger.name, # name TRACE, # level giter.gi_code.co_filename, # pathname self._lineno, # lineno message, # msg message_args, # args None, # exc_info func=giter.__name__)) def __next__(self): """Attempt to return the next value from the wrapped generator iterator. If a value is obtained, log the event at :obj:`TRACE` level in a "YIELD" record. If the wrapped generator iterator is exhausted, log the ``StopIteration`` event (exception) at :obj:`TRACE` level in a "STOP" record. This method (along with :meth:`__iter__`) implements the iterator protocol for the proxy object. """ giter = self._giter try: value = next(giter) except StopIteration: self._trace("STOP %r", giter) raise else: self._trace("YIELD %r %r", giter, value) return value # PYVER: 2.7 compatibility next = __next__ def send(self, value): """Send *value* to the wrapped generator iterator, logging the event at :obj:`TRACE` level in a "SEND" record. """ giter = self._giter self._trace("SEND %r %r", giter, value) return giter.send(value) def throw(self, exception): """Cause the wrapped generator iterator to raise *exception*, logging the event at :obj:`TRACE` level in a "THROW" record. :arg Exception exception: the exception object that the wrapped generator iterator should throw """ giter = self._giter self._trace("THROW %r %r", giter, exception) giter.throw(exception) def close(self): """Close the wrapped generator iterator, logging the event at :obj:`TRACE` level in a "CLOSE" record. """ giter = self._giter self._trace("CLOSE %r", giter) giter.close()