Description
ParseError defines a __eq__
method, but no __hash__
method. The default __hash__
method fails because the error
attribute is a list. This breaks PyTest and Python's traceback.format_exception_only
function.
Discovery
During testing of a new grammar, PyTest reported an internal error when Parsley raised a ParseError.
INTERNALERROR> File "/home/joel/.virtualenvs/meetup2xibo/lib/python3.5/site-packages/_pytest/_code/code.py", line 481, in exconly
INTERNALERROR> lines = format_exception_only(self.type, self.value)
INTERNALERROR> File "/usr/lib/python3.5/traceback.py", line 136, in format_exception_only
INTERNALERROR> return list(TracebackException(etype, value, None).format_exception_only())
INTERNALERROR> File "/usr/lib/python3.5/traceback.py", line 439, in __init__
INTERNALERROR> _seen.add(exc_value)
INTERNALERROR> TypeError: unhashable type: 'ParseError'
The TypeError is raised by Python standard library function traceback.format_exception_only
.
A Simple Test
The following code demonstrates the use of traceback.format_exception_only
and raises the TypeError without involving PyTest.
from parsley import makeGrammar, ParseError
import sys
import traceback
def format_exception():
(last_type, last_value, last_traceback) = sys.exc_info()
return traceback.format_exception_only(last_type, last_value)
def parse(text):
parser = makeGrammar("foo = 'a'", {})
try:
return parser(text).foo()
except ParseError:
return format_exception()
def divide(x, y):
try:
return x / y
except ZeroDivisionError:
return format_exception()
def test():
print(divide(6, 2))
print(divide(6, 0))
print(parse('a'))
print(parse('b'))
test()
Running the test code with Python 3.5 gives the following results:
- The quotient is printed.
- The ZeroDivisionError is formatted.
- The parser recognizes 'a'.
- The parser raises a ParseError when parsing 'b', but
traceback.format_exception_only
fails to format the error.
$ python foo.py
3.0
['ZeroDivisionError: division by zero\n']
a
Traceback (most recent call last):
File "foo.py", line 28, in <module>
test()
File "foo.py", line 26, in test
print(parse('b'))
File "foo.py", line 14, in parse
return format_exception()
File "foo.py", line 7, in format_exception
return traceback.format_exception_only(last_type, last_value)
File "/usr/lib/python3.5/traceback.py", line 136, in format_exception_only
return list(TracebackException(etype, value, None).format_exception_only())
File "/usr/lib/python3.5/traceback.py", line 439, in __init__
_seen.add(exc_value)
TypeError: unhashable type: 'ParseError'
Workaround
The following code monkey patches PyError to add a __hash__
method.
def parse_error_hash(self):
"""Define missing ParseError.__hash__()."""
return hash((self.position, self.formatReason()))
ParseError.__hash__ = parse_error_hash
Rerunning the test code with the monkey patch gives successful results.
o$ python foo.py
3.0
['ZeroDivisionError: division by zero\n']
a
["ometa.runtime.ParseError: \nb\n^\nParse error at line 1, column 0: expected the character 'a'. trail: [foo]\n\n"]
Activity