Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ParseError should be hashable #76

Open
jshprentz opened this issue Mar 9, 2019 · 0 comments
Open

ParseError should be hashable #76

jshprentz opened this issue Mar 9, 2019 · 0 comments

Comments

@jshprentz
Copy link

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"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant