diff options
author | Vinay Sajip <vinay_sajip@yahoo.co.uk> | 2010-10-28 18:28:38 +0100 |
---|---|---|
committer | Vinay Sajip <vinay_sajip@yahoo.co.uk> | 2010-10-28 18:28:38 +0100 |
commit | 1af03d40a496fa137e4cd159e8858ee7b447aafe (patch) | |
tree | ed87aae17cd3dd67898bae9a801a1c97275239ac /logutils | |
parent | e4d7ee3231a2544d18c0caece889026a1b0d11e0 (diff) | |
download | logutils-1af03d40a496fa137e4cd159e8858ee7b447aafe.tar.gz |
Changes for 0.20.2
Diffstat (limited to 'logutils')
-rw-r--r-- | logutils/__init__.py | 186 | ||||
-rw-r--r-- | logutils/adapter.py | 13 | ||||
-rw-r--r-- | logutils/dictconfig.py | 5 | ||||
-rw-r--r-- | logutils/http.py | 17 | ||||
-rw-r--r-- | logutils/queue.py | 72 | ||||
-rw-r--r-- | logutils/testing.py | 64 |
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 |