From 644cbf93f0e4e73c076d26d7b4bce74744bf87d0 Mon Sep 17 00:00:00 2001 From: Vinay Sajip Date: Mon, 24 Jan 2011 10:59:31 +0000 Subject: More changes for 0.3. --- doc/adapter.rst | 16 +++++ doc/colorize.rst | 11 +++ doc/dictconfig.rst | 15 ++++ doc/http.rst | 11 +++ doc/libraries.rst | 25 +++++++ doc/queue.rst | 6 ++ doc/testing.rst | 65 +++++++++++++++++ logutils/colorize.py | 186 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_formatter.py | 72 +++++++++++++++++++ tests/test_messages.py | 35 +++++++++ 10 files changed, 442 insertions(+) create mode 100644 doc/adapter.rst create mode 100644 doc/colorize.rst create mode 100644 doc/dictconfig.rst create mode 100644 doc/http.rst create mode 100644 doc/libraries.rst create mode 100644 doc/queue.rst create mode 100644 doc/testing.rst create mode 100644 logutils/colorize.py create mode 100644 tests/test_formatter.py create mode 100644 tests/test_messages.py 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') + + -- cgit v1.2.1