Repository: HypothesisWorks/hypothesis

Issue:
min_size arguments should default to zero instead of None
We default arguments to `None` where we don't have a known bound (e.g. `integers()`), or where a non-`None` default argument would require us to weaken our checks for consistency (e.g. if `min_value` defaulted to `-inf` for `floats()`, we couldn't warn a user who passed `min_value=-inf, allow_infinity=False`).  For strategies such as `dates()` we don't accept `None` at all!

It turns out that neither of these reasons apply to minimum sizes!  We should therefore:

- Set `min_size=0` as the default for all size arguments
- In [`hypothesis.internal.validation.check_valid_size`](https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/src/hypothesis/internal/validation.py#L103), add a check `if name == 'min_size' and value is None:` and use `note_deprecation` to warn users that they should use `min_size=0` instead.
- Search for any remaining cases in our codebase where *we* use this argument, with [`git grep --line-number --perl-regexp "min_size ?= ?None"`](https://git-scm.com/docs/git-grep)
- Write a test in `hypothesis-python/tests/cover/test_validation.py` that uses `@checks_deprecated_behaviour` and passes `min_size=None` to `lists()`.


Discussion:

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/extra/django/models.py b/hypothesis-python/src/hypothesis/extra/django/models.py
--- a/hypothesis-python/src/hypothesis/extra/django/models.py
+++ b/hypothesis-python/src/hypothesis/extra/django/models.py
@@ -125,7 +125,7 @@ def _get_strategy_for_field(f):
         strategy = st.sampled_from(choices)
     elif type(f) == dm.SlugField:
         strategy = st.text(alphabet=string.ascii_letters + string.digits,
-                           min_size=(None if f.blank else 1),
+                           min_size=(0 if f.blank else 1),
                            max_size=f.max_length)
     elif type(f) == dm.GenericIPAddressField:
         lookup = {'both': ip4_addr_strings() | ip6_addr_strings(),
@@ -135,7 +135,7 @@ def _get_strategy_for_field(f):
         strategy = st.text(
             alphabet=st.characters(blacklist_characters=u'\x00',
                                    blacklist_categories=('Cs',)),
-            min_size=(None if f.blank else 1),
+            min_size=(0 if f.blank else 1),
             max_size=f.max_length,
         )
         # We can infer a vastly more precise strategy by considering the
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
@@ -107,6 +107,11 @@ def check_valid_size(value, name):
     Otherwise raises InvalidArgument.
     """
     if value is None:
+        if name == 'min_size':
+            from hypothesis._settings import note_deprecation
+            note_deprecation(
+                'min_size=None is deprecated; use min_size=0 instead.'
+            )
         return
     check_type(integer_types + (float,), value)
     if value < 0:
diff --git a/hypothesis-python/src/hypothesis/strategies.py b/hypothesis-python/src/hypothesis/strategies.py
--- a/hypothesis-python/src/hypothesis/strategies.py
+++ b/hypothesis-python/src/hypothesis/strategies.py
@@ -601,7 +601,7 @@ def sampled_from(elements):
 @defines_strategy
 def lists(
     elements=None,  # type: SearchStrategy[Ex]
-    min_size=None,  # type: int
+    min_size=0,  # type: int
     average_size=None,  # type: int
     max_size=None,  # type: int
     unique_by=None,  # type: Callable[..., Any]
@@ -671,7 +671,7 @@ def unique_by(x):
 @defines_strategy
 def sets(
     elements=None,  # type: SearchStrategy[Ex]
-    min_size=None,   # type: int
+    min_size=0,   # type: int
     average_size=None,  # type: int
     max_size=None,  # type: int
 ):
@@ -701,7 +701,7 @@ def sets(
 @defines_strategy
 def frozensets(
     elements=None,  # type: SearchStrategy[Ex]
-    min_size=None,   # type: int
+    min_size=0,   # type: int
     average_size=None,  # type: int
     max_size=None,  # type: int
 ):
@@ -738,7 +738,7 @@ def __repr__(self):
 
 
 @defines_strategy
-def iterables(elements=None, min_size=None, average_size=None, max_size=None,
+def iterables(elements=None, min_size=0, average_size=None, max_size=None,
               unique_by=None, unique=False):
     """This has the same behaviour as lists, but returns iterables instead.
 
@@ -788,7 +788,7 @@ def dictionaries(
     keys,  # type: SearchStrategy[Ex]
     values,  # type: SearchStrategy[T]
     dict_class=dict,  # type: type
-    min_size=None,  # type: int
+    min_size=0,  # type: int
     average_size=None,  # type: int
     max_size=None,  # type: int
 ):
@@ -936,7 +936,7 @@ def characters(
 @defines_strategy_with_reusable_values
 def text(
     alphabet=None,  # type: Union[Sequence[Text], SearchStrategy[Text]]
-    min_size=None,   # type: int
+    min_size=0,   # type: int
     average_size=None,   # type: int
     max_size=None  # type: int
 ):
@@ -1041,7 +1041,7 @@ def from_regex(regex, fullmatch=False):
 @cacheable
 @defines_strategy_with_reusable_values
 def binary(
-    min_size=None, average_size=None, max_size=None
+    min_size=0, average_size=None, max_size=None
 ):
     # type: (int, int, int) -> SearchStrategy[bytes]
     """Generates the appropriate binary type (str in python 2, bytes in python


Test patch:
diff --git a/hypothesis-python/tests/cover/test_validation.py b/hypothesis-python/tests/cover/test_validation.py
--- a/hypothesis-python/tests/cover/test_validation.py
+++ b/hypothesis-python/tests/cover/test_validation.py
@@ -21,11 +21,16 @@
 
 from hypothesis import find, given
 from hypothesis.errors import InvalidArgument
-from tests.common.utils import fails_with
+from tests.common.utils import fails_with, checks_deprecated_behaviour
 from hypothesis.strategies import sets, lists, floats, booleans, \
     integers, recursive, frozensets
 
 
+@checks_deprecated_behaviour
+def test_min_size_none_behavior():
+    lists(integers(), min_size=None).example()
+
+
 def test_errors_when_given_varargs():
     @given(integers())
     def has_varargs(*args):
diff --git a/hypothesis-python/tests/nocover/test_pretty_repr.py b/hypothesis-python/tests/nocover/test_pretty_repr.py
--- a/hypothesis-python/tests/nocover/test_pretty_repr.py
+++ b/hypothesis-python/tests/nocover/test_pretty_repr.py
@@ -54,7 +54,7 @@ def splat(value):
 
 
 size_strategies = dict(
-    min_size=st.integers(min_value=0, max_value=100) | st.none(),
+    min_size=st.integers(min_value=0, max_value=100),
     max_size=st.integers(min_value=0, max_value=100) | st.none(),
 )