calling/raises/pattern doesn't allow us to match multiple messages with nested exceptions
thehcma opened this issue · 5 comments
This is more of a feature request rather than a bug.
With Python 3, we can have nested exceptions (e.g., raise RuntimeError("blah") from e, where e is another exception that was just caught).
The full exception message can be retrieve, e.g., using:
def stack_trace_as_string(exception):
"""Returns the full stack trace as a string"""
return "".join(traceback.format_exception(etype=type(exception), value=exception, tb=exception.__traceback__))
This will then yield something like (stealing the example from here: https://stackoverflow.com/questions/16414744/python-exception-chaining):
Traceback (most recent call last):
File "t.py", line 2, in <module>
v = {}['a']
KeyError: 'a'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "t.py", line 4, in <module>
raise ValueError('failed') from e
ValueError: failed
As you can see we have 2 messages (from the two exceptions that were raised in a nested fashion).
So using the function I gave above, pyhamcrest
could potentially be extend to match/search for both messages.
Seems reasonable. What do you think an assertion for a nested exception might look like?
My inclination would be to have something like the following (assuming we want to keep close in spirit to what we already have):
assert_that(calling(foo).with_args(bar1, bar2...), raises([exception1, exception2, ...], patterns=[exception1_message, exception2_message]))
The semantics would be that the expected nesting (exception_type, exception_message) would be captured by the two parallel lists - in other words, the nesting stack trace should match these lists in the same order.
Alternatively, something like this would also work (it's a bit of departure to the current syntax, but, imo, a bit safer, since we don't have to keep parallel lists):
assert_that(calling(foo).with_args(bar1, bar2...), raises([(exception1, optional_exception1_message_pattern), (exception2, _optional_exception2_message_pattern]))
Naturally, we could have variations in the sense that we would not need to specify the whole trace - something along the lines of specifying an incomplete stack trace - that is, we could specify the initial frames (e.g., say we have a 5-level nested trace, we could specify it starts with these 2 exceptions and their patterns), the final frames (e.g., say that we expect these last 2 frames), and containment frames (e.g., the following sequence should be present anywhere in the nested frames). This is all extra-credit, but the first proposal, where we specify the whole expected set of frames would already go a long way.
PR #129 is now merged. Is it possible to use that style to match the inner exception that way?
I believe it could be used.
However, imo, it's suboptimal in the general case, because it doesn't really provide a way to make the nesting/structure associated with a chain of exceptions explicit.
In other words, the nesting includes several exception instances with their associated properties, so one would have to craft a regex to match that instead of simply enumerating what's being sought. In this case, this is akin to comparing two collections directly vs expanding a collection and using a regex to assess their equality. Yes, it works, but it ain't ideal (imho).
Let me clarify a bit and add a disclaimer - i.e., that I fully understood the #129 PR fix correctly.
First, I am changing my opinion, I don't think this change can be used for the purposes of this feature request. :)
While it seems possible that with this change we would be able to inspect the attributes of the outer exception (and, hence, the __cause__
attribute), I can't figure out a way to traverse the causal hierarchy (i.e., assume it's deeper than a single level). Perhaps, there is a way, but I really can't see it.
The change we need in the original feature request is something that would traverse the __cause__
attribute hierarchy in the exception instance and, hence, allow us to assert both the exception type as well as its properties, at each level.