summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVinay Sajip <vinay_sajip@yahoo.co.uk>2011-01-24 10:59:31 +0000
committerVinay Sajip <vinay_sajip@yahoo.co.uk>2011-01-24 10:59:31 +0000
commit644cbf93f0e4e73c076d26d7b4bce74744bf87d0 (patch)
tree9809a50297b764c18f2016c74b806be1ca447bae
parent25f7fa03a204cce1e3f982ae2833af0d5af215ab (diff)
downloadlogutils-git-644cbf93f0e4e73c076d26d7b4bce74744bf87d0.tar.gz
More changes for 0.3.
-rw-r--r--doc/adapter.rst16
-rw-r--r--doc/colorize.rst11
-rw-r--r--doc/dictconfig.rst15
-rw-r--r--doc/http.rst11
-rw-r--r--doc/libraries.rst25
-rw-r--r--doc/queue.rst6
-rw-r--r--doc/testing.rst65
-rw-r--r--logutils/colorize.py186
-rw-r--r--tests/test_formatter.py72
-rw-r--r--tests/test_messages.py35
10 files changed, 442 insertions, 0 deletions
diff --git a/doc/adapter.rst b/doc/adapter.rst
new file mode 100644
index 0000000..a277de4
--- /dev/null
+++ b/doc/adapter.rst
@@ -0,0 +1,16 @@
+Working with Logger adapters
+============================
+
+**N.B.** This is part of the standard library since Python 2.6 / 3.1, so the
+version here is for use with earlier Python versions.
+
+The class was enhanced for Python 3.2, so you may wish to use this version
+with earlier Python versions.
+
+However, note that the :class:`~logutils.adapter.LoggerAdapter` class will **not**
+work with Python 2.4 or earlier, as it uses the `extra` keyword argument which
+was added in later Python versions.
+
+.. automodule:: logutils.adapter
+ :members:
+
diff --git a/doc/colorize.rst b/doc/colorize.rst
new file mode 100644
index 0000000..f18a656
--- /dev/null
+++ b/doc/colorize.rst
@@ -0,0 +1,11 @@
+Colorizing Console Streams
+==========================
+
+``ColorizingStreamHandler`` is a handler which allows colorizing of console
+streams, described here_ in more detail.
+
+.. _here: http://plumberjack.blogspot.com/2010/12/colorizing-logging-output-in-terminals.html
+
+.. automodule:: logutils.colorize
+ :members:
+
diff --git a/doc/dictconfig.rst b/doc/dictconfig.rst
new file mode 100644
index 0000000..575e370
--- /dev/null
+++ b/doc/dictconfig.rst
@@ -0,0 +1,15 @@
+Dictionary-based Configuration
+==============================
+
+This module implements dictionary-based configuration according to PEP 391.
+
+**N.B.** This is part of the standard library since Python 2.7 / 3.2, so the
+version here is for use with earlier Python versions.
+
+.. automodule:: logutils.dictconfig
+
+.. autoclass:: logutils.dictconfig.DictConfigurator
+ :members: configure
+
+.. autofunction:: dictConfig
+
diff --git a/doc/http.rst b/doc/http.rst
new file mode 100644
index 0000000..621292d
--- /dev/null
+++ b/doc/http.rst
@@ -0,0 +1,11 @@
+Working with web sites
+======================
+
+**N.B.** The :class:`~logutils.http.HTTPHandler` class has been present in the
+:mod:`logging` package since the first release, but was enhanced for Python
+3.2 to add options for secure connections and user credentials. You may wish
+to use this version with earlier Python releases.
+
+.. automodule:: logutils.http
+ :members:
+
diff --git a/doc/libraries.rst b/doc/libraries.rst
new file mode 100644
index 0000000..611a466
--- /dev/null
+++ b/doc/libraries.rst
@@ -0,0 +1,25 @@
+Configuring Libraries
+=====================
+
+When developing libraries, you'll probably need to use the
+:class:`~logutils.NullHandler` class.
+
+**N.B.** This is part of the standard library since Python 2.7 / 3.1, so the
+version here is for use with earlier Python versions.
+
+Typical usage::
+
+ import logging
+ try:
+ from logging import NullHandler
+ except ImportError:
+ from logutils import NullHandler
+
+ # use this in all your library's subpackages/submodules
+ logger = logging.getLogger(__name__)
+
+ # use this just in your library's top-level package
+ logger.addHandler(NullHandler())
+
+.. autoclass:: logutils.NullHandler
+ :members:
diff --git a/doc/queue.rst b/doc/queue.rst
new file mode 100644
index 0000000..984631f
--- /dev/null
+++ b/doc/queue.rst
@@ -0,0 +1,6 @@
+Working with queues
+===================
+
+.. automodule:: logutils.queue
+ :members:
+
diff --git a/doc/testing.rst b/doc/testing.rst
new file mode 100644
index 0000000..1f959ca
--- /dev/null
+++ b/doc/testing.rst
@@ -0,0 +1,65 @@
+Unit testing
+============
+
+When developing unit tests, you may find the
+:class:`~logutils.testing.TestHandler` and :class:`~logutils.testing.Matcher`
+classes useful.
+
+Typical usage::
+
+ import logging
+ from logutils.testing import TestHandler, Matcher
+ import unittest
+
+ class LoggingTest(unittest.TestCase):
+ def setUp(self):
+ self.handler = h = TestHandler(Matcher())
+ self.logger = l = logging.getLogger()
+ l.addHandler(h)
+
+ def tearDown(self):
+ self.logger.removeHandler(self.handler)
+ self.handler.close()
+
+ def test_simple(self):
+ "Simple test of logging test harness."
+ # Just as a demo, let's log some messages.
+ # Only one should show up in the log.
+ self.logger.debug("This won't show up.")
+ self.logger.info("Neither will this.")
+ self.logger.warning("But this will.")
+ h = self.handler
+ self.assertTrue(h.matches(levelno=logging.WARNING))
+ self.assertFalse(h.matches(levelno=logging.DEBUG))
+ self.assertFalse(h.matches(levelno=logging.INFO))
+
+ def test_partial(self):
+ "Test of partial matching in logging test harness."
+ # Just as a demo, let's log some messages.
+ # Only one should show up in the log.
+ self.logger.debug("This won't show up.")
+ self.logger.info("Neither will this.")
+ self.logger.warning("But this will.")
+ h = self.handler
+ self.assertTrue(h.matches(msg="ut th")) # from "But this will"
+ self.assertTrue(h.matches(message="ut th")) # from "But this will"
+ self.assertFalse(h.matches(message="either"))
+ self.assertFalse(h.matches(message="won't"))
+
+ def test_multiple(self):
+ "Test of matching multiple values in logging test harness."
+ # Just as a demo, let's log some messages.
+ # Only one should show up in the log.
+ self.logger.debug("This won't show up.")
+ self.logger.info("Neither will this.")
+ self.logger.warning("But this will.")
+ self.logger.error("And so will this.")
+ h = self.handler
+ self.assertTrue(h.matches(levelno=logging.WARNING,
+ message='ut thi'))
+ self.assertTrue(h.matches(levelno=logging.ERROR,
+ message='nd so wi'))
+ self.assertFalse(h.matches(levelno=logging.INFO))
+
+.. automodule:: logutils.testing
+ :members:
diff --git a/logutils/colorize.py b/logutils/colorize.py
new file mode 100644
index 0000000..3dfaad0
--- /dev/null
+++ b/logutils/colorize.py
@@ -0,0 +1,186 @@
+#
+# Copyright (C) 2010-2011 Vinay Sajip. All rights reserved.
+#
+import ctypes
+import logging
+import os
+
+class ColorizingStreamHandler(logging.StreamHandler):
+ """
+ A stream handler which supports colorizing of console streams
+ under Windows, Linux and Mac OS X.
+
+ :param strm: The stream to colorize - typically ``sys.stdout``
+ or ``sys.stderr``.
+ """
+
+ # color names to indices
+ color_map = {
+ 'black': 0,
+ 'red': 1,
+ 'green': 2,
+ 'yellow': 3,
+ 'blue': 4,
+ 'magenta': 5,
+ 'cyan': 6,
+ 'white': 7,
+ }
+
+ #levels to (background, foreground, bold/intense)
+ if os.name == 'nt':
+ level_map = {
+ logging.DEBUG: (None, 'blue', True),
+ logging.INFO: (None, 'white', False),
+ logging.WARNING: (None, 'yellow', True),
+ logging.ERROR: (None, 'red', True),
+ logging.CRITICAL: ('red', 'white', True),
+ }
+ else:
+ "Maps levels to colour/intensity settings."
+ level_map = {
+ logging.DEBUG: (None, 'blue', False),
+ logging.INFO: (None, 'black', False),
+ logging.WARNING: (None, 'yellow', False),
+ logging.ERROR: (None, 'red', False),
+ logging.CRITICAL: ('red', 'white', True),
+ }
+
+ csi = '\x1b['
+ reset = '\x1b[0m'
+
+ @property
+ def is_tty(self):
+ "Returns true if the handler's stream is a terminal."
+ isatty = getattr(self.stream, 'isatty', None)
+ return isatty and isatty()
+
+ def emit(self, record):
+ try:
+ message = self.format(record)
+ stream = self.stream
+ if not self.is_tty:
+ stream.write(message)
+ else:
+ self.output_colorized(message)
+ stream.write(getattr(self, 'terminator', '\n'))
+ self.flush()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except:
+ self.handleError(record)
+
+ if os.name != 'nt':
+ def output_colorized(self, message):
+ """
+ Output a colorized message.
+
+ On Linux and Mac OS X, this method just writes the
+ already-colorized message to the stream, since on these
+ platforms console streams accept ANSI escape sequences
+ for colorization. On Windows, this handler implements a
+ subset of ANSI escape sequence handling by parsing the
+ message, extracting the sequences and making Win32 API
+ calls to colorize the output.
+
+ :param message: The message to colorize and output.
+ """
+ self.stream.write(message)
+ else:
+ import re
+ ansi_esc = re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m')
+
+ nt_color_map = {
+ 0: 0x00, # black
+ 1: 0x04, # red
+ 2: 0x02, # green
+ 3: 0x06, # yellow
+ 4: 0x01, # blue
+ 5: 0x05, # magenta
+ 6: 0x03, # cyan
+ 7: 0x07, # white
+ }
+
+ def output_colorized(self, message):
+ """
+ Output a colorized message.
+
+ On Linux and Mac OS X, this method just writes the
+ already-colorized message to the stream, since on these
+ platforms console streams accept ANSI escape sequences
+ for colorization. On Windows, this handler implements a
+ subset of ANSI escape sequence handling by parsing the
+ message, extracting the sequences and making Win32 API
+ calls to colorize the output.
+
+ :param message: The message to colorize and output.
+ """
+ parts = self.ansi_esc.split(message)
+ write = self.stream.write
+ h = None
+ fd = getattr(self.stream, 'fileno', None)
+ if fd is not None:
+ fd = fd()
+ if fd in (1, 2): # stdout or stderr
+ h = ctypes.windll.kernel32.GetStdHandle(-10 - fd)
+ while parts:
+ text = parts.pop(0)
+ if text:
+ write(text)
+ if parts:
+ params = parts.pop(0)
+ if h is not None:
+ params = [int(p) for p in params.split(';')]
+ color = 0
+ for p in params:
+ if 40 <= p <= 47:
+ color |= self.nt_color_map[p - 40] << 4
+ elif 30 <= p <= 37:
+ color |= self.nt_color_map[p - 30]
+ elif p == 1:
+ color |= 0x08 # foreground intensity on
+ elif p == 0: # reset to default color
+ color = 0x07
+ else:
+ pass # error condition ignored
+ ctypes.windll.kernel32.SetConsoleTextAttribute(h, color)
+
+ def colorize(self, message, record):
+ """
+ Colorize a message for a logging event.
+
+ This implementation uses the ``level_map`` class attribute to
+ map the LogRecord's level to a colour/intensity setting, which is
+ then applied to the whole message.
+
+ :param message: The message to colorize.
+ :param record: The ``LogRecord`` for the message.
+ """
+ if record.levelno in self.level_map:
+ bg, fg, bold = self.level_map[record.levelno]
+ params = []
+ if bg in self.color_map:
+ params.append(str(self.color_map[bg] + 40))
+ if fg in self.color_map:
+ params.append(str(self.color_map[fg] + 30))
+ if bold:
+ params.append('1')
+ if params:
+ message = ''.join((self.csi, ';'.join(params),
+ 'm', message, self.reset))
+ return message
+
+ def format(self, record):
+ """
+ Formats a record for output.
+
+ This implementation colorizes the message line, but leaves
+ any traceback unolorized.
+ """
+ message = logging.StreamHandler.format(self, record)
+ if self.is_tty:
+ # Don't colorize any traceback
+ parts = message.split('\n', 1)
+ parts[0] = self.colorize(parts[0], record)
+ message = '\n'.join(parts)
+ return message
+
diff --git a/tests/test_formatter.py b/tests/test_formatter.py
new file mode 100644
index 0000000..2c05428
--- /dev/null
+++ b/tests/test_formatter.py
@@ -0,0 +1,72 @@
+import logging
+import logutils
+import os
+import sys
+import unittest
+
+class FormatterTest(unittest.TestCase):
+ def setUp(self):
+ self.common = {
+ 'name': 'formatter.test',
+ 'level': logging.DEBUG,
+ 'pathname': os.path.join('path', 'to', 'dummy.ext'),
+ 'lineno': 42,
+ 'exc_info': None,
+ 'func': None,
+ 'msg': 'Message with %d %s',
+ 'args': (2, 'placeholders'),
+ }
+ self.variants = {
+ }
+
+ def get_record(self, name=None):
+ result = dict(self.common)
+ if name is not None:
+ result.update(self.variants[name])
+ return logging.makeLogRecord(result)
+
+ def test_percent(self):
+ "Test %-formatting"
+ r = self.get_record()
+ f = logutils.Formatter('${%(message)s}')
+ self.assertEqual(f.format(r), '${Message with 2 placeholders}')
+ f = logutils.Formatter('%(random)s')
+ self.assertRaises(KeyError, f.format, r)
+ self.assertFalse(f.usesTime())
+ f = logutils.Formatter('%(asctime)s')
+ self.assertTrue(f.usesTime())
+ f = logutils.Formatter('asctime')
+ self.assertFalse(f.usesTime())
+
+ if sys.version_info[:2] >= (2, 6):
+ def test_braces(self):
+ "Test {}-formatting"
+ r = self.get_record()
+ f = logutils.Formatter('$%{message}%$', style='{')
+ self.assertEqual(f.format(r), '$%Message with 2 placeholders%$')
+ f = logutils.Formatter('{random}', style='{')
+ self.assertRaises(KeyError, f.format, r)
+ self.assertFalse(f.usesTime())
+ f = logutils.Formatter('{asctime}', style='{')
+ self.assertTrue(f.usesTime())
+ f = logutils.Formatter('asctime', style='{')
+ self.assertFalse(f.usesTime())
+
+ def test_dollars(self):
+ "Test $-formatting"
+ r = self.get_record()
+ f = logutils.Formatter('$message', style='$')
+ self.assertEqual(f.format(r), 'Message with 2 placeholders')
+ f = logutils.Formatter('$$%${message}%$$', style='$')
+ self.assertEqual(f.format(r), '$%Message with 2 placeholders%$')
+ f = logutils.Formatter('${random}', style='$')
+ self.assertRaises(KeyError, f.format, r)
+ self.assertFalse(f.usesTime())
+ f = logutils.Formatter('${asctime}', style='$')
+ self.assertTrue(f.usesTime())
+ f = logutils.Formatter('$asctime', style='$')
+ self.assertTrue(f.usesTime())
+ f = logutils.Formatter('asctime', style='$')
+ self.assertFalse(f.usesTime())
+
+
diff --git a/tests/test_messages.py b/tests/test_messages.py
new file mode 100644
index 0000000..6ceed42
--- /dev/null
+++ b/tests/test_messages.py
@@ -0,0 +1,35 @@
+import logutils
+import sys
+import unittest
+
+class MessageTest(unittest.TestCase):
+ if sys.version_info[:2] >= (2, 6):
+ def test_braces(self):
+ "Test whether brace-formatting works."
+ __ = logutils.BraceMessage
+ m = __('Message with {0} {1}', 2, 'placeholders')
+ self.assertEqual(str(m), 'Message with 2 placeholders')
+ m = __('Message with {0:d} {1}', 2, 'placeholders')
+ self.assertEqual(str(m), 'Message with 2 placeholders')
+ m = __('Message without {0:x} {1}', 16, 'placeholders')
+ self.assertEqual(str(m), 'Message without 10 placeholders')
+
+ class Dummy:
+ pass
+
+ dummy = Dummy()
+ dummy.x, dummy.y = 0.0, 1.0
+ m = __('Message with coordinates: ({point.x:.2f}, {point.y:.2f})',
+ point=dummy)
+ self.assertEqual(str(m), 'Message with coordinates: (0.00, 1.00)')
+
+ def test_dollars(self):
+ "Test whether dollar-formatting works."
+ __ = logutils.DollarMessage
+ m = __('Message with $num ${what}', num=2, what='placeholders')
+ self.assertEqual(str(m), 'Message with 2 placeholders')
+ ignored = object()
+ self.assertRaises(TypeError, __, 'Message with $num ${what}',
+ ignored, num=2, what='placeholders')
+
+