-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
add RaisesGroup & Matcher #13192
base: main
Are you sure you want to change the base?
add RaisesGroup & Matcher #13192
Conversation
src/_pytest/_raises_group.py
Outdated
|
||
|
||
@final | ||
class Matcher(AbstractMatcher[MatchE]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We ought not to use such a generic name easily
There's a general wishlist plan to enable matchers as a whole that act similar to dirty equals
The matcher api that's being used here conflates multiple aspects of that in a manner that's problematic to partition later
Ideally the whole api around matchers is first evolved as a plugin to avoid early api lock in as it's hard to make good matchers
I'll elaborate further when the flu is over
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, I can rename it... maybe something that's closer to RaisesGroup
... RaisesExc
/RaisesException
? or something like ExpectedExc
/ExpectedException
This has been available & in use for >1 year in trio, which has allowed the interface to mature. I don't think pytest needs to repeat that process by putting it in a plugin.
I know you've mentioned matcher objects in #11538 and I'm excited for what that might entail in the future, but I don't think we want to wait for that to add usable exceptiongroup support. If you have concrete ways of improving the RaisesGroup
interface to harmonize with it I'm of course open to suggestions though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been heavily involved in the design and review for this feature as we've developed and shipped it in Trio. I'm excited to get it into Pytest (and ideally soonish), but I shouldn't be the only approving core dev for a substantial feature.
…s on RaisesGroup&RaisesExc. Add warnings to group_contains. Remove group_contains example from getting-started page
@Zac-HD please review the documentation :) It feels a bit silly to add big warning boxes to |
@Tusenka unfortunately pull requests are the wrong place to ask questions like this, unless you're either the author of the PR or a maintainer of the package. I suggest StackOverflow instead. |
src/_pytest/_raises_group.py
Outdated
|
||
|
||
@final | ||
class RaisesExc(AbstractRaises[BaseExcT_co_default]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please bring basic questions like these to StackOverFlow, the python discord, ask an LLM of your choice, or otherwise. This is not relevant to the PR at hand and only wastes the time of maintainers without any benefit to pytest.
src/_pytest/_raises_group.py
Outdated
|
||
|
||
@final | ||
class RaisesExc(AbstractRaises[BaseExcT_co_default]): |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
I think I'm mostly just waiting for a quick pass on docs changes from @Zac-HD (or others), and otherwise this can be merged and I can look at potential followup for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot @jakkdl for the excellent work here!
I left mostly requests for doc changes, and a recommendation regarding pytest.raises_group
, please take a look.
@@ -0,0 +1 @@ | |||
Added :class:`pytest.RaisesGroup` (also export as ``pytest.raises_group``) and :class:`pytest.RaisesExc`, as an equivalent to :func:`pytest.raises` for expecting :exc:`ExceptionGroup`. It includes the ability to specify multiple different expected exceptions, the structure of nested exception groups, and flags for emulating :ref:`except* <except_star>`. See :ref:`assert-matching-exception-groups` and docstrings for more information. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it would be best to not export RaisesGroup
at all, keeping only pytest.raises_group
as the official interface?
pytest.raises_group
harmonizes well with the existing pytest.raises
, plus I don't see any benefit of having two ways of doing the same thing. Another argument for that is that we do not expose the context manager used in pytest.raises
either, so I don't think we should do the same here.
I would make the pytest.raises_group
the first-class citizen in the API, we can keep RaisesGroup
in the docs if we like, but I would make all examples use pytest.raises_group
only.
@The-Compiler might want to comment on that given he does a lot of pytest training.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah only having a single name makes sense, but IMO it behaves so much like a class, and will get used a lot like a class, that the snake_case looks very weird. And surely RaisesGroup
and RaisesExc
should be the same? but raises_exc
is then close-to-identically-named to raises
which means it should be renamed, or they should be harmonized.
I think these examples look weird, but maybe that's just me?
with raises_group(raises_group(ValueError), raises_exc(match="foo")):
...
if raises_group(ValueError).matches(my_exc): # this one is especially bad imo
...
rg = raises_group(ValueError)
assert rg.matches(my_exc), rg.fail_reason
@pytest.mark.xfail(raises=raises_group(ValueError))
def foo():
...
I guess one might frame it as raises_group
is a factory for creating RaisesGroup
.. but then the user will assume there's a distinction and a reason for that distinction.
What I'm afraid might happen is people thinking that they need to do
with raises_group(RaisesGroup(ValueError)):
...
especially if the type hints specify RaisesGroup
... which means that maybe the class itself should be named in snake_case??? Ew. Or people just not considering the possibility that it does a lot more than pytest.raises
has historically supported.
In my very personal opinion the ideal would be having pytest.RaisesGroup
& pytest.RaisesExc
, and pytest.raises
being a thin wrapper around pytest.RaisesExc
to add support for the legacy calling mode. But if I'm in the minority here then /shrug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another argument for that is that we do not expose the context manager used in pytest.raises either, so I don't think we should do the same here.
Wait sorry I only now properly parsed this. I don't think you should pattern-match this beast with pytest.raises
, it's perhaps an overly engineered beast - but RaisesGroup
is way more than just "the context manager" of a function call. It has public attributes in fail_reason
& matches
that are meant to be used - and it even supports modifying any of the parameters passed to __init__
after creation (though that bypasses the verification in __init__
so it is a bit dangerous).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess one might frame it as raises_group is a factory for creating RaisesGroup.. but then the user will assume there's a distinction and a reason for that distinction.
Well at least this is consistent with pytest.raises
, which is also just a factory. I personally find it fine to have pytest.raises_group
a factory, and a separate RaisesExc
as a class, given their usage is different anyway.
As an additional data point, we have pytest.param
which is similar in purpose to RaisesExc
, so perhaps this is another argument for pytest.raises_exc
.
But I get your points, but I'm sure at least some users will wonder why there is pytest.raises
and pytest.RaisesGroup
.
But if we move forward with having pytest.RaisesGroup
I would remove the pytest.raises_group
alias, I don't see much benefit of having the alias in place.
But I'm not deadset on this either, just find the inconsistency a bit unnerving and might something to come up for new users. 👍
As I mentioned before, this is excellent work overall, I'm bringing this up because user facing API is important and is something we have overlooked in the past (regarding consistency I mean).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think RaisesGroup
and RaisesExc
has significant differences in their use, the only difference is that RaisesExc
does not support the context-manager use - but I kind of think it should and that RaisesContext
should be removed. I've refrained from bundling that as yet another part of this PR, but I'm starting to think I should.
pytest.param
is a good data point. And maybe the harmonization ends up with not needing a distinction between raises_exc
and raises
, in which case we get
with pytest.raises_group(pytest.raises(ValueError), pytest.raises_group(TypeError)):
...
I'll give it a go, and if the details on that becomes worthy of extensive review&discussion I'll split it off into another PR.
I'm very happy to get feedback! I've spent an ungodly amount of time on this feature, so I do have very strong personal opinions at this point, but hopefully I can soon let it go into the collective consciousness~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but I kind of think it should and that RaisesContext should be removed.
Do you mean:
pytest/src/_pytest/python_api.py
Line 1030 in b0caf3d
class RaisesContext(AbstractContextManager[_pytest._code.ExceptionInfo[E]]): |
Or is that a typo?
But before moving on, I would recommend to wait a few days for other opinions -- it is possible people will say "I agree with @jakkdl and @nicoddemus is being too cautious, this will not be a problem in practice" and we can just move forward, hehehe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, not a typo. The logic in RaisesContext
is a ~strict subset of the logic in RaisesGroup
, so if I move __enter__
and __exit__
to AbstractMatcher
I can make pytest.raises
return RaisesExc
(and also offload some input validation from pytest.raises
to RaisesExc
).
That also adds support for e.g. pytest.raises(check=lambda e: isinstance(e.__cause__, ValueError))
. #12763
src/_pytest/_raises_group.py
Outdated
with RaisesGroups(RaisesExc(check=lambda x: type(x) is ValueError)): | ||
... | ||
|
||
Tip: if you install ``hypothesis`` and import it in ``conftest.py`` you will get |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we elaborate on this a bit? Someone who does not understand the history behind this feature will have a hard time understanding this tip.
Probably also worth adding this under a .. note::
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gave it a go, and removed it from RaisesExc
to avoid repetition. (it's valid for RaisesExc
as well, but you're unlikely to care if only using RaisesExc
).
add `versionadded` Co-authored-by: Bruno Oliveira <[email protected]>
for more information, see https://pre-commit.ci
Co-authored-by: Bruno Oliveira <[email protected]>
for more information, see https://pre-commit.ci
…ion small things differing between raises and RaisesExc, and some random other stuff...
so, uh, sorry to everybody reviewing this, cause this last commit is a monster. I noticed a ton of small random things (though I probably shouldn't have bothered with backquotes tbh) Anyway, this gets rid of There's also a question of If any of this stuff is controversial I'll break stuff off into a separate PR, and I was hoping to do some of this in a follow-up PR, but it ended up becoming relevant to the discussion so I just did it in here for now. |
Previous PR's: #11656 (ExpectedExceptionGroup), #11671 (closed to give this a fresh PR w/o stale comments)
fixes: #11538 #12504
This adds
RaisesGroup
(also exported asraises_group
) andMatcher
, to allow a robust way of expectingExceptionGroup
and the ability to specify the structure of nested groups, etc etc.This is the exact implementation available in trio.testing.RaisesGroup which has been in use since december 2023 and gone through a couple iterations since to settle on a good interface. The most recent addition was python-trio/trio#3145 which added error messages, where the formatting can probably still be fine-tuned.
Pytest currently has excinfo.group_contains for checking exceptiongroups... which is very problematic. I think it should be deprecated, either in the same release as this PR, or very soon after.
This currently does not touch the implementation of
pytest.raises
, but if we madeMatcher
a contextmanager we could easily supportthis would fix e.g. #12763 with Matcher having the
check
parameter. The only complication is the legacy form of pytest.raises but that should be solvable with a small helper function.This can however be done in a followup PR.
This currently does not add an
.assert_matches
method, which previous iterations of the PR had. The reason to add that would be to improve the error message on failure, though it's possible to get the same result withthough note that
.matches()
is already a fairly niche feature that is entirely separate to the context-manager use (not to be confused with thematch
parameter).TODO: