summaryrefslogtreecommitdiff
path: root/logutils
diff options
context:
space:
mode:
authorVinay Sajip <vinay_sajip@yahoo.co.uk>2010-10-28 18:28:38 +0100
committerVinay Sajip <vinay_sajip@yahoo.co.uk>2010-10-28 18:28:38 +0100
commit1af03d40a496fa137e4cd159e8858ee7b447aafe (patch)
treeed87aae17cd3dd67898bae9a801a1c97275239ac /logutils
parente4d7ee3231a2544d18c0caece889026a1b0d11e0 (diff)
downloadlogutils-1af03d40a496fa137e4cd159e8858ee7b447aafe.tar.gz
Changes for 0.20.2
Diffstat (limited to 'logutils')
-rw-r--r--logutils/__init__.py186
-rw-r--r--logutils/adapter.py13
-rw-r--r--logutils/dictconfig.py5
-rw-r--r--logutils/http.py17
-rw-r--r--logutils/queue.py72
-rw-r--r--logutils/testing.py64
6 files changed, 313 insertions, 44 deletions
diff --git a/logutils/__init__.py b/logutils/__init__.py
index f57f517..6ec0e0b 100644
--- a/logutils/__init__.py
+++ b/logutils/__init__.py
@@ -1,2 +1,186 @@
-__version__ = '0.1'
+"""
+The logutils package provides a set of handlers for the Python standard
+library's logging package.
+
+Some of these handlers are out-of-scope for the standard library, and
+so they are packaged here. Others are updated versions which have
+appeared in recent Python releases, but are usable with older versions
+of Python, and so are packaged here.
+"""
+import logging
+from string import Template
+
+__version__ = '0.2'
+
+class NullHandler(logging.Handler):
+ """
+ This handler does nothing. It's intended to be used to avoid the
+ "No handlers could be found for logger XXX" one-off warning. This is
+ important for library code, which may contain code to log events. If a user
+ of the library does not configure logging, the one-off warning might be
+ produced; to avoid this, the library developer simply needs to instantiate
+ a NullHandler and add it to the top-level logger of the library module or
+ package.
+ """
+
+ def handle(self, record):
+ """
+ Handle a record. Does nothing in this class, but in other
+ handlers it typically filters and then emits the record in a
+ thread-safe way.
+ """
+ pass
+
+ def emit(self, record):
+ """
+ Emit a record. This does nothing and shouldn't be called during normal
+ processing, unless you redefine :meth:`~logutils.NullHandler.handle`.
+ """
+ pass
+
+ def createLock(self):
+ """
+ Since this handler does nothing, it has no underlying I/O to protect
+ against multi-threaded access, so this method returns `None`.
+ """
+ self.lock = None
+
+class PercentStyle(object):
+
+ default_format = '%(message)s'
+ asctime_format = '%(asctime)s'
+
+ def __init__(self, fmt):
+ self._fmt = fmt or self.default_format
+
+ def usesTime(self):
+ return self._fmt.find(self.asctime_format) >= 0
+
+ def format(self, record):
+ return self._fmt % record.__dict__
+
+class StrFormatStyle(PercentStyle):
+ default_format = '{message}'
+ asctime_format = '{asctime}'
+
+ def format(self, record):
+ return self._fmt.format(**record.__dict__)
+
+
+class StringTemplateStyle(PercentStyle):
+ default_format = '${message}'
+ asctime_format = '${asctime}'
+
+ def __init__(self, fmt):
+ self._fmt = fmt or self.default_format
+ self._tpl = Template(self._fmt)
+
+ def usesTime(self):
+ fmt = self._fmt
+ return fmt.find('$asctime') >= 0 or fmt.find(self.asctime_format) >= 0
+
+ def format(self, record):
+ return self._tpl.substitute(**record.__dict__)
+
+_STYLES = {
+ '%': PercentStyle,
+ '{': StrFormatStyle,
+ '$': StringTemplateStyle
+}
+
+class Formatter(logging.Formatter):
+ """
+ Subclasses Formatter in Pythons earlier than 3.2 in order to give
+ 3.2 Formatter behaviour with respect to allowing %-, {} or $-
+ formatting.
+ """
+ def __init__(self, fmt=None, datefmt=None, style='%'):
+ """
+ Initialize the formatter with specified format strings.
+
+ Initialize the formatter either with the specified format string, or a
+ default as described above. Allow for specialized date formatting with
+ the optional datefmt argument (if omitted, you get the ISO8601 format).
+
+ Use a style parameter of '%', '{' or '$' to specify that you want to
+ use one of %-formatting, :meth:`str.format` (``{}``) formatting or
+ :class:`string.Template` formatting in your format string.
+ """
+ if style not in _STYLES:
+ raise ValueError('Style must be one of: %s' % ','.join(
+ _STYLES.keys()))
+ self._style = _STYLES[style](fmt)
+ self._fmt = self._style._fmt
+ self.datefmt = datefmt
+
+ def usesTime(self):
+ """
+ Check if the format uses the creation time of the record.
+ """
+ return self._style.usesTime()
+
+ def formatMessage(self, record):
+ return self._style.format(record)
+
+ def format(self, record):
+ """
+ Format the specified record as text.
+
+ The record's attribute dictionary is used as the operand to a
+ string formatting operation which yields the returned string.
+ Before formatting the dictionary, a couple of preparatory steps
+ are carried out. The message attribute of the record is computed
+ using LogRecord.getMessage(). If the formatting string uses the
+ time (as determined by a call to usesTime(), formatTime() is
+ called to format the event time. If there is exception information,
+ it is formatted using formatException() and appended to the message.
+ """
+ record.message = record.getMessage()
+ if self.usesTime():
+ record.asctime = self.formatTime(record, self.datefmt)
+ s = self.formatMessage(record)
+ if record.exc_info:
+ # Cache the traceback text to avoid converting it multiple times
+ # (it's constant anyway)
+ if not record.exc_text:
+ record.exc_text = self.formatException(record.exc_info)
+ if record.exc_text:
+ if s[-1:] != "\n":
+ s = s + "\n"
+ s = s + record.exc_text
+ return s
+
+
+class BraceMessage(object):
+ def __init__(self, fmt, *args, **kwargs):
+ self.fmt = fmt
+ self.args = args
+ self.kwargs = kwargs
+
+ def __str__(self):
+ return self.fmt.format(*self.args, **self.kwargs)
+
+class DollarMessage(object):
+ def __init__(self, fmt, **kwargs):
+ self.fmt = fmt
+ self.kwargs = kwargs
+
+ def __str__(self):
+ from string import Template
+ return Template(self.fmt).substitute(**self.kwargs)
+
+def hasHandlers(logger):
+ """
+ See if a logger has any handlers.
+ """
+ rv = False
+ while logger:
+ if logger.handlers:
+ rv = True
+ break
+ elif not logger.propagate:
+ break
+ else:
+ logger = logger.parent
+ return rv
diff --git a/logutils/adapter.py b/logutils/adapter.py
index a28ec91..a9f5275 100644
--- a/logutils/adapter.py
+++ b/logutils/adapter.py
@@ -16,6 +16,7 @@
#
import logging
+import logutils
class LoggerAdapter(object):
"""
@@ -125,15 +126,5 @@ class LoggerAdapter(object):
"""
See if the underlying logger has any handlers.
"""
- l = self.logger
- rv = False
- while l:
- if l.handlers:
- rv = True
- break
- elif not l.propagate:
- break
- else:
- l = l.parent
- return rv
+ return logutils.hasHandlers(self.logger)
diff --git a/logutils/dictconfig.py b/logutils/dictconfig.py
index 3516f75..a4f07cb 100644
--- a/logutils/dictconfig.py
+++ b/logutils/dictconfig.py
@@ -154,8 +154,13 @@ class BaseConfigurator(object):
# We might want to use a different one, e.g. importlib
importer = __import__
+ "Allows the importer to be redefined."
def __init__(self, config):
+ """
+ Initialise an instance with the specified configuration
+ dictionary.
+ """
self.config = ConvertingDict(config)
self.config.configurator = self
diff --git a/logutils/http.py b/logutils/http.py
index cb95159..c3c6c57 100644
--- a/logutils/http.py
+++ b/logutils/http.py
@@ -4,11 +4,20 @@ class HTTPHandler(logging.Handler):
"""
A class which sends records to a Web server, using either GET or
POST semantics.
+
+ :param host: The Web server to connect to.
+ :param url: The URL to use for the connection.
+ :param method: The HTTP method to use. GET and POST are supported.
+ :param secure: set to True if HTTPS is to be used.
+ :param credentials: Set to a username/password tuple if desired. If
+ set, a Basic authentication header is sent. WARNING:
+ if using credentials, make sure `secure` is `True`
+ to avoid sending usernames and passwords in
+ cleartext over the wire.
"""
def __init__(self, host, url, method="GET", secure=False, credentials=None):
"""
- Initialize the instance with the host, the request URL, and the method
- ("GET" or "POST")
+ Initialize an instance.
"""
logging.Handler.__init__(self)
method = method.upper()
@@ -25,6 +34,8 @@ class HTTPHandler(logging.Handler):
Default implementation of mapping the log record into a dict
that is sent as the CGI data. Overwrite in your class.
Contributed by Franz Glasner.
+
+ :param record: The record to be mapped.
"""
return record.__dict__
@@ -33,6 +44,8 @@ class HTTPHandler(logging.Handler):
Emit a record.
Send the record to the Web server as a percent-encoded dictionary
+
+ :param record: The record to be emitted.
"""
try:
import http.client, urllib.parse
diff --git a/logutils/queue.py b/logutils/queue.py
index 6b30bd5..03ed570 100644
--- a/logutils/queue.py
+++ b/logutils/queue.py
@@ -14,7 +14,24 @@
# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
-
+"""
+This module contains classes which help you work with queues. A typical
+application is when you want to log from performance-critical threads, but
+where the handlers you want to use are slow (for example,
+:class:`~logging.handlers.SMTPHandler`). In that case, you can create a queue,
+pass it to a :class:`QueueHandler` instance and use that instance with your
+loggers. Elsewhere, you can instantiate a :class:`QueueListener` with the same
+queue and some slow handlers, and call :meth:`~QueueListener.start` on it.
+This will start monitoring the queue on a separate thread and call all the
+configured handlers *on that thread*, so that your logging thread is not held
+up by the slow handlers.
+
+Note that as well as in-process queues, you can use these classes with queues
+from the :mod:`multiprocessing` module.
+
+**N.B.** This is part of the standard library since Python 3.2, so the
+version here is for use with earlier Python versions.
+"""
import logging
try:
import Queue as queue
@@ -28,9 +45,8 @@ class QueueHandler(logging.Handler):
with a multiprocessing Queue to centralise logging to file in one process
(in a multi-process application), so as to avoid file write contention
between processes.
-
- This code is new in Python 3.2, but this class can be copy pasted into
- user code for use with earlier Python versions.
+
+ :param queue: The queue to send `LogRecords` to.
"""
def __init__(self, queue):
@@ -44,9 +60,11 @@ class QueueHandler(logging.Handler):
"""
Enqueue a record.
- The base implementation uses put_nowait. You may want to override
- this method if you want to use blocking, timeouts or custom queue
- implementations.
+ The base implementation uses :meth:`~queue.Queue.put_nowait`. You may
+ want to override this method if you want to use blocking, timeouts or
+ custom queue implementations.
+
+ :param record: The record to enqueue.
"""
self.queue.put_nowait(record)
@@ -62,6 +80,8 @@ class QueueHandler(logging.Handler):
You might want to override this method if you want to convert
the record to a dict or JSON string, or send a modified copy
of the record while leaving the original intact.
+
+ :param record: The record to prepare.
"""
# The format operation gets traceback text into record.exc_text
# (if there's exception data), and also puts the message into
@@ -80,6 +100,8 @@ class QueueHandler(logging.Handler):
Emit a record.
Writes the LogRecord to the queue, preparing it for pickling first.
+
+ :param record: The record to emit.
"""
try:
self.enqueue(self.prepare(record))
@@ -93,6 +115,10 @@ class QueueListener(object):
This class implements an internal threaded listener which watches for
LogRecords being added to a queue, removes them and passes them to a
list of handlers for processing.
+
+ :param record: The queue to listen to.
+ :param handlers: The handlers to invoke on everything received from
+ the queue.
"""
_sentinel = None
@@ -110,8 +136,13 @@ class QueueListener(object):
"""
Dequeue a record and return it, optionally blocking.
- The base implementation uses get. You may want to override this method
- if you want to use timeouts or work with custom queue implementations.
+ The base implementation uses :meth:`~queue.Queue.get`. You may want to
+ override this method if you want to use timeouts or work with custom
+ queue implementations.
+
+ :param block: Whether to block if the queue is empty. If `False` and
+ the queue is empty, an :class:`~queue.Empty` exception
+ will be thrown.
"""
return self.queue.get(block)
@@ -133,6 +164,8 @@ class QueueListener(object):
This method just returns the passed-in record. You may want to
override this method if you need to do any custom marshalling or
manipulation of the record before passing it to the handlers.
+
+ :param record: The record to prepare.
"""
return record
@@ -142,6 +175,8 @@ class QueueListener(object):
This just loops through the handlers offering them the record
to handle.
+
+ :param record: The record to handle.
"""
record = self.prepare(record)
for handler in self.handlers:
@@ -192,22 +227,3 @@ class QueueListener(object):
self._thread.join()
self._thread = None
-class NullHandler(logging.Handler):
- """
- This handler does nothing. It's intended to be used to avoid the
- "No handlers could be found for logger XXX" one-off warning. This is
- important for library code, which may contain code to log events. If a user
- of the library does not configure logging, the one-off warning might be
- produced; to avoid this, the library developer simply needs to instantiate
- a NullHandler and add it to the top-level logger of the library module or
- package.
- """
- def handle(self, record):
- pass
-
- def emit(self, record):
- pass
-
- def createLock(self):
- self.lock = None
-
diff --git a/logutils/testing.py b/logutils/testing.py
index 1cb21e6..2a623ce 100644
--- a/logutils/testing.py
+++ b/logutils/testing.py
@@ -18,6 +18,13 @@ import logging
from logging.handlers import BufferingHandler
class TestHandler(BufferingHandler):
+ """
+ This handler collects records in a buffer for later inspection by
+ your unit test code.
+
+ :param matcher: The :class:`~logutils.testing.Matcher` instance to
+ use for matching.
+ """
def __init__(self, matcher):
# BufferingHandler takes a "capacity" argument
# so as to know when to flush. As we're overriding
@@ -29,19 +36,41 @@ class TestHandler(BufferingHandler):
self.matcher = matcher
def shouldFlush(self):
+ """
+ Should the buffer be flushed?
+
+ This returns `False` - you'll need to flush manually, usually after
+ your unit test code checks the buffer contents against your
+ expectations.
+ """
return False
def emit(self, record):
+ """
+ Saves the `__dict__` of the record in the `buffer` attribute,
+ and the formatted records in the `formatted` attribute.
+
+ :param record: The record to emit.
+ """
self.formatted.append(self.format(record))
self.buffer.append(record.__dict__)
def flush(self):
+ """
+ Clears out the `buffer` and `formatted` attributes.
+ """
BufferingHandler.flush(self)
self.formatted = []
def matches(self, **kwargs):
"""
Look for a saved dict whose keys/values match the supplied arguments.
+
+ Return `True` if found, else `False`.
+
+ :param kwargs: A set of keyword arguments whose names are LogRecord
+ attributes and whose values are what you want to
+ match in a stored LogRecord.
"""
result = False
for d in self.buffer:
@@ -56,6 +85,12 @@ class TestHandler(BufferingHandler):
"""
Accept a list of keyword argument values and ensure that the handler's
buffer of stored records matches the list one-for-one.
+
+ Return `True` if exactly matched, else `False`.
+
+ :param kwarglist: A list of keyword-argument dictionaries, each of
+ which will be passed to :meth:`matches` with the
+ corresponding record from the buffer.
"""
if self.count != len(kwarglist):
result = False
@@ -69,12 +104,25 @@ class TestHandler(BufferingHandler):
@property
def count(self):
+ """
+ The number of records in the buffer.
+ """
return len(self.buffer)
class Matcher(object):
-
+ """
+ This utility class matches a stored dictionary of
+ :class:`logging.LogRecord` attributes with keyword arguments
+ passed to its :meth:`~logutils.testing.Matcher.matches` method.
+ """
+
_partial_matches = ('msg', 'message')
-
+ """
+ A list of :class:`logging.LogRecord` attribute names which
+ will be checked for partial matches. If not in this list,
+ an exact match will be attempted.
+ """
+
def matches(self, d, **kwargs):
"""
Try to match a single dict with the supplied arguments.
@@ -82,6 +130,12 @@ class Matcher(object):
Keys whose values are strings and which are in self._partial_matches
will be checked for partial (i.e. substring) matches. You can extend
this scheme to (for example) do regular expression matching, etc.
+
+ Return `True` if found, else `False`.
+
+ :param kwargs: A set of keyword arguments whose names are LogRecord
+ attributes and whose values are what you want to
+ match in a stored LogRecord.
"""
result = True
for k in kwargs:
@@ -96,6 +150,12 @@ class Matcher(object):
def match_value(self, k, dv, v):
"""
Try to match a single stored value (dv) with a supplied value (v).
+
+ Return `True` if found, else `False`.
+
+ :param k: The key value (LogRecord attribute name).
+ :param dv: The stored value to match against.
+ :param v: The value to compare with the stored value.
"""
if type(v) != type(dv):
result = False