Skip to content

ParseError should be hashable #76

Open
@jshprentz

Description

@jshprentz

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions