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
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.
``@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
file.
@ -73,7 +74,7 @@ These arguments have to specified explicitly by name. Some examples:
def some_fuction():
pass
@TraceFunction(trace_args = False, trace_args = False)
@TraceFunction(trace_args = False, trace_rv = False)
def some_fuction():
pass
@ -88,38 +89,90 @@ User Settings
-------------
The user can adjust several settings to suit their preferences. To do
so, create a file named ``pylg_settings.py`` in the top-level
directory and set any of the following variables to the desired values
in order to override the defaults. The settings.py file in the project
so, create a file named ``pylg_settings.py`` somewhere in your path
and set any of the following variables to the desired values in order
to override the defaults. The settings.py file in the project
directory contains all the default settings and can be used as a
template.
- PYLG_ENABLE (default = True) - enable/disable logs.
- PYLG (default = 'pylg.log') - the log file name.
- CLASS_NAME_RESOLUTION (default = False) - PyLg can also log the
class name along with the method name if one exists. However, for
this to work correctly the ``trace`` function cannot be called from
functions that are not decorated by ``@TraceFunction`` which is why
it is disabled by default.
- DEFAULT_TRACE_ARGS (default = True) - the default value for
``trace_args`` argument which can be passed to the ``@TraceFunction`
decorator. If ``trace_args`` is ``True`` all parameters passed to
the function will be logged. This can be overriden on an individual
function basis.
- DEFAULT_TRACE_RV (default = True) - the default value for trace_rv
argument which can be passed to the ``@TraceFunction`` decorator. If
``trace_rv`` is ``True`` the function's return value will be
logged. This can be overriden on an individual function basis.
- EXCEPTION_WARNING (default = True) - PyLg catches all exceptions in
traced functions, logs them, and then re-raises them with the full
backtrace. This setting determines whether it should also produce a
warning for the user using the Python warning mechanism.
- FILENAME_COLUMN_WIDTH (default = 32) - the column width reserved for
the file name. Names that are too short will be padded with
whitespace and names that are too long 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
whitespace and names that are too long will be truncated.
- ``PYLG_ENABLE`` (default = ``True``) - enable/disable PyLg.
- ``PYLG_FILE`` (default = ``'pylg.log'``) - the log file name.
- ``EXCEPTION_WARNING`` (default = ``True``) - if ``True``, PyLg will
print a warning about every exception caught to stderr.
- ``EXCEPTION_EXIT`` (default = ``False``) - 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.
- ``TRACE_TIME`` (default = ``TRUE``) - enable/disable time logging.
- ``TIME_FORMAT`` (default = ``"%Y-%m-%d %H:%M:%S.%f"``) - formatting
for the time trace. For a full list of options, see
https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior.
- ``TRACE_FILENAME`` (default = ``True``) - enable/disable file name
logging.
- ``FILENAME_COLUMN_WIDTH`` (default = ``20``) - the column width for
the file name. If a name is too long, it will be truncated.
- ``TRACE_LINENO`` (default = ``True``) - 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).
- ``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
-----------------

View File

@ -1,4 +1,4 @@
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# PyLg: module to facilitate and automate the process of writing runtime logs.
# 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
# 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.
# 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
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
from datetime import datetime
from functools import partial
import traceback
import warnings
import textwrap
import inspect
import sys
import os
#-------------------------------------------------------------------------------
# Load default settings.
#-------------------------------------------------------------------------------
from .settings import *
# -----------------------------------------------------------------------------
# Load settings.
# -----------------------------------------------------------------------------
from .loadSettings import *
try:
#---------------------------------------------------------------------------
# Load user settings.
#---------------------------------------------------------------------------
from pylg_settings import *
except ImportError:
#---------------------------------------------------------------------------
# User settings don't exist.
#---------------------------------------------------------------------------
pass
class ClassNameStack(object):
@ -67,13 +58,14 @@ class ClassNameStack(object):
else:
return None
class PyLg(object):
""" Class to handle the log file.
"""
wfile = None
filename = PYLG
filename = PYLG_FILE
@staticmethod
def set_filename(new_filename):
@ -104,6 +96,7 @@ class PyLg(object):
str(datetime.now()) + " ===\n\n")
PyLg.wfile.write(string)
PyLg.wfile.flush()
@staticmethod
def close():
@ -117,6 +110,7 @@ class PyLg(object):
else:
warnings.warn("PyLg wfile is not open - nothing to close")
class TraceFunction(object):
""" Class that serves as a decorator to trace entry and exit from
@ -152,9 +146,9 @@ class TraceFunction(object):
parameters. For details see __call__ in this class.
"""
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
# Make sure this decorator is never called with no arguments.
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
assert args or kwargs
if args:
@ -162,9 +156,9 @@ class TraceFunction(object):
self.trace_args = DEFAULT_TRACE_ARGS
self.trace_rv = DEFAULT_TRACE_RV
#-------------------------------------------------------------------
# -----------------------------------------------------------------
# The function init_function will verify the input.
#-------------------------------------------------------------------
# -----------------------------------------------------------------
self.init_function(*args, **kwargs)
if kwargs:
@ -172,11 +166,11 @@ class TraceFunction(object):
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:
@ -206,7 +200,7 @@ class TraceFunction(object):
:return: The return value of the decorated function.
"""
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
# __call__ has to behave differently depending on whether the
# decorator has been given any parameters. The reason for this
# is as follows:
@ -226,10 +220,10 @@ class TraceFunction(object):
# the first case, the callable object is an instance of
# TraceFunction, in the latter case the return value of
# TraceFunction.__call__ is the callable object.
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
if self.function is None:
#-------------------------------------------------------------------
# -----------------------------------------------------------------
# If the decorator has been passed a parameter, __init__
# will not define self.function and __call__ will be
# called immediately after __init__ with the decorated
@ -240,15 +234,13 @@ class TraceFunction(object):
# object as the callable handle for the decorated
# function. This if block should be hit only once at most
# and only during initialisation.
#-------------------------------------------------------------------
# -----------------------------------------------------------------
self.init_function(*args, **kwargs)
return self
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
# The actual decorating.
#-----------------------------------------------------------------------
if PYLG_ENABLE:
# ---------------------------------------------------------------------
ClassNameStack.insert(self.function.classname)
self.trace_entry(*args, **kwargs)
@ -257,18 +249,15 @@ class TraceFunction(object):
except Exception as e:
self.trace_exception(e)
exc_info = sys.exc_info()
raise (exc_info[0], exc_info[1], exc_info[2])
if EXCEPTION_EXIT:
traceback.print_exc(file=sys.stderr)
os._exit(1)
raise
self.trace_exit(rv)
ClassNameStack.pop()
else:
#-------------------------------------------------------------------
# If PYLG is disabled, don't wrap anything.
#-------------------------------------------------------------------
rv = self.function.function(*args, **kwargs)
return rv
def init_function(self, *args, **kwargs):
@ -277,13 +266,13 @@ class TraceFunction(object):
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])
@ -300,10 +289,10 @@ class TraceFunction(object):
zip(argspec.args[-len(argspec.defaults):],
argspec.defaults))
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
# init_function is called from either __init__ or __call__ and
# we want the frame before that.
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
frames_back = 2
caller_frame = inspect.stack()[frames_back]
@ -319,9 +308,9 @@ class TraceFunction(object):
trace.
"""
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
# The ENTRY message.
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
msg = "-> ENTRY"
if args or kwargs:
msg += ": "
@ -329,25 +318,30 @@ class TraceFunction(object):
n_args = len(args)
if self.trace_args:
for arg in range(n_args):
if not TRACE_SELF and \
self.function.varnames[arg] == "self":
continue
msg += (self.function.varnames[arg] + " = " +
str(args[arg]) + ", ")
self.get_value_string(args[arg]) + ", ")
for name in self.function.varnames[n_args:]:
msg += name + " = "
if name in kwargs:
msg += str(kwargs[name])
value = kwargs[name]
else:
msg += str(self.function.defaults[name])
msg += ", "
value = self.function.defaults[name]
msg += self.get_value_string(value) + ", "
msg = msg[:-2]
else:
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
finished executing.
@ -355,18 +349,18 @@ class TraceFunction(object):
:param rv: The return value of the traced function.
"""
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
# The EXIT message.
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
msg = "<- EXIT "
if rv is not None:
msg += ": "
if self.trace_rv:
msg += str(rv)
msg += self.get_value_string(rv)
else:
msg += "---"
trace(msg, function = self.function)
trace(msg, function=self.function)
return
def trace_exception(self, exception):
@ -376,9 +370,9 @@ class TraceFunction(object):
:param exception: The raised exception.
"""
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
# The EXIT message.
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
core_msg = type(exception).__name__ + " RAISED"
msg = "<- EXIT : " + core_msg
@ -388,10 +382,29 @@ class TraceFunction(object):
if EXCEPTION_WARNING:
warnings.warn(core_msg, RuntimeWarning)
trace(msg, function = self.function)
trace(msg, function=self.function)
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,
filename, line number and function name.
@ -401,17 +414,11 @@ def trace(message, function = None):
TraceFunction.
"""
if not PYLG_ENABLE:
#-----------------------------------------------------------------------
# Don't do anything if PYLG is disabled
#-----------------------------------------------------------------------
return
if function is None:
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
# If there is no function object, we need to work out
# where the trace call was made from.
#-----------------------------------------------------------------------
# ---------------------------------------------------------------------
frames_back = 1
caller_frame = inspect.stack()[frames_back]
@ -424,25 +431,115 @@ def trace(message, function = None):
lineno = function.lineno
functionname = function.functionname
#---------------------------------------------------------------------------
# -------------------------------------------------------------------------
# If CLASS_NAME_RESOLUTION is enabled, the top element of the
# 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
# sure this is the case by ensuring that trace is only called
# outside of any function or from within functions that have the
# @TraceFunction decorator.
#---------------------------------------------------------------------------
# -------------------------------------------------------------------------
classname = ClassNameStack.get()
if classname is not None and classname != "<module>":
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.
#---------------------------------------------------------------------------
PyLg.write(str(datetime.now()) + " " +
'{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")
# -------------------------------------------------------------------------
PyLg.write(msg)

View File

@ -1,4 +1,4 @@
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# PyLg: module to facilitate and automate the process of writing runtime logs.
# 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
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Enable/disable PyLg.
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
PYLG_ENABLE = True
#-------------------------------------------------------------------------------
# Log file.
#-------------------------------------------------------------------------------
PYLG = 'pylg.log'
# -----------------------------------------------------------------------------
# The log file name.
# -----------------------------------------------------------------------------
PYLG_FILE = 'pylg.log'
#-------------------------------------------------------------------------------
# Enable class name resolution. Function names will be printed with
# their class names.
# -----------------------------------------------------------------------------
# If True, PyLg will print a warning about every exception caught to
# 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
# ONLY be called from within functions that have the @TraceFunction
# decorator OR outside of any function.
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
CLASS_NAME_RESOLUTION = False
#-------------------------------------------------------------------------------
# The default for whether TraceFunction should trace function
# parameters and return values.
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Enable/disable message logging.
# -----------------------------------------------------------------------------
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_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()
setup(
name = 'PyLg',
version = '1.1.0',
description = 'Python module to facilitate and automate the process of writing runtime logs.',
long_description = long_description,
url = 'https://gitlab.wojciechkozlowski.eu/wojtek/PyLg',
name='PyLg',
version='1.2.0',
description='Python module to facilitate and automate the process of writing runtime logs.',
long_description=long_description,
url='https://gitlab.wojciechkozlowski.eu/wojtek/PyLg',
author = 'Wojciech Kozlowski',
author_email = 'wojciech.kozlowski@vivaldi.net',
classifiers = [
author='Wojciech Kozlowski',
author_email='wojciech.kozlowski@vivaldi.net',
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'Topic :: Software Development :: Debuggers',
@ -26,7 +26,7 @@ setup(
],
keywords='development log debug trace',
include_package_data = True,
include_package_data=True,
packages=["pylg"]