From 92c413712914c2c71b508c3f142f2482a766ad0e Mon Sep 17 00:00:00 2001 From: Vinay Sajip Date: Thu, 28 Oct 2010 18:28:38 +0100 Subject: Changes for 0.2 --- MANIFEST.in | 1 + NEWS.txt | 14 ++++ doc/Makefile | 42 +++++------ doc/conf.py | 68 +++++++++--------- doc/index.rst | 29 ++++++++ logutils/__init__.py | 186 ++++++++++++++++++++++++++++++++++++++++++++++++- logutils/adapter.py | 13 +--- logutils/dictconfig.py | 5 ++ logutils/http.py | 17 ++++- logutils/queue.py | 72 +++++++++++-------- logutils/testing.py | 64 ++++++++++++++++- setup.py | 2 +- tests/logutil_tests.py | 3 + tests/test_adapter.py | 8 +++ 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 # " v 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/. +#html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a 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() -- cgit v1.2.1