-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
pytest.raises could be improved to match exception fields instead / in addition to regex #3362
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
Comments
GitMate.io thinks possibly related issues are #467 (Cache fixtures which raise pytest.skip.Exception and pytest.fail.Exception), #767 (pytest.raises() doesn't always return Exception instance in py26), #692 (xfail decorator ignores "raises" parameter when given as single exception (instead of list)), #475 (Misleading pytest exception description), and #150 (improve pylint / pytest compatibility). |
Hi @wvxvw, thanks for writing. Could you please provide an example of how a test using this feature would look like? |
I actually did something similar to what I'm suggesting: import py
import sys
import logging
from _pytest.python_api import RaisesContext
from _pytest.compat import isclass
from _pytest.outcomes import fail
import _pytest._code
def raises(expected_exception, *args, **kwargs):
'''
See ``pytest.raises()`` for docs and examples.
'''
__tracebackhide__ = True
msg = ('exceptions must be old-style classes or'
' derived from BaseException, not %s')
if isinstance(expected_exception, tuple):
for exc in expected_exception:
if not isclass(exc):
raise TypeError(msg % type(exc))
elif not isclass(expected_exception):
raise TypeError(msg % type(expected_exception))
message = 'DID NOT RAISE {0}'.format(expected_exception)
match_expr = None
if not args:
if 'message' in kwargs:
message = kwargs.pop('message')
if 'match' in kwargs:
match_expr = kwargs.pop('match')
return MatcherRaisesContext(
expected_exception,
message,
match_expr,
kwargs,
)
elif isinstance(args[0], str):
code, = args
assert isinstance(code, str)
frame = sys._getframe(1)
loc = frame.f_locals.copy()
loc.update(kwargs)
# print 'raises frame scope: %r' % frame.f_locals
try:
code = _pytest._code.Source(code).compile()
py.builtin.exec_(code, frame.f_globals, loc)
# XXX didn'T mean f_globals == f_locals something special?
# this is destroyed here ...
except expected_exception:
return _pytest._code.ExceptionInfo()
else:
func = args[0]
try:
func(*args[1:], **kwargs)
except expected_exception:
return _pytest._code.ExceptionInfo()
fail(message)
class MatcherRaisesContext(RaisesContext):
def __init__(self, expected_exception, message, match_expr, extras):
super().__init__(expected_exception, message, match_expr)
self.extras = extras
def __exit__(self, *tp):
suppress = super().__exit__(*tp)
if suppress:
for k, v in self.extras.items():
actual = getattr(tp[1], k)
logging.debug('actual: {} == expected: {}'.format(actual, v))
assert actual == v, 'Expected {}.{} == {}, found {}'.format(
type(tp[1]).__name__, k, v, actual,
)
return suppress The test then looks like this: def test_upload_csv_failure_org(client, user_data, caplog):
caplog.set_level(logging.DEBUG)
org_data = json.dumps(... some proprietary stuff ...)
with raises(DecodedError, status=403):
client.upload_csv(user_data, org_data) |
i propose having a because this proposal just ensures that all random keyword arguments suddenly have a meaning, which in turn would a) give misspelligns a really strange behaviour and b) would ensure we no longer have a right for new argument names in addition it would create a general matcher vs speical arugments difference which in terms of api is just not acceptable |
Definitely agree, 👎 on letting any arbitrary keyword argument name become an attribute for checking. The def test_upload_csv_failure_org(client, user_data, caplog):
org_data = json.dumps(... some proprietary stuff ...)
with raises(DecodedError, check=lambda e: status==403):
client.upload_csv(user_data, org_data) over: def test_upload_csv_failure_org(client, user_data, caplog):
org_data = json.dumps(... some proprietary stuff ...)
with raises(DecodedError) as e:
client.upload_csv(user_data, org_data)
assert e.status==403 BUT I had the same argument over |
The thing about the last variant, with Another thing about it, is that |
having the match object be something more general might be interesting the main question is how to nicely express/integrate it |
@RonnyPfannschmidt absolutely agree, that's why in my initial proposal I wrote about a single callable object which fulfills that purpose. I would argue for having a parameter that specifies an implementation for |
@wvxvw Ouch sorry, I definitely meant
I think it is unfair to state that one has to be "100% familiar with the source of of raises()", after all inspecting the
Agree, indeed usually you use the object returned by a context-manager inside the
Indeed about the I'm just mentioning the above points to emphasize that one does not need to know the actual source code of I'm not against the general idea per-se, but I guess we all agree with @RonnyPfannschmidt here:
So it is more of a question of finding a proper API for checking attributes of the exception in a convenient manner.
But if the user went to all the trouble of implementing their own context manager, isn't it more explicit and better to just use that directly? I see little value in doing this: with pytest.raises(RuntimeError, manager=raises_with_attr_checking(status=404)):
... Over: with raises_with_attr_checking(RuntimeError, status=404)):
... |
I'm -1 on adding special facilities for ad-hoc matching. I think it would have been better to not have |
I consider Match a good addition as it works well for all exceptions Special checking should use custom context managers that may build on raises |
Heh! FTR, I've since changed my mind on that, because it is a very common case to check the exception message. I've also get feedback that being so "handy" some colleagues have started checking messages more often than before. So all in all I was wrong on thinking this would not be a good addition. But I agree with your other points about automatically checking attributes. I still think my suggestion in #3362 (comment) is valid, to use a custom context manager instead. So perhaps we should close this as "won't do"? Btw even if we close this, we definitely appreciate the suggestion and discussion that it generated @wvxvw! |
Suppose you have an exception which carries along information like status + message, or errno + message. Something as trivial as system call or an HTTP request. Trying to match on the message may not be very precise, if, say, HTTP server sends the same message with all 500+ status responses.
Thus, I suggest extending
python_api.RaisesContext
withmatcher
key, which would take user function, which, in turn, should expect an exception and returnTrue/False
to be later used assuppress_exception
.The text was updated successfully, but these errors were encountered: