Repository: HypothesisWorks/hypothesis

Issue:
Error with `floats(max_value=<one less than inf>, allow_infinity=False, exclude_max=True)` could be more helpful
This has already been mentioned in https://github.com/HypothesisWorks/hypothesis/issues/1859#issuecomment-471589345, but I couldn't find a standalone issue for it.

The error `allow_infinity=False excludes min_value=inf` (added in https://github.com/HypothesisWorks/hypothesis/pull/1860 I believe) is a bit confusing to see when you are certain you _didn't_ pass `max_value=inf`. It took me far longer than I'd like to realize that the highest-possible finite float was getting passed in as `max_value`, leaving no room for a larger value besides infinity. If worded differently, this might have saved me some time.

I assume this is happening because the `min_value` gets `next_up`'d in: https://github.com/HypothesisWorks/hypothesis/blob/22607fe63a93b6fc70453502cc1d07831e124f6b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py#L370-L373
so when the check actually happens, it appears the user has passed in `inf`.

Even a message like `allow_infinity=False exclude_max=True excludes min_value={sys.float_info.max}` would have made the problem much clearer, though I think it could be worded in a more friendly way for users who are less familiar with how floats work.
Improve error message when `st.builds()` is missing required uninferrable arguments
Right now `hypothesis` ignores `@no_type_check` decorator applied to functions and types.

For example:

```python
from hypothesis import strategies as st, given

class NotTypes:
    def __init__(self, arg: str) -> None:
        self.arg = arg

@given(st.builds(NotTypes))
def test_no_type_check(instance):
    assert isinstance(instance, NotTypes)
```

This passes as expected. But, consider this:

```python
from hypothesis import strategies as st, given
from typing import no_type_check

@no_type_check
class NotTypes:
    def __init__(self, arg: str) -> None:
        self.arg = arg

@given(st.builds(NotTypes))
def test_no_type_check(instance):
    assert isinstance(instance, NotTypes)
```

This example fails with unclear error:

```
_________________________________ test_no_type_check _________________________________

    @given(st.builds(NotTypes))
>   def test_no_type_check(instance):

ex.py:10: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/engine.py:470: in run
    self._run()
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/engine.py:870: in _run
    self.reuse_existing_examples()
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/engine.py:548: in reuse_existing_examples
    data = self.cached_test_function(existing)
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/engine.py:1051: in cached_test_function
    self.test_function(data)
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/engine.py:208: in test_function
    self.__stoppable_test_function(data)
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/engine.py:184: in __stoppable_test_function
    self._test_function(data)
.venv/lib/python3.9/site-packages/hypothesis/core.py:725: in _execute_once_for_engine
    escalate_hypothesis_internal_error()
.venv/lib/python3.9/site-packages/hypothesis/core.py:699: in _execute_once_for_engine
    result = self.execute_once(data)
.venv/lib/python3.9/site-packages/hypothesis/core.py:637: in execute_once
    result = self.test_runner(data, run)
.venv/lib/python3.9/site-packages/hypothesis/executors.py:47: in default_new_style_executor
    return function(data)
.venv/lib/python3.9/site-packages/hypothesis/core.py:596: in run
    args, kwargs = data.draw(self.search_strategy)
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/data.py:878: in draw
    return strategy.do_draw(self)
.venv/lib/python3.9/site-packages/hypothesis/strategies/_internal/collections.py:58: in do_draw
    return tuple(data.draw(e) for e in self.element_strategies)
.venv/lib/python3.9/site-packages/hypothesis/strategies/_internal/collections.py:58: in <genexpr>
    return tuple(data.draw(e) for e in self.element_strategies)
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/data.py:874: in draw
    return strategy.do_draw(self)
.venv/lib/python3.9/site-packages/hypothesis/strategies/_internal/strategies.py:818: in do_draw
    result = self.pack(data.draw(self.mapped_strategy))
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/data.py:874: in draw
    return strategy.do_draw(self)
.venv/lib/python3.9/site-packages/hypothesis/strategies/_internal/lazy.py:164: in do_draw
    return data.draw(self.wrapped_strategy)
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/data.py:874: in draw
    return strategy.do_draw(self)
.venv/lib/python3.9/site-packages/hypothesis/strategies/_internal/strategies.py:818: in do_draw
    result = self.pack(data.draw(self.mapped_strategy))
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/data.py:874: in draw
    return strategy.do_draw(self)
.venv/lib/python3.9/site-packages/hypothesis/strategies/_internal/collections.py:58: in do_draw
    return tuple(data.draw(e) for e in self.element_strategies)
.venv/lib/python3.9/site-packages/hypothesis/strategies/_internal/collections.py:58: in <genexpr>
    return tuple(data.draw(e) for e in self.element_strategies)
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/data.py:874: in draw
    return strategy.do_draw(self)
.venv/lib/python3.9/site-packages/hypothesis/strategies/_internal/lazy.py:164: in do_draw
    return data.draw(self.wrapped_strategy)
.venv/lib/python3.9/site-packages/hypothesis/internal/conjecture/data.py:874: in draw
    return strategy.do_draw(self)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = builds(NotTypes), data = ConjectureData(VALID, 0 bytes, frozen)

    def do_draw(self, data):
        try:
>           return self.target(
                *(data.draw(a) for a in self.args),
                **{k: data.draw(v) for k, v in self.kwargs.items()},
            )
E           TypeError: __init__() missing 1 required positional argument: 'arg'

.venv/lib/python3.9/site-packages/hypothesis/strategies/_internal/core.py:777: TypeError
```

I think it happens because `get_type_hints` return `{}` for types like `NotTypes`.
That's why we can't create a correct amount of arguments for `__init__`.

Probably, we can at least improve the error message to soething like:

```
Auto resolving types marked with `@no_type_check` is not supported. Explicit `register_type_strategy()` is required.
```

What do you think?

Context: https://bugs.python.org/issue46571


Discussion:

This isn't specific to `@no_type_check`; it's a generic error message for any case where `st.builds()` is missing a strategy for a required unannotated argument.

I think it makes sense here to catch `TypeError`, check if our `args` and `kwargs` *can't* satisfy the required args for the target, and if so raise a `TypeError(...) from None` with a much more specific error message.  That message might or might not have a variant for "you used `@no_type_check`" :slightly_smiling_face: 

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
@@ -798,6 +798,13 @@ def do_draw(self, data):
                         f"error - try using from_type({self.target!r}) instead "
                         f"of builds({self.target!r})"
                     ) from err
+            if getattr(self.target, "__no_type_check__", None) is True:
+                # Note: could use PEP-678 __notes__ here.  Migrate over once we're
+                # using an `exceptiongroup` backport with support for that.
+                raise TypeError(
+                    "This might be because the @no_type_check decorator prevented "
+                    "Hypothesis from inferring a strategy for some required arguments."
+                ) from err
             raise
 
     def validate(self):
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
@@ -436,9 +436,19 @@ def floats(
                 "with both min_value and max_value"
             )
     elif min_value == math.inf:
-        raise InvalidArgument("allow_infinity=False excludes min_value=inf")
+        if min_arg == math.inf:
+            raise InvalidArgument("allow_infinity=False excludes min_value=inf")
+        raise InvalidArgument(
+            f"exclude_min=True turns min_value={min_arg!r} into inf, "
+            "but allow_infinity=False"
+        )
     elif max_value == -math.inf:
-        raise InvalidArgument("allow_infinity=False excludes max_value=-inf")
+        if max_arg == -math.inf:
+            raise InvalidArgument("allow_infinity=False excludes max_value=-inf")
+        raise InvalidArgument(
+            f"exclude_max=True turns max_value={max_arg!r} into -inf, "
+            "but allow_infinity=False"
+        )
 
     smallest_normal = width_smallest_normals[width]
     if allow_subnormal is None:


Test patch:
diff --git a/hypothesis-python/tests/cover/test_lookup.py b/hypothesis-python/tests/cover/test_lookup.py
--- a/hypothesis-python/tests/cover/test_lookup.py
+++ b/hypothesis-python/tests/cover/test_lookup.py
@@ -886,3 +886,13 @@ def test_builds_suggests_from_type(type_):
         raise AssertionError("Expected strategy to raise an error")
     except TypeError as err:
         assert not isinstance(err, InvalidArgument)
+
+
+def test_builds_mentions_no_type_check():
+    @typing.no_type_check
+    def f(x: int):
+        pass
+
+    msg = "@no_type_check decorator prevented Hypothesis from inferring a strategy"
+    with pytest.raises(TypeError, match=msg):
+        st.builds(f).example()
diff --git a/hypothesis-python/tests/cover/test_numerics.py b/hypothesis-python/tests/cover/test_numerics.py
--- a/hypothesis-python/tests/cover/test_numerics.py
+++ b/hypothesis-python/tests/cover/test_numerics.py
@@ -172,3 +172,30 @@ def test_consistent_decimal_error():
         with decimal.localcontext(decimal.Context(traps=[])):
             decimals(bad).example()
     assert str(excinfo.value) == str(excinfo2.value)
+
+
+@pytest.mark.parametrize(
+    "s, msg",
+    [
+        (
+            floats(min_value=inf, allow_infinity=False),
+            "allow_infinity=False excludes min_value=inf",
+        ),
+        (
+            floats(min_value=next_down(inf), exclude_min=True, allow_infinity=False),
+            "exclude_min=True turns min_value=.+? into inf, but allow_infinity=False",
+        ),
+        (
+            floats(max_value=-inf, allow_infinity=False),
+            "allow_infinity=False excludes max_value=-inf",
+        ),
+        (
+            floats(max_value=next_up(-inf), exclude_max=True, allow_infinity=False),
+            "exclude_max=True turns max_value=.+? into -inf, but allow_infinity=False",
+        ),
+    ],
+)
+def test_floats_message(s, msg):
+    # https://github.com/HypothesisWorks/hypothesis/issues/3207
+    with pytest.raises(InvalidArgument, match=msg):
+        s.validate()