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

Correctly typing draw argument in composite strategies #3005

Closed
glowthrower opened this issue Jun 3, 2021 · 7 comments
Closed

Correctly typing draw argument in composite strategies #3005

glowthrower opened this issue Jun 3, 2021 · 7 comments
Labels
interop how to play nicely with other packages legibility make errors helpful and Hypothesis grokable question not sure it's a bug? questions welcome

Comments

@glowthrower
Copy link

glowthrower commented Jun 3, 2021

I think this is probably a trivially answered question, but I wasn't able to find an answer in the documentation and poking around in the source code didn't help either.

I'm a big fan of type-hinting, and I also use composite strategies aggressively to add narrative to my property-based testing. It makes sense to put the two together. So consider the following strategy, which is totally useless but illustrative :

from typing import Tuple

import hypothesis.strategies as hp_st


@hp_st.composite
def foo(draw) -> hp_st.SearchStrategy[Tuple[int, int]]:
    return draw(
        hp_st.tuples(
            hp_st.integers(),
            hp_st.integers(),
        )
    )

My reading of the documentation suggests that this is the correct way to type the return value of the decorated function. And indeed, using mypy==0.812 with hypothesis==6.13.11, I see:

> mypy ./foo.py
Success: no issues found in 1 source file

But wait! All is not well. If we change (for example), one of the int annotations in the return type to, say, str, and rerun mypy, it still reports no issues. That doesn't seem right. In fact, it turns out that the permissive defaults of mypy are providing a false sense of security here -- as can be seen by running (with the original code given above):

> mypy --warn-return-any ./foo.py
foo.py:8: error: Returning Any from function declared to return "SearchStrategy[Tuple[int, int]]"
Found 1 error in 1 file (checked 1 source file)

The issue is, of course, that the draw argument in the strategy is untyped, so mypy can't infer the type of draw(...).

So here's my question: how should I then type draw? I've tried messing around with type variables etc., and so far I haven't come up with something that seems nice. Are there any recommendations on how to make this work, or indeed a recommendation that this CAN'T work?

@Zac-HD
Copy link
Member

Zac-HD commented Jun 3, 2021

draw is of type Callable[[SearchStrategy[Ex]], Ex], where Ex is a TypeVar.

Happily, PEP-612 means that we'll be able to annotate this correctly from Python 3.10 onwards. Until then, you might still have some trouble with type-checkers like mypy wondering where the draw argument comes from...

@Zac-HD Zac-HD added interop how to play nicely with other packages legibility make errors helpful and Hypothesis grokable question not sure it's a bug? questions welcome labels Jun 3, 2021
@Zac-HD Zac-HD closed this as completed Jun 3, 2021
@glowthrower
Copy link
Author

glowthrower commented Jun 4, 2021

Thanks for the quick response.

I had indeed tried something Callable, and your suggestion was enough to remind me that I'd had to solve a similar issue elsewhere. For the benefit of anyone who runs across this via a search engine, the typing of draw can be done as:

from typing import Callable, Tuple, TypeVar

import hypothesis.strategies as hp_st

T = TypeVar('T')
Draw = TypeVar('Draw', bound=Callable[[hp_st.SearchStrategy[T]], T])


@hp_st.composite
def foo(draw: Draw) -> hp_st.SearchStrategy[Tuple[int, int]]:
    return draw(
        hp_st.tuples(
            hp_st.integers(),
            hp_st.integers(),
        )
    )

Unfortunately, that still isn't enough to satisfy mypy, which now complains

foo.py:12: error: Argument 1 has incompatible type "SearchStrategy[Tuple[Any, ...]]"; expected "SearchStrategy[SearchStrategy[Tuple[int, int]]]"
Found 1 error in 1 file (checked 1 source file)

In other words, the type(s) of the hp_st.integers() strategies don't percolate through the call to hp_st.tuples(...). I think this is probably unfixable at the moment (EDIT: without explicit casts, which I would rather not use), although if there's a really obvious solution I'd love to hear it.

Otherwise, thanks again. 😄

@Zac-HD
Copy link
Member

Zac-HD commented Jun 4, 2021

Ah... you've found the same problem as Guido van Rossum! Specifically, we'll need the not-yet-drafted follow-up to PEP-646 to support this "properly".

But, we can just add some overloads for lengths 0-5 like we already have for one_of() and that'll handle your usecase here. I'm working on the PR now and it should be released in a day or two 😁

@glowthrower
Copy link
Author

Haha awesome, I feel better to know that I wasn't making some obvious error here.

And I look forward to trying out whatever you come up with. 😄 Thanks!

@Zac-HD
Copy link
Member

Zac-HD commented Jun 4, 2021

Now released as version 6.13.14 🎉

pip install -U hypothesis and try it out!

@glowthrower
Copy link
Author

Works for me! Nice one, I appreciate it.

@cleder
Copy link

cleder commented Mar 21, 2024

Just for the record (if someone stumbles across this) use hypothesis.strategies.DrawFn to annotate draw

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interop how to play nicely with other packages legibility make errors helpful and Hypothesis grokable question not sure it's a bug? questions welcome
Projects
None yet
Development

No branches or pull requests

3 participants