Merge lp:~allenap/launchpad/use-zope-tb-formatter into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 12351
Proposed branch: lp:~allenap/launchpad/use-zope-tb-formatter
Merge into: lp:launchpad
Diff against target: 252 lines (+94/-21)
4 files modified
lib/canonical/launchpad/scripts/logger.py (+15/-9)
lib/canonical/launchpad/scripts/tests/test_logger.py (+44/-2)
lib/lp/services/tests/test_utils.py (+23/-10)
lib/lp/services/utils.py (+12/-0)
To merge this branch: bzr merge lp:~allenap/launchpad/use-zope-tb-formatter
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+48843@code.launchpad.net

Commit message

[r=jtv][bug=715659] Use zope.exceptions to format logged exceptions so that __traceback_info__ is reported.

Description of the change

This branch does two things:

- Adds a new function, traceback_info(), that injects a
  __traceback_info__ local variable into the calling frame. From
  its docstring:

    This is more aesthetically pleasing that assigning to
    __traceback_info__, but it more importantly avoids spurious
    lint warnings about unused local variables, and helps to
    avoid typos.

- Uses zope.exceptions to format logged exceptions, which means that
  the traceback info will be included.

The OOPS tools already use zope.exceptions to format exceptions so
this extra info should show up in OOPS reports soon.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Awesome. I had to struggle to come up with a nit to pick.

What I came up with was pitiful:

213 + # The local variable __traceback_info__ is set by `traceback_info`.

Text may be improved if the passive voice is avoided.

Jeroen

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/scripts/logger.py'
2--- lib/canonical/launchpad/scripts/logger.py 2010-12-20 20:36:16 +0000
3+++ lib/canonical/launchpad/scripts/logger.py 2011-02-09 11:03:45 +0000
4@@ -15,13 +15,19 @@
5
6 # Don't import stuff from this module. Import it from canonical.scripts
7 __all__ = [
8+ 'DEBUG2',
9+ 'DEBUG3',
10+ 'DEBUG4',
11+ 'DEBUG5',
12+ 'DEBUG6',
13+ 'DEBUG7',
14+ 'DEBUG8',
15+ 'DEBUG9',
16+ 'LaunchpadFormatter',
17 'log',
18 'logger',
19 'logger_options',
20 'OopsHandler',
21- 'LaunchpadFormatter',
22- 'DEBUG2', 'DEBUG3', 'DEBUG4', 'DEBUG5',
23- 'DEBUG6', 'DEBUG7', 'DEBUG8', 'DEBUG9',
24 ]
25
26
27@@ -38,10 +44,11 @@
28 import re
29 import sys
30 import time
31-import traceback
32+from traceback import format_exception_only
33
34 from pytz import utc
35 from zope.component import getUtility
36+from zope.exceptions.log import Formatter
37
38 from canonical.base import base
39 from canonical.config import config
40@@ -55,7 +62,6 @@
41 )
42 from lp.services.log import loglevels
43
44-
45 # Reexport our custom loglevels for old callsites. These callsites
46 # should be importing the symbols from lp.services.log.loglevels
47 DEBUG2 = loglevels.DEBUG2
48@@ -91,7 +97,7 @@
49 self.handleError(record)
50
51
52-class LaunchpadFormatter(logging.Formatter):
53+class LaunchpadFormatter(Formatter):
54 """logging.Formatter encoding our preferred output format."""
55
56 def __init__(self, fmt=None, datefmt=None):
57@@ -124,7 +130,7 @@
58 Returns the URL, or the formatted exception if the Librarian is
59 not available.
60 """
61- traceback = logging.Formatter.formatException(self, ei)
62+ traceback = LaunchpadFormatter.formatException(self, ei)
63 # Uncomment this line to stop exception storage in the librarian.
64 # Useful for debugging tests.
65 # return traceback
66@@ -158,7 +164,7 @@
67 # information, we can stuff our own problems in there too.
68 return '%s\n\nException raised in formatter:\n%s\n' % (
69 traceback,
70- logging.Formatter.formatException(self, sys.exc_info()))
71+ LaunchpadFormatter.formatException(self, sys.exc_info()))
72
73
74 def logger_options(parser, default=logging.INFO):
75@@ -408,7 +414,7 @@
76 def shortException(self, msg, *args):
77 """Like Logger.exception, but does not print a traceback."""
78 exctype, value = sys.exc_info()[:2]
79- report = ''.join(traceback.format_exception_only(exctype, value))
80+ report = ''.join(format_exception_only(exctype, value))
81 # _log.error interpolates msg, so we need to escape % chars
82 msg += '\n' + report.rstrip('\n').replace('%', '%%')
83 self._log.error(msg, *args)
84
85=== modified file 'lib/canonical/launchpad/scripts/tests/test_logger.py'
86--- lib/canonical/launchpad/scripts/tests/test_logger.py 2010-10-04 19:50:45 +0000
87+++ lib/canonical/launchpad/scripts/tests/test_logger.py 2011-02-09 11:03:45 +0000
88@@ -6,13 +6,55 @@
89 __metaclass__ = type
90 __all__ = []
91
92+import doctest
93+from sys import exc_info
94 import unittest
95
96+from testtools.matchers import DocTestMatches
97+
98+from canonical.launchpad.scripts.logger import LaunchpadFormatter
99 from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite
100 from canonical.testing.layers import BaseLayer
101+from lp.services.utils import traceback_info
102+from lp.testing import TestCase
103+
104+
105+DOCTEST_FLAGS = (
106+ doctest.ELLIPSIS |
107+ doctest.NORMALIZE_WHITESPACE |
108+ doctest.REPORT_NDIFF)
109+
110+
111+class TestLaunchpadFormatter(TestCase):
112+ """Tests of `LaunchpadFormatter`."""
113+
114+ def test_traceback_info(self):
115+ # LaunchpadFormatter inherits from zope.exceptions.log.Formatter, so
116+ # __traceback_info__ annotations are included in formatted exceptions.
117+
118+ traceback_info("Captain Kirk")
119+
120+ try:
121+ 0/0
122+ except ZeroDivisionError:
123+ info = exc_info()
124+
125+ self.assertThat(
126+ LaunchpadFormatter().formatException(info),
127+ DocTestMatches(
128+ flags=DOCTEST_FLAGS, example="""
129+ Traceback (most recent call last):
130+ ...
131+ __traceback_info__: Captain Kirk
132+ ZeroDivisionError: ...
133+ """))
134
135
136 def test_suite():
137- return unittest.TestSuite([
138+ suite = unittest.TestSuite()
139+ suite.addTest(
140 LayeredDocFileSuite(
141- 'test_logger.txt', layer=BaseLayer),])
142+ 'test_logger.txt', layer=BaseLayer))
143+ suite.addTest(
144+ unittest.TestLoader().loadTestsFromName(__name__))
145+ return suite
146
147=== modified file 'lib/lp/services/tests/test_utils.py'
148--- lib/lp/services/tests/test_utils.py 2011-01-26 22:07:08 +0000
149+++ lib/lp/services/tests/test_utils.py 2011-02-09 11:03:45 +0000
150@@ -14,6 +14,7 @@
151 decorate_with,
152 docstring_dedent,
153 iter_split,
154+ traceback_info,
155 )
156 from lp.testing import TestCase
157
158@@ -42,35 +43,35 @@
159 # The same iterator can be used multiple times.
160 iterator = CachingIterator(itertools.count())
161 self.assertEqual(
162- [0,1,2,3,4], list(itertools.islice(iterator, 0, 5)))
163+ [0, 1, 2, 3, 4], list(itertools.islice(iterator, 0, 5)))
164 self.assertEqual(
165- [0,1,2,3,4], list(itertools.islice(iterator, 0, 5)))
166+ [0, 1, 2, 3, 4], list(itertools.islice(iterator, 0, 5)))
167
168 def test_more_values(self):
169 # If a subsequent call to iter causes more values to be fetched, they
170 # are also cached.
171 iterator = CachingIterator(itertools.count())
172 self.assertEqual(
173- [0,1,2], list(itertools.islice(iterator, 0, 3)))
174+ [0, 1, 2], list(itertools.islice(iterator, 0, 3)))
175 self.assertEqual(
176- [0,1,2,3,4], list(itertools.islice(iterator, 0, 5)))
177+ [0, 1, 2, 3, 4], list(itertools.islice(iterator, 0, 5)))
178
179 def test_limited_iterator(self):
180 # Make sure that StopIteration is handled correctly.
181- iterator = CachingIterator(iter([0,1,2,3,4]))
182+ iterator = CachingIterator(iter([0, 1, 2, 3, 4]))
183 self.assertEqual(
184- [0,1,2], list(itertools.islice(iterator, 0, 3)))
185- self.assertEqual([0,1,2,3,4], list(iterator))
186+ [0, 1, 2], list(itertools.islice(iterator, 0, 3)))
187+ self.assertEqual([0, 1, 2, 3, 4], list(iterator))
188
189 def test_parallel_iteration(self):
190 # There can be parallel iterators over the CachingIterator.
191- ci = CachingIterator(iter([0,1,2,3,4]))
192+ ci = CachingIterator(iter([0, 1, 2, 3, 4]))
193 i1 = iter(ci)
194 i2 = iter(ci)
195 self.assertEqual(0, i1.next())
196 self.assertEqual(0, i2.next())
197- self.assertEqual([1,2,3,4], list(i2))
198- self.assertEqual([1,2,3,4], list(i1))
199+ self.assertEqual([1, 2, 3, 4], list(i2))
200+ self.assertEqual([1, 2, 3, 4], list(i1))
201
202
203 class TestDecorateWith(TestCase):
204@@ -155,5 +156,17 @@
205 result = 'This is a multiline docstring.\n\nThis is the second line.'
206 self.assertEqual(docstring_dedent(docstring), result)
207
208+
209+class TestTracebackInfo(TestCase):
210+ """Tests of `traceback_info`."""
211+
212+ def test(self):
213+ # `traceback_info` sets the local variable __traceback_info__ in the
214+ # caller's frame.
215+ self.assertEqual(None, locals().get("__traceback_info__"))
216+ traceback_info("Pugwash")
217+ self.assertEqual("Pugwash", locals().get("__traceback_info__"))
218+
219+
220 def test_suite():
221 return unittest.TestLoader().loadTestsFromName(__name__)
222
223=== modified file 'lib/lp/services/utils.py'
224--- lib/lp/services/utils.py 2011-02-08 15:08:01 +0000
225+++ lib/lp/services/utils.py 2011-02-09 11:03:45 +0000
226@@ -15,10 +15,12 @@
227 'iter_split',
228 'synchronize',
229 'text_delta',
230+ 'traceback_info',
231 'value_string',
232 ]
233
234 from itertools import tee
235+import sys
236 from textwrap import dedent
237
238 from lazr.enum import BaseItem
239@@ -150,3 +152,13 @@
240 # Make sure there is at least one newline so the split works.
241 first, rest = (s+'\n').split('\n', 1)
242 return (first + '\n' + dedent(rest)).strip()
243+
244+
245+def traceback_info(info):
246+ """Set `__traceback_info__` in the caller's locals.
247+
248+ This is more aesthetically pleasing that assigning to __traceback_info__,
249+ but it more importantly avoids spurious lint warnings about unused local
250+ variables, and helps to avoid typos.
251+ """
252+ sys._getframe(1).f_locals["__traceback_info__"] = info