From 317cebcb6b2d04c264ddcffa78373496a1b900ff Mon Sep 17 00:00:00 2001 From: Andreas Jung Date: Fri, 18 Nov 2005 15:46:28 +0000 Subject: [PATCH] forgotten to update the ZPL due to a script error --- ExceptionFormatter.py | 252 ++++++++++++++++++++++++++++++++ ITracebackSupplement.py | 92 ++++++++++++ __init__.py | 39 +++++ tests/testExceptionFormatter.py | 154 +++++++++++++++++++ unauthorized.py | 69 +++++++++ 5 files changed, 606 insertions(+) create mode 100644 ExceptionFormatter.py create mode 100644 ITracebackSupplement.py create mode 100644 __init__.py create mode 100644 tests/testExceptionFormatter.py create mode 100644 unauthorized.py diff --git a/ExceptionFormatter.py b/ExceptionFormatter.py new file mode 100644 index 0000000..662952c --- /dev/null +++ b/ExceptionFormatter.py @@ -0,0 +1,252 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""An exception formatter that shows traceback supplements and traceback info, +optionally in HTML. + +$Id$ +""" + +import sys +import cgi + + +DEBUG_EXCEPTION_FORMATTER = 1 + + +class TextExceptionFormatter: + + line_sep = '\n' + show_revisions = 0 + + def __init__(self, limit=None): + self.limit = limit + + def escape(self, s): + return s + + def getPrefix(self): + return 'Traceback (innermost last):' + + def getLimit(self): + limit = self.limit + if limit is None: + limit = getattr(sys, 'tracebacklimit', None) + return limit + + def getRevision(self, globals): + if not self.show_revisions: + return None + revision = globals.get('__revision__', None) + if revision is None: + # Incorrect but commonly used spelling + revision = globals.get('__version__', None) + + if revision is not None: + try: + revision = str(revision).strip() + except: + revision = '???' + return revision + + def formatSupplementLine(self, line): + return ' - %s' % line + + def formatObject(self, object): + return [self.formatSupplementLine(repr(object))] + + def formatSourceURL(self, url): + return [self.formatSupplementLine('URL: %s' % url)] + + def formatSupplement(self, supplement, tb): + result = [] + fmtLine = self.formatSupplementLine + + object = getattr(supplement, 'object', None) + if object is not None: + result.extend(self.formatObject(object)) + + url = getattr(supplement, 'source_url', None) + if url is not None: + result.extend(self.formatSourceURL(url)) + + line = getattr(supplement, 'line', 0) + if line == -1: + line = tb.tb_lineno + col = getattr(supplement, 'column', -1) + if line: + if col is not None and col >= 0: + result.append(fmtLine('Line %s, Column %s' % ( + line, col))) + else: + result.append(fmtLine('Line %s' % line)) + elif col is not None and col >= 0: + result.append(fmtLine('Column %s' % col)) + + expr = getattr(supplement, 'expression', None) + if expr: + result.append(fmtLine('Expression: %s' % expr)) + + warnings = getattr(supplement, 'warnings', None) + if warnings: + for warning in warnings: + result.append(fmtLine('Warning: %s' % warning)) + + extra = self.formatExtraInfo(supplement) + if extra: + result.append(extra) + return result + + def formatExtraInfo(self, supplement): + getInfo = getattr(supplement, 'getInfo', None) + if getInfo is not None: + extra = getInfo() + if extra: + return extra + return None + + def formatTracebackInfo(self, tbi): + return self.formatSupplementLine('__traceback_info__: %s' % (tbi,)) + + def formatLine(self, tb): + f = tb.tb_frame + lineno = tb.tb_lineno + co = f.f_code + filename = co.co_filename + name = co.co_name + locals = f.f_locals + globals = f.f_globals + modname = globals.get('__name__', filename) + + s = ' Module %s, line %d' % (modname, lineno) + + revision = self.getRevision(globals) + if revision: + s = s + ', rev. %s' % revision + + s = s + ', in %s' % name + + result = [] + result.append(self.escape(s)) + + # Output a traceback supplement, if any. + if locals.has_key('__traceback_supplement__'): + # Use the supplement defined in the function. + tbs = locals['__traceback_supplement__'] + elif globals.has_key('__traceback_supplement__'): + # Use the supplement defined in the module. + # This is used by Scripts (Python). + tbs = globals['__traceback_supplement__'] + else: + tbs = None + if tbs is not None: + factory = tbs[0] + args = tbs[1:] + try: + supp = factory(*args) + result.extend(self.formatSupplement(supp, tb)) + except: + if DEBUG_EXCEPTION_FORMATTER: + import traceback + traceback.print_exc() + # else just swallow the exception. + + try: + tbi = locals.get('__traceback_info__', None) + if tbi is not None: + result.append(self.formatTracebackInfo(tbi)) + except: + pass + + return self.line_sep.join(result) + + def formatExceptionOnly(self, etype, value): + import traceback + return self.line_sep.join( + traceback.format_exception_only(etype, value)) + + def formatLastLine(self, exc_line): + return self.escape(exc_line) + + def formatException(self, etype, value, tb, limit=None): + # The next line provides a way to detect recursion. + __exception_formatter__ = 1 + result = [self.getPrefix() + '\n'] + if limit is None: + limit = self.getLimit() + n = 0 + while tb is not None and (limit is None or n < limit): + if tb.tb_frame.f_locals.get('__exception_formatter__'): + # Stop recursion. + result.append('(Recursive formatException() stopped)\n') + break + line = self.formatLine(tb) + result.append(line + '\n') + tb = tb.tb_next + n = n + 1 + exc_line = self.formatExceptionOnly(etype, value) + result.append(self.formatLastLine(exc_line)) + return result + + + +class HTMLExceptionFormatter (TextExceptionFormatter): + + line_sep = '
\r\n' + + def escape(self, s): + return cgi.escape(s) + + def getPrefix(self): + return '

Traceback (innermost last):\r\n

%s

' % self.escape(exc_line) + + def formatExtraInfo(self, supplement): + getInfo = getattr(supplement, 'getInfo', None) + if getInfo is not None: + extra = getInfo(1) + if extra: + return extra + return None + + + +limit = 200 + +if hasattr(sys, 'tracebacklimit'): + limit = min(limit, sys.tracebacklimit) + +text_formatter = TextExceptionFormatter(limit) +html_formatter = HTMLExceptionFormatter(limit) + + +def format_exception(t, v, tb, limit=None, as_html=0): + if as_html: + fmt = html_formatter + else: + fmt = text_formatter + return fmt.formatException(t, v, tb, limit=limit) diff --git a/ITracebackSupplement.py b/ITracebackSupplement.py new file mode 100644 index 0000000..76937ed --- /dev/null +++ b/ITracebackSupplement.py @@ -0,0 +1,92 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""ITracebackSupplement interface definition. + +$Id$ +""" + + +from Interface import Interface +from Interface.Attribute import Attribute + +class ITracebackSupplement(Interface): + """Provides valuable information to supplement an exception traceback. + + The interface is geared toward providing meaningful feedback when + exceptions occur in user code written in mini-languages like + Zope page templates and restricted Python scripts. + """ + + source_url = Attribute( + 'source_url', + """Optional. Set to URL of the script where the exception occurred. + + Normally this generates a URL in the traceback that the user + can visit to manage the object. Set to None if unknown or + not available. + """ + ) + + object = Attribute( + 'object', + """Optional. Set to the script or template where the exception + occurred. + + Set to None if unknown or not available. + """ + ) + + line = Attribute( + 'line', + """Optional. Set to the line number (>=1) where the exception + occurred. + + Set to 0 or None if the line number is unknown. + """ + ) + + column = Attribute( + 'column', + """Optional. Set to the column offset (>=0) where the exception + occurred. + + Set to None if the column number is unknown. + """ + ) + + expression = Attribute( + 'expression', + """Optional. Set to the expression that was being evaluated. + + Set to None if not available or not applicable. + """ + ) + + warnings = Attribute( + 'warnings', + """Optional. Set to a sequence of warning messages. + + Set to None if not available, not applicable, or if the exception + itself provides enough information. + """ + ) + + + def getInfo(as_html=0): + """Optional. Returns a string containing any other useful info. + + If as_html is set, the implementation must HTML-quote the result + (normally using cgi.escape()). Returns None to provide no + extra info. + """ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..cddfc8f --- /dev/null +++ b/__init__.py @@ -0,0 +1,39 @@ +############################################################################## +# +# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +"""General exceptions that wish they were standard exceptions + +These exceptions are so general purpose that they don't belong in Zope +application-specific packages. + +$Id$ +""" + +from unauthorized import Unauthorized + +class BadRequest(Exception): + pass + +class InternalError(Exception): + pass + +class NotFound(Exception): + pass + +class Forbidden(Exception): + pass + +class MethodNotAllowed(Exception): + pass + +class Redirect(Exception): + pass diff --git a/tests/testExceptionFormatter.py b/tests/testExceptionFormatter.py new file mode 100644 index 0000000..a0ea85c --- /dev/null +++ b/tests/testExceptionFormatter.py @@ -0,0 +1,154 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +""" +ExceptionFormatter tests. + +Revision information: +$Id$ +""" + +from __future__ import nested_scopes + +from unittest import TestCase, TestSuite, main, makeSuite + +try: + from Testing.CleanUp import CleanUp # Base class w registry cleanup +except ImportError: + class CleanUp: + pass + +import sys +from zExceptions.ExceptionFormatter import format_exception + + +def tb(as_html=0): + t, v, b = sys.exc_info() + try: + return ''.join(format_exception(t, v, b, as_html=as_html)) + finally: + del b + + +class ExceptionForTesting (Exception): + pass + + + +class TestingTracebackSupplement: + + source_url = '/somepath' + line = 634 + column = 57 + warnings = ['Repent, for the end is nigh'] + + def __init__(self, expression): + self.expression = expression + + + +class Test(CleanUp, TestCase): + + def testBasicNamesText(self, as_html=0): + try: + raise ExceptionForTesting + except ExceptionForTesting: + s = tb(as_html) + # The traceback should include the name of this function. + self.assert_(s.find('testBasicNamesText') >= 0) + # The traceback should include the name of the exception. + self.assert_(s.find('ExceptionForTesting') >= 0) + else: + self.fail('no exception occurred') + + def testBasicNamesHTML(self): + self.testBasicNamesText(1) + + def testSupplement(self, as_html=0): + try: + __traceback_supplement__ = (TestingTracebackSupplement, + "You're one in a million") + raise ExceptionForTesting + except ExceptionForTesting: + s = tb(as_html) + # The source URL + self.assert_(s.find('/somepath') >= 0, s) + # The line number + self.assert_(s.find('634') >= 0, s) + # The column number + self.assert_(s.find('57') >= 0, s) + # The expression + self.assert_(s.find("You're one in a million") >= 0, s) + # The warning + self.assert_(s.find("Repent, for the end is nigh") >= 0, s) + else: + self.fail('no exception occurred') + + def testSupplementHTML(self): + self.testSupplement(1) + + def testTracebackInfo(self, as_html=0): + try: + __traceback_info__ = "Adam & Eve" + raise ExceptionForTesting + except ExceptionForTesting: + s = tb(as_html) + if as_html: + # Be sure quoting is happening. + self.assert_(s.find('Adam & Eve') >= 0, s) + else: + self.assert_(s.find('Adam & Eve') >= 0, s) + else: + self.fail('no exception occurred') + + def testTracebackInfoHTML(self): + self.testTracebackInfo(1) + + def testMultipleLevels(self): + # Makes sure many levels are shown in a traceback. + def f(n): + """Produces a (n + 1)-level traceback.""" + __traceback_info__ = 'level%d' % n + if n > 0: + f(n - 1) + else: + raise ExceptionForTesting + + try: + f(10) + except ExceptionForTesting: + s = tb() + for n in range(11): + self.assert_(s.find('level%d' % n) >= 0, s) + else: + self.fail('no exception occurred') + + def testQuoteLastLine(self): + class C: pass + try: raise TypeError, C() + except: + s = tb(1) + else: + self.fail('no exception occurred') + self.assert_(s.find('<') >= 0, s) + self.assert_(s.find('>') >= 0, s) + + + +def test_suite(): + return TestSuite(( + makeSuite(Test), + )) + +if __name__=='__main__': + main(defaultTest='test_suite') diff --git a/unauthorized.py b/unauthorized.py new file mode 100644 index 0000000..9ac7a08 --- /dev/null +++ b/unauthorized.py @@ -0,0 +1,69 @@ +############################################################################## +# +# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +""" +$Id$ +""" + +from types import StringType + +class Unauthorized(Exception): + """Some user wasn't allowed to access a resource""" + + def __init__(self, message=None, value=None, needed=None, name=None, **kw): + """Possible signatures: + + Unauthorized() + Unauthorized(message) # Note that message includes a space + Unauthorized(name) + Unauthorized(name, value) + Unauthorized(name, value, needed) + Unauthorized(message, value, needed, name) + + Where needed is a mapping objects with items represnting requirements + (e.g. {'permission': 'add spam'}). Any extra keyword arguments + provides are added to needed. + """ + if name is None and ( + not isinstance(message, StringType) or len(message.split()) <= 1): + # First arg is a name, not a message + name=message + message=None + + self.name=name + self.message=message + self.value=value + + if kw: + if needed: needed.update(kw) + else: needed=kw + + self.needed=needed + + def __str__(self): + if self.message is not None: return self.message + if self.name is not None: + return ("You are not allowed to access '%s' in this context" + % self.name) + elif self.value is not None: + return ("You are not allowed to access '%s' in this context" + % self.getValueName()) + return repr(self) + + + def getValueName(self): + v=self.value + vname=getattr(v, '__name__', None) + if vname: return vname + c = getattr(v, '__class__', type(v)) + c = getattr(c, '__name__', 'object') + return "a particular %s" % c