Repository: HypothesisWorks/hypothesis

Issue:
Strategy type annotations - explicit Optional
A lot of the strategies in [core](https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/src/hypothesis/strategies/_internal/core.py) take parameters that can be `None`-valued but aren't typed as `Optional`. For example,

``` python
def integers(min_value: int = None, max_value: int = None) -> SearchStrategy[int]:
```

which can cause issues if mypy is run with `--no-implicit-optional`. The only strategy that has (partially) typed `Optional` in core is `complex_numbers`:

``` python
def complex_numbers(
    *,
    min_magnitude: Optional[Real] = 0,
    max_magnitude: Optional[Real] = None,
    allow_infinity: bool = None,
    allow_nan: bool = None
) -> SearchStrategy[complex]:
```

Would you like to have a PR updating the rest of the strategies with explicit `Optional` types or do you prefer relying on implicit optional (in which case perhaps remove it from `complex_numbers` for consistency)?


Discussion:
Hey David - thanks for pointing this out!  My personal preference is for strict, implicit optional: it's exactly equivalent for mypy and also `typing.get_type_hints` at runtime, and avoids some redundancy.

If it's causing issues downstream though (@Stranger6667 might chime in?), I'd accept a `--no-implicit-optional` PR and live with the aesthetics.  Otherwise, a PR `st.complex_numbers()` would also be welcome!
@Zac-HD 
Hi! There are no issues caused by this in Schemathesis (at least from what I can see) :) I just looked at the issue and added :+1: because of my personal preferences :) I actually thought, that with `--no-implicit-optional` mypy won't allow `None` arguments if they aren't declared as `Optional` which could prevent some bugs, but I am not sure if it actually behaves like this
> I actually thought, that with `--no-implicit-optional` mypy won't allow `None` arguments if they aren't declared as `Optional` which could prevent some bugs

You're correct, but IMO we have good enough code review that on balance I prefer the more concise form.  Thanks for the comment!

@davidmascharka I would therefore prefer to change `st.complex_numbers()` :slightly_smiling_face: 

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/internal/validation.py b/hypothesis-python/src/hypothesis/internal/validation.py
--- a/hypothesis-python/src/hypothesis/internal/validation.py
+++ b/hypothesis-python/src/hypothesis/internal/validation.py
@@ -72,6 +72,14 @@ def check_valid_magnitude(value, name):
     check_valid_bound(value, name)
     if value is not None and value < 0:
         raise InvalidArgument("%s=%r must not be negative." % (name, value))
+    if value is None and name == "min_magnitude":
+        from hypothesis._settings import note_deprecation
+
+        note_deprecation(
+            "min_magnitude=None is deprecated; use min_magnitude=0 "
+            "or omit the argument entirely.",
+            since="RELEASEDAY",
+        )
 
 
 @check_function
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
@@ -1768,8 +1768,8 @@ def accept(*args, **kwargs):
 @deprecated_posargs
 def complex_numbers(
     *,
-    min_magnitude: Optional[Real] = 0,
-    max_magnitude: Optional[Real] = None,
+    min_magnitude: Real = 0,
+    max_magnitude: Real = None,
     allow_infinity: bool = None,
     allow_nan: bool = None
 ) -> SearchStrategy[complex]:
@@ -1777,10 +1777,10 @@ def complex_numbers(
 
     This strategy draws complex numbers with constrained magnitudes.
     The ``min_magnitude`` and ``max_magnitude`` parameters should be
-    non-negative :class:`~python:numbers.Real` numbers; values
-    of ``None`` correspond to zero and infinite values respectively.
+    non-negative :class:`~python:numbers.Real` numbers; a value
+    of ``None`` corresponds an infinite upper bound.
 
-    If ``min_magnitude`` is positive or ``max_magnitude`` is finite, it
+    If ``min_magnitude`` is nonzero or ``max_magnitude`` is finite, it
     is an error to enable ``allow_nan``.  If ``max_magnitude`` is finite,
     it is an error to enable ``allow_infinity``.
 
@@ -1799,10 +1799,10 @@ def complex_numbers(
     check_valid_magnitude(min_magnitude, "min_magnitude")
     check_valid_magnitude(max_magnitude, "max_magnitude")
     check_valid_interval(min_magnitude, max_magnitude, "min_magnitude", "max_magnitude")
+    if min_magnitude is None:
+        min_magnitude = 0
     if max_magnitude == math.inf:
         max_magnitude = None
-    if min_magnitude == 0:
-        min_magnitude = None
 
     if allow_infinity is None:
         allow_infinity = bool(max_magnitude is None)
@@ -1812,15 +1812,15 @@ def complex_numbers(
             % (allow_infinity, max_magnitude)
         )
     if allow_nan is None:
-        allow_nan = bool(min_magnitude is None and max_magnitude is None)
-    elif allow_nan and not (min_magnitude is None and max_magnitude is None):
+        allow_nan = bool(min_magnitude == 0 and max_magnitude is None)
+    elif allow_nan and not (min_magnitude == 0 and max_magnitude is None):
         raise InvalidArgument(
             "Cannot have allow_nan=%r, min_magnitude=%r max_magnitude=%r"
             % (allow_nan, min_magnitude, max_magnitude)
         )
     allow_kw = {"allow_nan": allow_nan, "allow_infinity": allow_infinity}
 
-    if min_magnitude is None and max_magnitude is None:
+    if min_magnitude == 0 and max_magnitude is None:
         # In this simple but common case, there are no constraints on the
         # magnitude and therefore no relationship between the real and
         # imaginary parts.
@@ -1837,18 +1837,14 @@ def constrained_complex(draw):
             zi = draw(floats(-max_magnitude, max_magnitude, **allow_kw))
             rmax = cathetus(max_magnitude, zi)
         # Draw the real part from the allowed range given the imaginary part
-        if min_magnitude is None or math.fabs(zi) >= min_magnitude:
+        if min_magnitude == 0 or math.fabs(zi) >= min_magnitude:
             zr = draw(floats(None if rmax is None else -rmax, rmax, **allow_kw))
         else:
             zr = draw(floats(cathetus(min_magnitude, zi), rmax, **allow_kw))
         # Order of conditions carefully tuned so that for a given pair of
         # magnitude arguments, we always either draw or do not draw the bool
         # (crucial for good shrinking behaviour) but only invert when needed.
-        if (
-            min_magnitude is not None
-            and draw(booleans())
-            and math.fabs(zi) <= min_magnitude
-        ):
+        if min_magnitude > 0 and draw(booleans()) and math.fabs(zi) <= min_magnitude:
             zr = -zr
         return complex(zr, zi)
 


Test patch:
diff --git a/hypothesis-python/tests/cover/test_complex_numbers.py b/hypothesis-python/tests/cover/test_complex_numbers.py
--- a/hypothesis-python/tests/cover/test_complex_numbers.py
+++ b/hypothesis-python/tests/cover/test_complex_numbers.py
@@ -20,6 +20,7 @@
 from hypothesis import given, reject
 from hypothesis.strategies import complex_numbers
 from tests.common.debug import minimal
+from tests.common.utils import checks_deprecated_behaviour
 
 
 def test_minimal():
@@ -75,6 +76,7 @@ def test_minimal_min_magnitude_zero():
     assert minimal(complex_numbers(min_magnitude=0), lambda x: True) == 0
 
 
+@checks_deprecated_behaviour
 def test_minimal_min_magnitude_none():
     assert minimal(complex_numbers(min_magnitude=None), lambda x: True) == 0