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
Integration between pytest-trio and hypothesis #1257
Comments
We would really, really like to support async test runners. This would obviously include Trio (this issue), as well as Twisted (#703) and asyncio (#968)! The main reason we don't support this yet is that none of the Hypothesis core team uses any of these frameworks, and we really don't want to commit to an API that turns out to be horrible to use or only narrowly compatible. If you're offering to help out though, I think between us we could find a solution 😄
hypothesis/hypothesis-python/src/hypothesis/extra/pytestplugin.py Lines 152 to 157 in aa4c860
TLDR - all we need to provide is a way to set the Hypothesis |
I'm actually having trouble thinking of any ways that "deep" integration would be better than "shallow" :-). I guess in Trio's case threading the
I've had mixed experiences with This is a pretty edge-casey kind of thing to worry about; we could probably get away with ignoring this problem and most people wouldn't notice. But I hear that you like to worry about edge cases around here :-).
"Is the function you're wrapping async" is just a question about python built-in types, right? Not sure what's library-specific about it?
Agreed that this is the main issue. The first API that comes to to mind is to use some thread-local to pass this through – probably this would mean hypothesis exposing some kind of def run_async_test(test_fn):
if getattr(test_fn.is_hypothesis_test, False):
with hypothesis.test_runner(real_async_test_runner):
return test_fn()
else:
return real_async_test_runner(test_fn) What do you think? Do we care about the case where someone uses a library like pytest-trio and sets an explicit |
To me, a shallow integration is one that hides async-ness from Hypothesis internals, while a deep integration actually handles async stuff. Shallow is much easier to support in a Python 2.7-compatible way, since we're stuck with that through to 2020. On the other hand, deep integration might (I don't know!) be important to support magic clocks or interleave execution if they're not supported (in asyncio, Twisted, etc). Performance aside, it's usually a bad idea to share a fixture between examples, especially with mutable state - that tends to violate all the assumptions shrinking is premised on!
API idea, just to get a sense of the constraints: would replacing # Obviously a super sketchy version, but I hope you get the idea...
def given_(*args, **kwargs):
def inner(func):
return given(*args, **kwargs)(partial(trio.run, func))
return inner |
Yeah. There is a weird quirk about how pytest-trio works: for regular fixtures, they're instantiated outside of the test and would be kept the same through all of hypothesis's examples because, well, that's just the standard pytest behavior. But for async fixtures, we have to play a game to get them inside the call to I guess #377 is to enable the async-style delayed evaluation for all fixtures in hypothesis, so maybe I shouldn't worry about this – pytest-trio is just ahead in the game, and hopefully pytest-hypothesis will catch up ;-).
Okay, here's an idea, that might kill two birds with one stone: what if hypothesis-wrapped tests exposed the underlying function as an attribute, like: It feels a little janky and fragile to have these different libraries poking at each others attributes like this. I think a certain amount of jankiness is unavoidable though given the problem...
There are a few problems with that specific code, but yeah, the basic idea is that we have a function that takes an async test function, and converts it into a synchronous test function, so we "just" need to somehow wedge it into the appropriate place inside
Hmm, good point. And if we did go with mutating I guess another problem with mutating Maybe we want a @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
if 'trio' in item.keywords:
if getattr(item.obj.is_hypothesis_test, False):
if not iscoroutinefunction(item.obj.hypothesis_wrapped_test):
pytest.fail("expected an async test")
item.obj.hypothesis_add_runner(trio_test_runner)
else:
# regular non-hypothesis branch
... |
Also, maybe opening a can of worms: is What about... stateful tests? |
Sounds good to me! Keep Trio in the future where it belongs 😄
Yep, that's pretty much what I was thinking of! Psudeocode: # inside pytest_runtest_call
if not iscoroutinefunction(item.obj.hypothesis_wrapped_test):
pytest.fail("expected an async test")
rewrapper = item.obj.hypothesis_rewrapper # todo: better name
# rewrapper == given(*args, **kwargs) # ie you wrap it around a newly-sync test
item.obj = rewrapper(_some_trio_magic(item.obj.hypothesis_wrapped_test)) # wedge into place! Hopefully, replacing the object instead of mutating it will avoid all the problems associated with reading state of the wrong layer, which would be very very difficult to debug. (we also like high-visibility errors, including detecting possible misuse)
Um. To a first approximation, not gonna work. However it's probably not too awful to reimplement - there's a pretty direct translation layer between the defined class and |
After talking it over with David, I think we have an even simpler option: allow Trio (and anyone else) to assign to # Note: trio.run doesn't accept keyword args, so @given must be passed positional args
item.obj.hypothesis_wrapped_test = functools.partial(trio.run, item.obj.hypothesis_wrapped_test) So all we need to do internally is ensure that we always get the wrapped test from this attribute, rather than holding onto a direct reference, and it should "just work". |
On Wed, 13 Jun 2018 at 18:22, Zac Hatfield-Dodds ***@***.***> wrote:
After talking it over with David, I think we have an even simpler option:
allow Trio (and anyone else) to assign to f.hypothesis_wrapped_test.
Example code:
# Note: trio.run doesn't accept keyword args, so @given must be passed positional args
item.obj.hypothesis_wrapped_test = functools.partial(trio.run, item.obj.hypothesis_wrapped_test)
Actually I think the keyword args thing is going to cause a lot of
problems. Given passes everything it can to the test function as kwargs.
This will be hard to change
… |
Ah, heck. I think the concept is still fine, but the wrapper will probably be more complicated than just dropping in Ping @njsmith; you probably (hopefully) have a better idea of how hard that will be in Trio than we do and whether this idea would help. |
I don't think kwargs are an issue. Pytest also likes to pass kwargs. It makes things a bit more convoluted, but it's fine. Something like: wrapped_test = lambda **kwargs: trio.run(partial(test_func, **kwargs)) There were two other concerns that I raised above (#1257 (comment)), though not very clearly. One is: if we're mutating The other is: suppose there's another decorator in play. def trivial_wrapper(fn):
@wraps(fn)
def wrapped(*args, **kwargs):
return fn(*args, **kwargs)
@trivial_wrapper
@given(...)
@pytest.mark.trio
async def test_something(...):
... This is pretty arcane, dunno if it would ever happen or why. But let me point out a strange thing that will happen if anyone does write this. First, Now, a fun thing about That's why I suggested that we might want an attribute like: These are pretty weird edge cases. I think for introspection we definitely want to expose |
Ugh misclicked sorry, let me reopen this and then edit my comment above real quick |
|
pytest-trio is a pytest plugin to help with testing trio code; the main feature is you can do:
and it automagically arranges to run the test using
trio.run
, with various bells and whistles to handle async fixtures, and so forth; probably in the future it will also be able to detectasync
tests and add thepytest.mark.trio
automatically. (It's generally sort of similar to pytest-asyncio, in case anyone's more familiar with it.)As you might expect, this turns out to interact badly with hypothesis, since both pytest-trio and hypothesis want to take over the actual running of the test: python-trio/pytest-trio#42
From the analysis in that issue, I think hypothesis almost has enough to make this work already, via the
execute_example
hook. The problem here is that we would want to automatically configure hypothesis to use our customexecute_example
hook without having to create a class etc. If there were some way to pass this in via e.g. a thread-local, then that would be awesome.The other tricky bit will be when we add auto-detection of async functions, we'll need to be able to "see through" hypothesis's wrappers. This might be possible right now via some hacks like walking the
__wrapped__
chain, but alternatively, maybe hypothesis could expose an.is_async_hypothesis_test
attribute or something, so we don't have to?The text was updated successfully, but these errors were encountered: