summaryrefslogtreecommitdiff
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
commit92c413712914c2c71b508c3f142f2482a766ad0e (patch)
treea58e33430857498c58f85d5386228f770b962462
parent44c0c75e67086a0a9b22e4e614c7774d44d9fe84 (diff)
downloadlogutils-git-92c413712914c2c71b508c3f142f2482a766ad0e.tar.gz
Changes for 0.20.2
-rw-r--r--MANIFEST.in1
-rw-r--r--NEWS.txt14
-rw-r--r--doc/Makefile42
-rw-r--r--doc/conf.py68
-rw-r--r--doc/index.rst29
-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
-rw-r--r--setup.py2
-rw-r--r--tests/logutil_tests.py3
-rw-r--r--tests/test_adapter.py8
14 files changed, 423 insertions, 101 deletions
diff --git a/MANIFEST.in b/MANIFEST.in
index 98e10af..0265041 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -7,5 +7,6 @@ include doc/_static/*.js
include doc/_static/*.png
include doc/*.rst
include doc/conf.py
+include doc/Makefile
include tests/*.py
diff --git a/NEWS.txt b/NEWS.txt
index abffcf3..5f5f32a 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -1,6 +1,20 @@
What's New in logutils
======================
+Version 0.2
+-----------
+
+- Updated docstrings for improved documentation.
+- Added hasHanders() function.
+- Changed LoggerAdapter.hasHandlers() to use logutils.hasHandlers().
+- Documentation improvements.
+- NullHandler moved to logutils package (from queue package).
+- Formatter added to logutils package. Adds support for {}- and $-formatting
+ in format strings, as well as %-formatting.
+- BraceMessage and DollarMessage classes added to facilitate {}- and $-
+ formatting in logging calls (as opposed to Formatter formats).
+- Added some more unit tests.
+
Version 0.1
-----------
diff --git a/doc/Makefile b/doc/Makefile
index ef87680..dbc6dec 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -9,7 +9,7 @@ PAPER =
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html web pickle htmlhelp latex changes linkcheck
@@ -24,52 +24,52 @@ help:
@echo " linkcheck to check all external links for integrity"
clean:
- -rm -rf .build/*
+ -rm -rf _build/*
html:
- mkdir -p .build/html .build/doctrees
- $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html
+ mkdir -p _build/html _build/doctrees
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html
@echo
- @echo "Build finished. The HTML pages are in .build/html."
+ @echo "Build finished. The HTML pages are in _build/html."
pickle:
- mkdir -p .build/pickle .build/doctrees
- $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle
+ mkdir -p _build/pickle _build/doctrees
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle
@echo
@echo "Build finished; now you can process the pickle files."
web: pickle
json:
- mkdir -p .build/json .build/doctrees
- $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) .build/json
+ mkdir -p _build/json _build/doctrees
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
- mkdir -p .build/htmlhelp .build/doctrees
- $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp
+ mkdir -p _build/htmlhelp _build/doctrees
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
- ".hhp project file in .build/htmlhelp."
+ ".hhp project file in _build/htmlhelp."
latex:
- mkdir -p .build/latex .build/doctrees
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex
+ mkdir -p _build/latex _build/doctrees
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex
@echo
- @echo "Build finished; the LaTeX files are in .build/latex."
+ @echo "Build finished; the LaTeX files are in _build/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
- mkdir -p .build/changes .build/doctrees
- $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes
+ mkdir -p _build/changes _build/doctrees
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes
@echo
- @echo "The overview file is in .build/changes."
+ @echo "The overview file is in _build/changes."
linkcheck:
- mkdir -p .build/linkcheck .build/doctrees
- $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck
+ mkdir -p _build/linkcheck _build/doctrees
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
- "or in .build/linkcheck/output.txt."
+ "or in _build/linkcheck/output.txt."
diff --git a/doc/conf.py b/doc/conf.py
index eedf48d..a1af384 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
#
-# argparse documentation build configuration file, created by
-# sphinx-quickstart on Thu Mar 26 10:47:44 2009.
+# Logutils documentation build configuration file, created by
+# sphinx-quickstart on Fri Oct 1 15:54:52 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
+# The contents of this file are pickled, so don't put values in the namespace
+# that aren't pickleable (module imports are okay, they're removed automatically).
+#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
@@ -13,16 +16,17 @@
import sys, os
-# If extensions (or modules to document with autodoc) are in another directory,
+# If your extensions (or modules documented by autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.append(os.path.abspath('.'))
+sys.path.append(os.path.abspath('..'))
-# -- General configuration -----------------------------------------------------
+# General configuration
+# ---------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.doctest', 'sphinx.ext.coverage']
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -37,7 +41,7 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
-project = u'logutils'
+project = u'Logutils'
copyright = u'2010, Vinay Sajip'
# The version info for the project you're documenting, acts as replacement for
@@ -45,9 +49,9 @@ copyright = u'2010, Vinay Sajip'
# built documents.
#
# The short X.Y version.
-version = '0.1'
+version = '0.2'
# The full version, including alpha/beta/rc tags.
-release = '0.1'
+release = '0.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -64,7 +68,7 @@ release = '0.1'
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
-exclude_trees = []
+exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
@@ -83,23 +87,14 @@ exclude_trees = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
-# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
-
-
-# -- Options for HTML output ---------------------------------------------------
-# The theme to use for HTML and HTML Help pages. Major themes that come with
-# Sphinx are currently 'default' and 'sphinxdoc'.
-html_theme = 'default'
+# Options for HTML output
+# -----------------------
-# Theme options are theme-specific and customize the look and feel of a theme
-# further. For a list of options available for each theme, see the
-# documentation.
-#html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# The style sheet to use for HTML and HTML Help pages. A file of that name
+# must exist either in Sphinx' static/ path, or in one of the custom paths
+# given in html_static_path.
+html_style = 'default.css'
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
@@ -146,8 +141,8 @@ html_static_path = ['_static']
# If true, the index is split into individual pages for each letter.
#html_split_index = False
-# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# If true, the reST sources are included in the HTML build as _sources/<name>.
+#html_copy_source = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
@@ -158,10 +153,11 @@ html_static_path = ['_static']
#html_file_suffix = ''
# Output file base name for HTML help builder.
-htmlhelp_basename = 'logutils'
+htmlhelp_basename = 'Logutilsdoc'
-# -- Options for LaTeX output --------------------------------------------------
+# Options for LaTeX output
+# ------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
@@ -170,10 +166,10 @@ htmlhelp_basename = 'logutils'
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
+# (source start file, target name, title, author, document class [howto/manual]).
latex_documents = [
- ('index', 'logutils.tex', u'logutils Documentation',
- u'Vinay Sajip', 'manual'),
+ ('index', 'Logutils.tex', ur'Logutils Documentation',
+ ur'Vinay Sajip', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -193,6 +189,8 @@ latex_documents = [
# If false, no module index is generated.
#latex_use_modindex = True
-# Python code that is treated like it were put in a testsetup directive for
-# every file that is tested, and for every group.
-doctest_global_setup = "import logutils"
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+ 'http://docs.python.org/dev': None,
+}
diff --git a/doc/index.rst b/doc/index.rst
index e69de29..2d971ee 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -0,0 +1,29 @@
+.. Logutils documentation master file, created by sphinx-quickstart on Fri Oct 1 15:54:52 2010.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Logutils documentation
+======================
+
+.. automodule:: logutils
+
+There are a number of subcomponents to this package, relating to particular
+tasks you may want to perform:
+
+.. toctree::
+ :maxdepth: 2
+
+ libraries
+ queue
+ testing
+ dictconfig
+ adapter
+ http
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
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
diff --git a/setup.py b/setup.py
index 29dcac6..a8a7da6 100644
--- a/setup.py
+++ b/setup.py
@@ -63,7 +63,7 @@ distutils.core.setup(
'Development Status :: 4 - Beta',
'Environment :: Console',
'Intended Audience :: Developers',
- 'License :: OSI Approved :: New BSD License',
+ 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development',
diff --git a/tests/logutil_tests.py b/tests/logutil_tests.py
index 3767e96..8f1bfb6 100644
--- a/tests/logutil_tests.py
+++ b/tests/logutil_tests.py
@@ -2,6 +2,9 @@ import sys
from test_testing import LoggingTest
from test_dictconfig import ConfigDictTest
from test_queue import QueueTest
+from test_formatter import FormatterTest
+from test_messages import MessageTest
+
# The adapter won't work in < 2.5 because the "extra" parameter used by it
# only appeared in 2.5 :-(
if sys.version_info[:2] >= (2, 5):
diff --git a/tests/test_adapter.py b/tests/test_adapter.py
index f195255..bb008a0 100644
--- a/tests/test_adapter.py
+++ b/tests/test_adapter.py
@@ -54,6 +54,14 @@ class AdapterTest(unittest.TestCase):
message='nd so w'))
self.assertFalse(h.matches(levelno=logging.INFO))
+ def test_hashandlers(self):
+ "Test of hasHandlers() functionality."
+ self.assertTrue(self.adapter.hasHandlers())
+ self.logger.removeHandler(self.handler)
+ self.assertFalse(self.adapter.hasHandlers())
+ self.logger.addHandler(self.handler)
+ self.assertTrue(self.adapter.hasHandlers())
+
if __name__ == '__main__':
unittest.main()