Release 1.2.0

This commit is contained in:
Wojciech Kozlowski 2017-03-09 00:10:28 +00:00
parent 142017c57f
commit c29553985c
9 changed files with 898 additions and 179 deletions

29
CHANGES.rst Normal file
View File

@ -0,0 +1,29 @@
Changelog
=========
1.2.0
-----
- Fixed bug that didn't preserve exception backtrace correctly.
- Function ``trace`` now automatically converts a message to a string.
- Added several new options that can now be set in
``pylg_settings.py``, see the README for details.
- Improved dummy implementation for the case when PyLg is disabled.
- User settings provided in ``pylg_settings.py`` are now checked for
errors. In the case of a failed import, PyLg will set all settings
to defaults. In case of an invalid individual value, only the
relevant setting will be reset to its default.
1.1.0
-----
- PyLg can now be installed with pip.
1.0.0
-----
- Initial PyLg version.

View File

@ -1,2 +1,3 @@
recursive-include pylg *.py recursive-include pylg *.py
include LICENSE.txt include LICENSE.txt
include CHANGES.rst

View File

@ -55,8 +55,9 @@ To automatically log function entry and exit use the
Despite the name, this works for both functions and methods. Despite the name, this works for both functions and methods.
``@TraceFunction`` can take up to two optional arguments: ``@TraceFunction`` can take up to two optional arguments:
- trace_args - if ``True``, input parameters will be logged.
- trace_rv - if ``True``, the return value will be logged. - ``trace_args`` - if ``True``, input parameters will be logged.
- ``trace_rv`` - if ``True``, the return value will be logged.
The default values for these arguments are set in a global settings The default values for these arguments are set in a global settings
file. file.
@ -73,7 +74,7 @@ These arguments have to specified explicitly by name. Some examples:
def some_fuction(): def some_fuction():
pass pass
@TraceFunction(trace_args = False, trace_args = False) @TraceFunction(trace_args = False, trace_rv = False)
def some_fuction(): def some_fuction():
pass pass
@ -88,38 +89,90 @@ User Settings
------------- -------------
The user can adjust several settings to suit their preferences. To do The user can adjust several settings to suit their preferences. To do
so, create a file named ``pylg_settings.py`` in the top-level so, create a file named ``pylg_settings.py`` somewhere in your path
directory and set any of the following variables to the desired values and set any of the following variables to the desired values in order
in order to override the defaults. The settings.py file in the project to override the defaults. The settings.py file in the project
directory contains all the default settings and can be used as a directory contains all the default settings and can be used as a
template. template.
- PYLG_ENABLE (default = True) - enable/disable logs. - ``PYLG_ENABLE`` (default = ``True``) - enable/disable PyLg.
- PYLG (default = 'pylg.log') - the log file name.
- CLASS_NAME_RESOLUTION (default = False) - PyLg can also log the - ``PYLG_FILE`` (default = ``'pylg.log'``) - the log file name.
class name along with the method name if one exists. However, for
this to work correctly the ``trace`` function cannot be called from - ``EXCEPTION_WARNING`` (default = ``True``) - if ``True``, PyLg will
functions that are not decorated by ``@TraceFunction`` which is why print a warning about every exception caught to stderr.
it is disabled by default.
- DEFAULT_TRACE_ARGS (default = True) - the default value for - ``EXCEPTION_EXIT`` (default = ``False``) - if ``True``, PyLg will
``trace_args`` argument which can be passed to the ``@TraceFunction` force the program to exit (and not just raise ``SystemExit``)
decorator. If ``trace_args`` is ``True`` all parameters passed to whenever an exception occurs. This will happen even if the exception
the function will be logged. This can be overriden on an individual would be handled at a later point.
function basis.
- DEFAULT_TRACE_RV (default = True) - the default value for trace_rv - ``TRACE_TIME`` (default = ``TRUE``) - enable/disable time logging.
argument which can be passed to the ``@TraceFunction`` decorator. If
``trace_rv`` is ``True`` the function's return value will be - ``TIME_FORMAT`` (default = ``"%Y-%m-%d %H:%M:%S.%f"``) - formatting
logged. This can be overriden on an individual function basis. for the time trace. For a full list of options, see
- EXCEPTION_WARNING (default = True) - PyLg catches all exceptions in https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior.
traced functions, logs them, and then re-raises them with the full
backtrace. This setting determines whether it should also produce a - ``TRACE_FILENAME`` (default = ``True``) - enable/disable file name
warning for the user using the Python warning mechanism. logging.
- FILENAME_COLUMN_WIDTH (default = 32) - the column width reserved for
the file name. Names that are too short will be padded with - ``FILENAME_COLUMN_WIDTH`` (default = ``20``) - the column width for
whitespace and names that are too long will be truncated. the file name. If a name is too long, it will be truncated.
- FUNCTION_COLUMN_WIDTH (default = 32) - the column width reserved for
the function name. Names that are too short will be padded with - ``TRACE_LINENO`` (default = ``True``) - enable/disable the logging
whitespace and names that are too long will be truncated. of the line number from which the trace call was made. For entry and
exit messages this logs the line in which the decorator is placed
(which should be directly above the function itself).
- ``LINENO_WIDTH`` (default = ``4``) - the minimum number of digits to
use to print the line number. If the number is too long, more digits
will be used.
- ``TRACE_FUNCTION`` (default = ``True``) - enable/disable the logging
of the function name from which the trace call was made. Entry/exit
logs refer to the function they enter into and exit from.
- ``FUNCTION_COLUMN_WIDTH`` (default = ``32``) - the column width for
the function name. If a name is too long, it will be truncated.
- ``CLASS_NAME_RESOLUTION`` (default = ``False``) - enable/disable
class name resolution. Function names will be printed with their
class names. IMPORTANT: If this setting is enabled, the trace
function should ONLY be called from within functions that have the
``@TraceFunction`` decorator OR outside of any function.
- ``TRACE_MESSAGE`` (default = ``True``) - enable/disable message
logging.
- ``MESSAGE_WIDTH`` (default = ``0``) - the column width for the
message. A width of zero means unlimited.
- ``MESSAGE_WRAP`` (default = ``True``) - if ``True``, PyLG will wrap
the message to fit within the column width. Otherwise, the message
will be truncated.
- ``MESSAGE_MARK_TRUNCATION`` (default = ``True``) - if ``True``,
truncated message lines should have the last character replaced with
``\``.
- ``TRACE_SELF`` (default = ``False``) - enable/disable logging of the
``self`` function argument.
- ``COLLAPSE_LISTS`` (default = ``False``) - if ``True`` lists will be
collapsed to ``[ len=x ]`` where ``x`` denotes the number of
elements in the list.
- ``COLLAPSE_DICTS`` (default = ``False``) - if ``True`` dictionaries
will be collapsed to ``{ len=x }`` where ``x`` denotes the number of
elements in the dictionary.
- ``DEFAULT_TRACE_ARGS`` (default = ``True``) - the default setting
for ``trace_args`` which determines whether TraceFunction should
trace function parameters on entry.
- ``DEFAULT_TRACE_RV`` (default = ``True``) - the default setting for
``trace_rv`` which determines whether TraceFunction should trace
function return values on exit.
Under development Under development
----------------- -----------------

View File

@ -1,4 +1,4 @@
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# PyLg: module to facilitate and automate the process of writing runtime logs. # PyLg: module to facilitate and automate the process of writing runtime logs.
# Copyright (C) 2017 Wojciech Kozlowski <wojciech.kozlowski@vivaldi.net> # Copyright (C) 2017 Wojciech Kozlowski <wojciech.kozlowski@vivaldi.net>
# #
@ -14,6 +14,11 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from .pylg import TraceFunction, trace from .loadSettings import PYLG_ENABLE
if PYLG_ENABLE:
from .pylg import TraceFunction, trace
else:
from .dummy import TraceFunction, trace

116
pylg/dummy.py Normal file
View File

@ -0,0 +1,116 @@
# -----------------------------------------------------------------------------
# PyLg: module to facilitate and automate the process of writing runtime logs.
# Copyright (C) 2017 Wojciech Kozlowski <wojciech.kozlowski@vivaldi.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# -----------------------------------------------------------------------------
from functools import partial
class TraceFunction(object):
""" Dummy implementation of TraceFunction.
"""
def __get__(self, obj, objtype):
""" Support for instance functions.
"""
return partial(self.__call__, obj)
def __init__(self, *args, **kwargs):
""" Constructor for dummy TraceFunction. Note that the behaviour is
different depending on whether TraceFunction is passed any
parameters. For details see the non-dummy implementation.
"""
# ---------------------------------------------------------------------
# Make sure this decorator is never called with no arguments.
# ---------------------------------------------------------------------
assert args or kwargs
if args:
# -----------------------------------------------------------------
# The function init_function will verify the input.
# -----------------------------------------------------------------
self.init_function(*args, **kwargs)
if kwargs:
trace_args_str = 'trace_args'
trace_rv_str = 'trace_rv'
# -----------------------------------------------------------------
# If kwargs is non-empty, it should only contain trace_rv,
# trace_args, or both and args should be empty. Assert all
# this.
# -----------------------------------------------------------------
assert not args
assert (len(kwargs) > 0) and (len(kwargs) <= 2)
if len(kwargs) == 1:
assert (trace_rv_str in kwargs) or (trace_args_str in kwargs)
elif len(kwargs) == 2:
assert (trace_rv_str in kwargs) and (trace_args_str in kwargs)
self.function = None
def __call__(self, *args, **kwargs):
""" The actual wrapper that is called when a call to a
decorated function is made. It also handles extra
initialisation when parameters are passed to
TraceFunction.
:return: The return value of the decorated function.
"""
if self.function is None:
# -----------------------------------------------------------------
# For an explanation of the logic here, see the non-dummy
# implementations in pylg.py.
# -----------------------------------------------------------------
self.init_function(*args, **kwargs)
return self
# ---------------------------------------------------------------------
# The actual decorating. The dummy implementation doesn't do anything.
# ---------------------------------------------------------------------
return self.function(*args, **kwargs)
def init_function(self, *args, **kwargs):
""" Function to initialise the TraceFunctionStruct kept by the
decorator.
"""
# ---------------------------------------------------------------------
# This function should only ever be called with one parameter
# - the function to be decorated. These checks are done here,
# rather than by the caller, as anything that calls this
# function should also have been called with the decorated
# function as its only parameter.
# ---------------------------------------------------------------------
assert not kwargs
assert len(args) == 1
assert callable(args[0])
self.function = args[0]
def trace(message, function=None):
pass

332
pylg/loadSettings.py Normal file
View File

@ -0,0 +1,332 @@
# -----------------------------------------------------------------------------
# PyLg: module to facilitate and automate the process of writing runtime logs.
# Copyright (C) 2017 Wojciech Kozlowski <wojciech.kozlowski@vivaldi.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# -----------------------------------------------------------------------------
import traceback
import warnings
import inspect
import sys
import os
# -----------------------------------------------------------------------------
# Import all the defaults first.
# -----------------------------------------------------------------------------
from .settings import *
# -----------------------------------------------------------------------------
# The filename of the user settings. It will be set once it can be
# determined after loading the module.
# -----------------------------------------------------------------------------
PYLG_USER_FILE = None
# -----------------------------------------------------------------------------
# Attempt to load the module pylg_settings module. If successful, set
# PYLG_USER_FILE to the module's path. By attempting an import rather
# than checking if a file exists, we can handle the case of the user
# having the settings file elsewhere in their path.
# -----------------------------------------------------------------------------
try:
# -------------------------------------------------------------------------
# We import the module itself to be able to determine its source
# file before we import all the settings.
# -------------------------------------------------------------------------
import pylg_settings
from pylg_settings import *
PYLG_USER_FILE = inspect.getsourcefile(pylg_settings)
except ImportError:
# -------------------------------------------------------------------------
# The user settings don't exist. We assume the user is happy with
# the defaults.
# -------------------------------------------------------------------------
pass
except (NameError, SyntaxError):
warnings.warn("There was a problem importing user settings")
sys.stderr.write("\n")
traceback.print_exc(file=sys.stderr)
sys.stderr.write("\n")
# -----------------------------------------------------------------------------
# Utility functions for sanity checking user settings. They raise an
# ImportError if something is wrong.
# -----------------------------------------------------------------------------
def pylg_check_bool(value, name):
if not isinstance(value, bool):
warning_msg = ("Invalid type for " + name + " in " +
PYLG_USER_FILE +
" - should be bool, is type " +
type(value).__name__)
warnings.warn(warning_msg)
raise ImportError
def pylg_check_string(value, name):
if not isinstance(value, basestring):
warning_msg = ("Invalid type for " + name + " in " +
PYLG_USER_FILE +
" - should be a string, is type " +
type(value).__name__)
warnings.warn(warning_msg)
raise ImportError
def pylg_check_int(value, name):
# -------------------------------------------------------------------------
# We check for bool as well as bools are an instance of int, but
# we don't want to let that go through.
# -------------------------------------------------------------------------
if not isinstance(value, int) or isinstance(value, bool):
warning_msg = ("Invalid type for " + name + " in " +
PYLG_USER_FILE +
" - should be int, is " +
type(value).__name__)
warnings.warn(warning_msg)
raise ImportError
def pylg_check_nonneg_int(value, name):
pylg_check_int(value, name)
if value < 0:
warning_msg = ("Invalid value for " + name + " in " +
PYLG_USER_FILE +
" - should be non-negative, is " +
str(value))
warnings.warn(warning_msg)
raise ImportError
def pylg_check_pos_int(value, name):
pylg_check_int(value, name)
if value <= 0:
warning_msg = ("Invalid value for " + name + " in " +
PYLG_USER_FILE +
" - should be positive, is " +
str(value))
warnings.warn(warning_msg)
raise ImportError
if PYLG_USER_FILE is not None:
# -------------------------------------------------------------------------
# If PYLG_USER_FILE is set, we have successfully imported user
# settings. Nowe, we need to sanity check them. If anything is
# wrong we reset the value to its default. At this stage a single
# error should not affect any other settings.
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
# PYLG_ENABLE - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(PYLG_ENABLE, "PYLG_ENABLE")
except ImportError:
from .settings import PYLG_ENABLE
# -------------------------------------------------------------------------
# PYLG_FILE - string
# -------------------------------------------------------------------------
try:
pylg_check_string(PYLG_FILE, "PYLG_FILE")
except ImportError:
from .settings import PYLG_FILE
# -------------------------------------------------------------------------
# EXCEPTION_WARNING - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(EXCEPTION_WARNING, "EXCEPTION_WARNING")
except ImportError:
from .settings import EXCEPTION_WARNING
# -------------------------------------------------------------------------
# EXCEPTION_EXIT - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(EXCEPTION_EXIT, "EXCEPTION_EXIT")
except ImportError:
from .settings import EXCEPTION_EXIT
# -------------------------------------------------------------------------
# TRACE_TIME - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(TRACE_TIME, "TRACE_TIME")
except ImportError:
from .settings import TRACE_TIME
# -------------------------------------------------------------------------
# TIME_FORMAT - string
# -------------------------------------------------------------------------
try:
pylg_check_string(TIME_FORMAT, "TIME_FORMAT")
except ImportError:
from .settings import TIME_FORMAT
# -------------------------------------------------------------------------
# TRACE_FILENAME - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(TRACE_FILENAME, "TRACE_FILENAME")
except ImportError:
from .settings import TRACE_FILENAME
# -------------------------------------------------------------------------
# FILENAME_COLUMN_WIDTH - non-negative integer
# -------------------------------------------------------------------------
try:
pylg_check_pos_int(FILENAME_COLUMN_WIDTH, "FILENAME_COLUMN_WIDTH")
except ImportError:
from .settings import FILENAME_COLUMN_WIDTH
# -------------------------------------------------------------------------
# TRACE_LINENO - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(TRACE_LINENO, "TRACE_LINENO")
except ImportError:
from .settings import TRACE_LINENO
# -------------------------------------------------------------------------
# LINENO_WIDTH - non-negative integer
# -------------------------------------------------------------------------
try:
pylg_check_nonneg_int(LINENO_WIDTH, "LINENO_WIDTH")
except ImportError:
from .settings import LINENO_WIDTH
# -------------------------------------------------------------------------
# TRACE_FUNCTION - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(TRACE_FUNCTION, "TRACE_FUNCTION")
except ImportError:
from .settings import TRACE_FUNCTION
# -------------------------------------------------------------------------
# FUNCTION_COLUMN_WIDTH - non-negative integer
# -------------------------------------------------------------------------
try:
pylg_check_pos_int(FUNCTION_COLUMN_WIDTH, "FUNCTION_COLUMN_WIDTH")
except ImportError:
from .settings import FUNCTION_COLUMN_WIDTH
# -------------------------------------------------------------------------
# CLASS_NAME_RESOLUTION - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(CLASS_NAME_RESOLUTION, "CLASS_NAME_RESOLUTION")
except ImportError:
from .settings import CLASS_NAME_RESOLUTION
# -------------------------------------------------------------------------
# TRACE_MESSAGE - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(TRACE_MESSAGE, "TRACE_MESSAGE")
except ImportError:
from .settings import TRACE_MESSAGE
# -------------------------------------------------------------------------
# MESSAGE_WIDTH - non-negative integer - note 0 denotes unlimited
# -------------------------------------------------------------------------
try:
pylg_check_nonneg_int(MESSAGE_WIDTH, "MESSAGE_WIDTH")
except ImportError:
from .settings import MESSAGE_WIDTH
# -------------------------------------------------------------------------
# MESSAGE_WRAP - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(MESSAGE_WRAP, "MESSAGE_WRAP")
except ImportError:
from .settings import MESSAGE_WRAP
# -------------------------------------------------------------------------
# MESSAGE_MARK_TRUNCATION - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(MESSAGE_MARK_TRUNCATION, "MESSAGE_MARK_TRUNCATION")
except ImportError:
from .settings import MESSAGE_MARK_TRUNCATION
# -------------------------------------------------------------------------
# TRACE_SELF - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(TRACE_SELF, "TRACE_SELF")
except ImportError:
from .settings import TRACE_SELF
# -------------------------------------------------------------------------
# COLLAPSE_LISTS - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(COLLAPSE_LISTS, "COLLAPSE_LISTS")
except ImportError:
from .settings import COLLAPSE_LISTS
# -------------------------------------------------------------------------
# COLLAPSE_DICTS - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(COLLAPSE_DICTS, "COLLAPSE_DICTS")
except ImportError:
from .settings import COLLAPSE_DICTS
# -------------------------------------------------------------------------
# DEFAULT_TRACE_ARGS - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(DEFAULT_TRACE_ARGS, "DEFAULT_TRACE_ARGS")
except ImportError:
from .settings import DEFAULT_TRACE_ARGS
# -------------------------------------------------------------------------
# DEFAULT_TRACE_RV - bool
# -------------------------------------------------------------------------
try:
pylg_check_bool(DEFAULT_TRACE_RV, "DEFAULT_TRACE_RV")
except ImportError:
from .settings import DEFAULT_TRACE_RV

View File

@ -1,4 +1,4 @@
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# PyLg: module to facilitate and automate the process of writing runtime logs. # PyLg: module to facilitate and automate the process of writing runtime logs.
# Copyright (C) 2017 Wojciech Kozlowski <wojciech.kozlowski@vivaldi.net> # Copyright (C) 2017 Wojciech Kozlowski <wojciech.kozlowski@vivaldi.net>
# #
@ -14,31 +14,22 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from datetime import datetime from datetime import datetime
from functools import partial from functools import partial
import traceback
import warnings import warnings
import textwrap
import inspect import inspect
import sys import sys
import os import os
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Load default settings. # Load settings.
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from .settings import * from .loadSettings import *
try:
#---------------------------------------------------------------------------
# Load user settings.
#---------------------------------------------------------------------------
from pylg_settings import *
except ImportError:
#---------------------------------------------------------------------------
# User settings don't exist.
#---------------------------------------------------------------------------
pass
class ClassNameStack(object): class ClassNameStack(object):
@ -67,13 +58,14 @@ class ClassNameStack(object):
else: else:
return None return None
class PyLg(object): class PyLg(object):
""" Class to handle the log file. """ Class to handle the log file.
""" """
wfile = None wfile = None
filename = PYLG filename = PYLG_FILE
@staticmethod @staticmethod
def set_filename(new_filename): def set_filename(new_filename):
@ -104,6 +96,7 @@ class PyLg(object):
str(datetime.now()) + " ===\n\n") str(datetime.now()) + " ===\n\n")
PyLg.wfile.write(string) PyLg.wfile.write(string)
PyLg.wfile.flush()
@staticmethod @staticmethod
def close(): def close():
@ -117,6 +110,7 @@ class PyLg(object):
else: else:
warnings.warn("PyLg wfile is not open - nothing to close") warnings.warn("PyLg wfile is not open - nothing to close")
class TraceFunction(object): class TraceFunction(object):
""" Class that serves as a decorator to trace entry and exit from """ Class that serves as a decorator to trace entry and exit from
@ -129,13 +123,13 @@ class TraceFunction(object):
""" Internal object to handle traced function properties. """ Internal object to handle traced function properties.
""" """
function = None function = None
varnames = None varnames = None
defaults = None defaults = None
filename = None filename = None
lineno = None lineno = None
classname = None classname = None
functionname = None functionname = None
def __get__(self, obj, objtype): def __get__(self, obj, objtype):
@ -152,31 +146,31 @@ class TraceFunction(object):
parameters. For details see __call__ in this class. parameters. For details see __call__ in this class.
""" """
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
# Make sure this decorator is never called with no arguments. # Make sure this decorator is never called with no arguments.
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
assert args or kwargs assert args or kwargs
if args: if args:
self.trace_args = DEFAULT_TRACE_ARGS self.trace_args = DEFAULT_TRACE_ARGS
self.trace_rv = DEFAULT_TRACE_RV self.trace_rv = DEFAULT_TRACE_RV
#------------------------------------------------------------------- # -----------------------------------------------------------------
# The function init_function will verify the input. # The function init_function will verify the input.
#------------------------------------------------------------------- # -----------------------------------------------------------------
self.init_function(*args, **kwargs) self.init_function(*args, **kwargs)
if kwargs: if kwargs:
trace_args_str = 'trace_args' trace_args_str = 'trace_args'
trace_rv_str = 'trace_rv' trace_rv_str = 'trace_rv'
#------------------------------------------------------------------- # -----------------------------------------------------------------
# If kwargs is non-empty, it should only contain trace_rv, # If kwargs is non-empty, it should only contain trace_rv,
# trace_args, or both and args should be empty. Assert all # trace_args, or both and args should be empty. Assert all
# this. # this.
#------------------------------------------------------------------- # -----------------------------------------------------------------
assert not args assert not args
assert (len(kwargs) > 0) and (len(kwargs) <= 2) assert (len(kwargs) > 0) and (len(kwargs) <= 2)
if len(kwargs) == 1: if len(kwargs) == 1:
@ -206,7 +200,7 @@ class TraceFunction(object):
:return: The return value of the decorated function. :return: The return value of the decorated function.
""" """
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
# __call__ has to behave differently depending on whether the # __call__ has to behave differently depending on whether the
# decorator has been given any parameters. The reason for this # decorator has been given any parameters. The reason for this
# is as follows: # is as follows:
@ -226,10 +220,10 @@ class TraceFunction(object):
# the first case, the callable object is an instance of # the first case, the callable object is an instance of
# TraceFunction, in the latter case the return value of # TraceFunction, in the latter case the return value of
# TraceFunction.__call__ is the callable object. # TraceFunction.__call__ is the callable object.
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
if self.function is None: if self.function is None:
#------------------------------------------------------------------- # -----------------------------------------------------------------
# If the decorator has been passed a parameter, __init__ # If the decorator has been passed a parameter, __init__
# will not define self.function and __call__ will be # will not define self.function and __call__ will be
# called immediately after __init__ with the decorated # called immediately after __init__ with the decorated
@ -240,34 +234,29 @@ class TraceFunction(object):
# object as the callable handle for the decorated # object as the callable handle for the decorated
# function. This if block should be hit only once at most # function. This if block should be hit only once at most
# and only during initialisation. # and only during initialisation.
#------------------------------------------------------------------- # -----------------------------------------------------------------
self.init_function(*args, **kwargs) self.init_function(*args, **kwargs)
return self return self
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
# The actual decorating. # The actual decorating.
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
if PYLG_ENABLE: ClassNameStack.insert(self.function.classname)
self.trace_entry(*args, **kwargs)
ClassNameStack.insert(self.function.classname) try:
self.trace_entry(*args, **kwargs)
try:
rv = self.function.function(*args, **kwargs)
except Exception as e:
self.trace_exception(e)
exc_info = sys.exc_info()
raise (exc_info[0], exc_info[1], exc_info[2])
self.trace_exit(rv)
ClassNameStack.pop()
else:
#-------------------------------------------------------------------
# If PYLG is disabled, don't wrap anything.
#-------------------------------------------------------------------
rv = self.function.function(*args, **kwargs) rv = self.function.function(*args, **kwargs)
except Exception as e:
self.trace_exception(e)
if EXCEPTION_EXIT:
traceback.print_exc(file=sys.stderr)
os._exit(1)
raise
self.trace_exit(rv)
ClassNameStack.pop()
return rv return rv
@ -277,13 +266,13 @@ class TraceFunction(object):
decorator. decorator.
""" """
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
# This function should only ever be called with one parameter # This function should only ever be called with one parameter
# - the function to be decorated. These checks are done here, # - the function to be decorated. These checks are done here,
# rather than by the caller, as anything that calls this # rather than by the caller, as anything that calls this
# function should also have been called with the decorated # function should also have been called with the decorated
# function as its only parameter. # function as its only parameter.
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
assert not kwargs assert not kwargs
assert len(args) == 1 assert len(args) == 1
assert callable(args[0]) assert callable(args[0])
@ -300,16 +289,16 @@ class TraceFunction(object):
zip(argspec.args[-len(argspec.defaults):], zip(argspec.args[-len(argspec.defaults):],
argspec.defaults)) argspec.defaults))
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
# init_function is called from either __init__ or __call__ and # init_function is called from either __init__ or __call__ and
# we want the frame before that. # we want the frame before that.
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
frames_back = 2 frames_back = 2
caller_frame = inspect.stack()[frames_back] caller_frame = inspect.stack()[frames_back]
self.function.filename = os.path.basename(caller_frame[1]) self.function.filename = os.path.basename(caller_frame[1])
self.function.lineno = caller_frame[2] self.function.lineno = caller_frame[2]
self.function.classname = caller_frame[3] self.function.classname = caller_frame[3]
self.function.functionname = self.function.function.__name__ self.function.functionname = self.function.function.__name__
def trace_entry(self, *args, **kwargs): def trace_entry(self, *args, **kwargs):
@ -319,9 +308,9 @@ class TraceFunction(object):
trace. trace.
""" """
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
# The ENTRY message. # The ENTRY message.
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
msg = "-> ENTRY" msg = "-> ENTRY"
if args or kwargs: if args or kwargs:
msg += ": " msg += ": "
@ -329,25 +318,30 @@ class TraceFunction(object):
n_args = len(args) n_args = len(args)
if self.trace_args: if self.trace_args:
for arg in range(n_args): for arg in range(n_args):
if not TRACE_SELF and \
self.function.varnames[arg] == "self":
continue
msg += (self.function.varnames[arg] + " = " + msg += (self.function.varnames[arg] + " = " +
str(args[arg]) + ", ") self.get_value_string(args[arg]) + ", ")
for name in self.function.varnames[n_args:]: for name in self.function.varnames[n_args:]:
msg += name + " = " msg += name + " = "
if name in kwargs: if name in kwargs:
msg += str(kwargs[name]) value = kwargs[name]
else: else:
msg += str(self.function.defaults[name]) value = self.function.defaults[name]
msg += ", " msg += self.get_value_string(value) + ", "
msg = msg[:-2] msg = msg[:-2]
else: else:
msg += "---" msg += "---"
trace(msg, function = self.function) trace(msg, function=self.function)
def trace_exit(self, rv = None): def trace_exit(self, rv=None):
""" Called on function exit to log the fact that a function has """ Called on function exit to log the fact that a function has
finished executing. finished executing.
@ -355,18 +349,18 @@ class TraceFunction(object):
:param rv: The return value of the traced function. :param rv: The return value of the traced function.
""" """
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
# The EXIT message. # The EXIT message.
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
msg = "<- EXIT " msg = "<- EXIT "
if rv is not None: if rv is not None:
msg += ": " msg += ": "
if self.trace_rv: if self.trace_rv:
msg += str(rv) msg += self.get_value_string(rv)
else: else:
msg += "---" msg += "---"
trace(msg, function = self.function) trace(msg, function=self.function)
return return
def trace_exception(self, exception): def trace_exception(self, exception):
@ -376,9 +370,9 @@ class TraceFunction(object):
:param exception: The raised exception. :param exception: The raised exception.
""" """
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
# The EXIT message. # The EXIT message.
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
core_msg = type(exception).__name__ + " RAISED" core_msg = type(exception).__name__ + " RAISED"
msg = "<- EXIT : " + core_msg msg = "<- EXIT : " + core_msg
@ -388,10 +382,29 @@ class TraceFunction(object):
if EXCEPTION_WARNING: if EXCEPTION_WARNING:
warnings.warn(core_msg, RuntimeWarning) warnings.warn(core_msg, RuntimeWarning)
trace(msg, function = self.function) trace(msg, function=self.function)
return return
def trace(message, function = None): def get_value_string(self, value):
""" Convert value to a string for the log.
"""
if isinstance(value, list) and COLLAPSE_LISTS:
return self.collapse_list(value)
elif isinstance(value, dict) and COLLAPSE_DICTS:
return self.collapse_dict(value)
else:
return str(value)
def collapse_list(self, ll):
return "[ len=" + str(len(ll)) + " ]"
def collapse_dict(self, dd):
return "{ len=" + str(len(dd)) + " }"
def trace(message, function=None):
""" Writes message to the log file. It will also log the time, """ Writes message to the log file. It will also log the time,
filename, line number and function name. filename, line number and function name.
@ -401,48 +414,132 @@ def trace(message, function = None):
TraceFunction. TraceFunction.
""" """
if not PYLG_ENABLE:
#-----------------------------------------------------------------------
# Don't do anything if PYLG is disabled
#-----------------------------------------------------------------------
return
if function is None: if function is None:
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
# If there is no function object, we need to work out # If there is no function object, we need to work out
# where the trace call was made from. # where the trace call was made from.
#----------------------------------------------------------------------- # ---------------------------------------------------------------------
frames_back = 1 frames_back = 1
caller_frame = inspect.stack()[frames_back] caller_frame = inspect.stack()[frames_back]
filename = os.path.basename(caller_frame[1]) filename = os.path.basename(caller_frame[1])
lineno = caller_frame[2] lineno = caller_frame[2]
functionname = caller_frame[3] functionname = caller_frame[3]
else: else:
filename = function.filename filename = function.filename
lineno = function.lineno lineno = function.lineno
functionname = function.functionname functionname = function.functionname
#--------------------------------------------------------------------------- # -------------------------------------------------------------------------
# If CLASS_NAME_RESOLUTION is enabled, the top element of the # If CLASS_NAME_RESOLUTION is enabled, the top element of the
# stack should be the class name of the function from which this # stack should be the class name of the function from which this
# trace call is made. This cannot be policed so the user must make # trace call is made. This cannot be policed so the user must make
# sure this is the case by ensuring that trace is only called # sure this is the case by ensuring that trace is only called
# outside of any function or from within functions that have the # outside of any function or from within functions that have the
# @TraceFunction decorator. # @TraceFunction decorator.
#--------------------------------------------------------------------------- # -------------------------------------------------------------------------
classname = ClassNameStack.get() classname = ClassNameStack.get()
if classname is not None and classname != "<module>": if classname is not None and classname != "<module>":
functionname = classname + "." + functionname functionname = classname + "." + functionname
#--------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Generate the string based on the settings.
# -------------------------------------------------------------------------
msg = ""
if TRACE_TIME:
msg += datetime.now().strftime(TIME_FORMAT) + " "
if TRACE_FILENAME:
msg += '{filename:{w}.{w}} '.format(filename=filename,
w=FILENAME_COLUMN_WIDTH)
if TRACE_LINENO:
msg += '{lineno:0{w}}: '.format(lineno=lineno, w=LINENO_WIDTH)
if TRACE_FUNCTION:
msg += '{function:{w}.{w}} '.format(function=functionname,
w=FUNCTION_COLUMN_WIDTH)
if TRACE_MESSAGE:
message = str(message)
if MESSAGE_WIDTH > 0:
# -----------------------------------------------------------------
# Get the length of the trace line so far
# -----------------------------------------------------------------
premsglen = len(msg)
# -----------------------------------------------------------------
# Wrap the text.
# -----------------------------------------------------------------
wrapped = textwrap.wrap(message, MESSAGE_WIDTH)
if wrapped:
if MESSAGE_WRAP:
# ---------------------------------------------------------
# Print the first line. It gets special treatment
# as it doesn't need whitespace in front of it.
# ---------------------------------------------------------
msg += wrapped[0]
# ---------------------------------------------------------
# Print the remaining lines. Append whitespace to
# align it with the first line.
# ---------------------------------------------------------
for line in wrapped[1:]:
msg += '\n' + '{:{w}}'.format('', w=premsglen) + line
else:
# ---------------------------------------------------------
# The message is not being wrapped.
# ---------------------------------------------------------
if MESSAGE_MARK_TRUNCATION and wrapped[1:]:
# -----------------------------------------------------
# We want to mark truncated lines so we need
# to determine if the line is being
# truncated. If it is we replace the last
# character with '\'.
# -----------------------------------------------------
if MESSAGE_WIDTH > 1:
wrapped = textwrap.wrap(wrapped[0],
MESSAGE_WIDTH - 1)
assert wrapped
msg += ('{m:{w}}'.format(m=wrapped[0],
w=MESSAGE_WIDTH - 1) +
'\\')
else:
assert MESSAGE_WIDTH == 1
msg += '\\'
else:
# -----------------------------------------------------
# Either the message is not being truncated or
# MESSAGE_MARK_TRUNCATION is False.
# -----------------------------------------------------
msg += wrapped[0]
else:
# -----------------------------------------------------------------
# A MESSAGE_WIDTH of 0 denotes no limit.
# -----------------------------------------------------------------
assert MESSAGE_WIDTH == 0
msg += message
# -------------------------------------------------------------------------
# Terminate the log line with a newline.
# -------------------------------------------------------------------------
msg += "\n"
# -------------------------------------------------------------------------
# Write the data to the log file. # Write the data to the log file.
#--------------------------------------------------------------------------- # -------------------------------------------------------------------------
PyLg.write(str(datetime.now()) + " " + PyLg.write(msg)
'{filename:{w}.{w}} '.format(filename = filename,
w = FILENAME_COLUMN_WIDTH) +
'{0:04d}: '.format(lineno) +
'{function:{w}.{w}} '.format(function = functionname,
w = FUNCTION_COLUMN_WIDTH) +
message + "\n")

View File

@ -1,4 +1,4 @@
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# PyLg: module to facilitate and automate the process of writing runtime logs. # PyLg: module to facilitate and automate the process of writing runtime logs.
# Copyright (C) 2017 Wojciech Kozlowski <wojciech.kozlowski@vivaldi.net> # Copyright (C) 2017 Wojciech Kozlowski <wojciech.kozlowski@vivaldi.net>
# #
@ -14,43 +14,129 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Enable/disable PyLg. # Enable/disable PyLg.
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
PYLG_ENABLE = True PYLG_ENABLE = True
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Log file. # The log file name.
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
PYLG = 'pylg.log' PYLG_FILE = 'pylg.log'
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Enable class name resolution. Function names will be printed with # If True, PyLg will print a warning about every exception caught to
# their class names. # stderr.
# -----------------------------------------------------------------------------
EXCEPTION_WARNING = True
# -----------------------------------------------------------------------------
# If True, PyLg will force the program to exit (and not just raise
# SystemExit) whenever an exception occurs. This will happen even if
# the exception would be handled at a later point.
# -----------------------------------------------------------------------------
EXCEPTION_EXIT = False
# -----------------------------------------------------------------------------
# Enable/disable time logging.
# -----------------------------------------------------------------------------
TRACE_TIME = True
# -----------------------------------------------------------------------------
# Formatting for the time trace. For a full list of options, see
# https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior.
# -----------------------------------------------------------------------------
TIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
# -----------------------------------------------------------------------------
# Enable/disable file name logging.
# -----------------------------------------------------------------------------
TRACE_FILENAME = True
# -----------------------------------------------------------------------------
# The column width for the file name. If a name is too long, it will
# be truncated.
# -----------------------------------------------------------------------------
FILENAME_COLUMN_WIDTH = 20
# -----------------------------------------------------------------------------
# Enable/disable the logging of the line number from which the trace
# call was made. For entry and exit messages this logs the line in
# which the decorator is placed (which should be directly above the
# function itself).
# -----------------------------------------------------------------------------
TRACE_LINENO = True
# -----------------------------------------------------------------------------
# The minimum number of digits to use to print the line number. If the
# number is too long, more digits will be used.
# -----------------------------------------------------------------------------
LINENO_WIDTH = 4
# -----------------------------------------------------------------------------
# Enable/disable the logging of the function name from which the trace
# call was made. Entry/exit logs refer to the function they enter into
# and exit from.
# -----------------------------------------------------------------------------
TRACE_FUNCTION = True
# -----------------------------------------------------------------------------
# The column width for the function name. If a name is too long, it
# will be truncated.
# -----------------------------------------------------------------------------
FUNCTION_COLUMN_WIDTH = 32
# -----------------------------------------------------------------------------
# Enable/disable class name resolution. Function names will be printed
# with their class names.
# #
# IMPORTANT: If this setting is enabled, the trace function should # IMPORTANT: If this setting is enabled, the trace function should
# ONLY be called from within functions that have the @TraceFunction # ONLY be called from within functions that have the @TraceFunction
# decorator OR outside of any function. # decorator OR outside of any function.
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
CLASS_NAME_RESOLUTION = False CLASS_NAME_RESOLUTION = False
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# The default for whether TraceFunction should trace function # Enable/disable message logging.
# parameters and return values. # -----------------------------------------------------------------------------
#------------------------------------------------------------------------------- TRACE_MESSAGE = True
# -----------------------------------------------------------------------------
# The column width for the message. A width of zero means unlimited.
# -----------------------------------------------------------------------------
MESSAGE_WIDTH = 0
# -----------------------------------------------------------------------------
# If True, PyLG will wrap the message to fit within the column
# width. Otherwise, the message will be truncated.
# -----------------------------------------------------------------------------
MESSAGE_WRAP = True
# -----------------------------------------------------------------------------
# If true, truncated message lines should have the last character
# replaced with '\'.
# -----------------------------------------------------------------------------
MESSAGE_MARK_TRUNCATION = True
# -----------------------------------------------------------------------------
# Enable/disable logging of the 'self' function argument.
# -----------------------------------------------------------------------------
TRACE_SELF = False
# -----------------------------------------------------------------------------
# If True lists/dictionaries will be collapsed to '[ len=x ]' and '{
# len=x }' respectively, where x denotes the number of elements in the
# collection.
# -----------------------------------------------------------------------------
COLLAPSE_LISTS = False
COLLAPSE_DICTS = False
# -----------------------------------------------------------------------------
# The default settings for 'trace_args' and 'trace_rv' which determine
# whether TraceFunction should trace function parameters on entry and
# return values on exit.
# -----------------------------------------------------------------------------
DEFAULT_TRACE_ARGS = True DEFAULT_TRACE_ARGS = True
DEFAULT_TRACE_RV = True DEFAULT_TRACE_RV = True
#-------------------------------------------------------------------------------
# Whether to warn the user when an Exception has been raised in a
# traced funcion.
#-------------------------------------------------------------------------------
EXCEPTION_WARNING = True
#-------------------------------------------------------------------------------
# The column width for file and function names.
#-------------------------------------------------------------------------------
FILENAME_COLUMN_WIDTH = 32
FUNCTION_COLUMN_WIDTH = 32

View File

@ -8,15 +8,15 @@ with open(path.join(pwd, 'README.rst'), encoding='utf-8') as f:
long_description = f.read() long_description = f.read()
setup( setup(
name = 'PyLg', name='PyLg',
version = '1.1.0', version='1.2.0',
description = 'Python module to facilitate and automate the process of writing runtime logs.', description='Python module to facilitate and automate the process of writing runtime logs.',
long_description = long_description, long_description=long_description,
url = 'https://gitlab.wojciechkozlowski.eu/wojtek/PyLg', url='https://gitlab.wojciechkozlowski.eu/wojtek/PyLg',
author = 'Wojciech Kozlowski', author='Wojciech Kozlowski',
author_email = 'wojciech.kozlowski@vivaldi.net', author_email='wojciech.kozlowski@vivaldi.net',
classifiers = [ classifiers=[
'Development Status :: 3 - Alpha', 'Development Status :: 3 - Alpha',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Topic :: Software Development :: Debuggers', 'Topic :: Software Development :: Debuggers',
@ -26,7 +26,7 @@ setup(
], ],
keywords='development log debug trace', keywords='development log debug trace',
include_package_data = True, include_package_data=True,
packages=["pylg"] packages=["pylg"]