Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions Lib/cgitb.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def reset():
</table> </table> </table> </table> </table> </font> </font> </font>'''

__UNDEF__ = [] # a special sentinel object
__GETATTR_FAILED__ = '<em>&lt;ungettable attribute&gt;</em>'
__HTML_REPR_FAILED__ = '<em>&lt;unreprable value&gt;</em>'
def small(text):
if text:
return '<small>' + text + '</small>'
Expand Down Expand Up @@ -74,9 +76,32 @@ def lookup(name, frame, locals):
return 'builtin', builtins[name]
else:
if hasattr(builtins, name):
return 'builtin', getattr(builtins, name)
return 'builtin', _safe_getattr(builtins, name)
return None, __UNDEF__

class _safe_call:
def __init__(self, func, failval):
self.func = func
self.failval = failval
def __call__(self, *args):
if len(args) == 1 and (
args[0] is __HTML_REPR_FAILED__
or args[0] is __GETATTR_FAILED__
):
return args[0]
try:
return self.func(*args)
except:
return self.failval

def _pydoc_repr(value, sub='html'):
return getattr(pydoc, sub).repr(value)

_safe_getattr = _safe_call(getattr, __GETATTR_FAILED__)
_safe_html_repr = _safe_call(_pydoc_repr, __HTML_REPR_FAILED__)
_safe_text_repr = _safe_call(lambda x: _pydoc_repr(x, 'text'),
'unprintable value')

def scanvars(reader, frame, locals):
"""Scan one logical line of Python and look up values of variables used."""
vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__
Expand All @@ -85,7 +110,7 @@ def scanvars(reader, frame, locals):
if ttype == tokenize.NAME and token not in keyword.kwlist:
if lasttoken == '.':
if parent is not __UNDEF__:
value = getattr(parent, token, __UNDEF__)
value = _safe_getattr(parent, token, __UNDEF__)
vars.append((prefix + token, prefix, value))
else:
where, value = lookup(token, frame, locals)
Expand Down Expand Up @@ -127,7 +152,7 @@ def html(einfo, context=5):
call = 'in ' + strong(pydoc.html.escape(func))
if func != "<module>":
call += inspect.formatargvalues(args, varargs, varkw, locals,
formatvalue=lambda value: '=' + pydoc.html.repr(value))
formatvalue=lambda value: '=' + _safe_html_repr(value))

highlight = {}
def reader(lnum=[lnum]):
Expand Down Expand Up @@ -161,7 +186,7 @@ def reader(lnum=[lnum]):
name = strong(name)
else:
name = where + strong(name.split('.')[-1])
dump.append('%s&nbsp;= %s' % (name, pydoc.html.repr(value)))
dump.append('%s&nbsp;= %s' % (name, _safe_html_repr(value)))
else:
dump.append(name + ' <em>undefined</em>')

Expand All @@ -174,7 +199,7 @@ def reader(lnum=[lnum]):
pydoc.html.escape(str(evalue)))]
for name in dir(evalue):
if name[:1] == '_': continue
value = pydoc.html.repr(getattr(evalue, name))
value = _safe_html_repr(_safe_getattr(evalue, name))
exception.append('\n<br>%s%s&nbsp;=\n%s' % (indent, name, value))

return head + ''.join(frames) + ''.join(exception) + '''
Expand Down Expand Up @@ -211,7 +236,7 @@ def text(einfo, context=5):
call = 'in ' + func
if func != "<module>":
call += inspect.formatargvalues(args, varargs, varkw, locals,
formatvalue=lambda value: '=' + pydoc.text.repr(value))
formatvalue=lambda value: '=' + _safe_text_repr(value))

highlight = {}
def reader(lnum=[lnum]):
Expand All @@ -235,7 +260,7 @@ def reader(lnum=[lnum]):
if value is not __UNDEF__:
if where == 'global': name = 'global ' + name
elif where != 'local': name = where + name.split('.')[-1]
dump.append('%s = %s' % (name, pydoc.text.repr(value)))
dump.append('%s = %s' % (name, _safe_text_repr(value)))
else:
dump.append(name + ' undefined')

Expand All @@ -244,7 +269,7 @@ def reader(lnum=[lnum]):

exception = ['%s: %s' % (str(etype), str(evalue))]
for name in dir(evalue):
value = pydoc.text.repr(getattr(evalue, name))
value = _safe_text_repr(_safe_getattr(evalue, name))
exception.append('\n%s%s = %s' % (" "*4, name, value))

return head + ''.join(frames) + ''.join(exception) + '''
Expand Down
72 changes: 72 additions & 0 deletions Lib/test/test_cgitb.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@
import sys
import cgitb

class BadGetAttr:
"""Support class to ValueError on getattr while examining the
traceback.
"""
def __init__(self):
self._bad = False
self._h = self._fn
def __getattr__(self, attr):
if self._bad:
raise ValueError('getattr exception')
return self.__dict__['_'+attr]
def _fn(self):
self._bad = True
raise ValueError('real exception')

class BadRepr:
"""Support class to ValueError on repr while examining the
traceback."""
def __repr__(self):
raise ValueError('repr exception')
def fn(self):
raise ValueError('real exception')


class TestCgitb(unittest.TestCase):

def test_fonts(self):
Expand Down Expand Up @@ -63,6 +87,54 @@ def test_syshook_no_logdir_text_format(self):
self.assertNotIn('<p>', out)
self.assertNotIn('</p>', out)

def test_masking_getattr_exception(self):
# bpo 1047397: if examining the traceback provoked a
# ValueError from attribute look-up, the original exception
# was lost. Provoking this artificially involves a convoluted
# dance.
bga = BadGetAttr()
try:
bga.h()
except ValueError as err:
html = cgitb.html(sys.exc_info())
self.assertIn('ValueError', html)
self.assertIn(str(err), html)

def test_text_masking_getattr_exception(self):
# bpo 1047397: if examining the traceback provoked a
# ValueError from attribute look-up, the original exception
# was lost. Provoking this artificially involves a convoluted
# dance.
bga = BadGetAttr()
try:
bga.h()
except ValueError as err:
text = cgitb.text(sys.exc_info())
self.assertIn('ValueError', text)
self.assertIn(str(err), text)

def test_masking_repr_exception(self):
# bpo 1047397: if examining the traceback provoked a
# ValueError from a repr, the original exception was lost.
br = BadRepr()
try:
br.fn()
except ValueError as err:
html = cgitb.html(sys.exc_info())
self.assertIn('ValueError', html)
self.assertIn(str(err), html)

def test_masking_repr_exception(self):
# bpo 1047397: if examining the traceback provoked a
# ValueError from a repr, the original exception was lost.
br = BadRepr()
try:
br.fn()
except ValueError as err:
text = cgitb.text(sys.exc_info())
self.assertIn('ValueError', text)
self.assertIn(str(err), text)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Prevent getattr/repr exceptions from losing the original exception in cgitb.
Original patch by R Becker, updated by R James.