Repository: HypothesisWorks/hypothesis

Issue:
Add a `full_match` argument to `st.from_regex()`
Just reading #839, and thinking of what I skimmed on the hypothesis-regex PRs, I wonder if the need for boundary markers is strongly signalled enough? And if not, how we might make it clearer.

One way to make it more visible would be to add a `full_match` parameter to the strategy, which automatically adds boundary markers if not already present. That feels like it wouldn’t be a huge amount of work, and might make this limitation clearer. But I missed most of the initial discussion on this strategy, so I might be missing something.

What do other people/maintainers think?

(I’m assuming prepending a dollar to a string without throwing a TypeError is possible, because strings are obviously so easy and error-free to work with…)


Discussion:
I feel like it's reasonably strongly signalled! The following is the concluding paragraph of the docstring:

> If you want the generated string to match the whole regex you should use boundary markers. So e.g. r"\A.\Z" will return a single character string, while "." will return any string, and r"\A.$" will return a single character optionally followed by a "\n".

It would probably be reasonable to add a `full_match` parameter. We went with the "a string containing a match" semantics because full matching plays very badly with things like lookahead, but adding it in as an option by just literally inserting boundary markers (which are "\A" and  "\Z", not "." and "$") and failing to produce any results in that scenario is perfectly reasonable.
I agree with David - the current behaviour is a good default but adding an argument (that adds `\A` and `\Z` boundaries) is also pretty useful.  I'd prefer that the new parameter be a verb or verb phrase, but don't have any good suggestions.  (`match_all`?)

Considerations:  we rejected a `flags` argument last time on the basis that `regex` can be compiled with flags, but if we're adding arguments that would be the natural second argument and we can only do that once.  On the other hand, how would we handle conflicting flags if both were given?  I still like our current system and propose no change, but thought it worth mentioning.
Claimed by yours truly at PyCon sprints (see #1513).

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/searchstrategy/regex.py b/hypothesis-python/src/hypothesis/searchstrategy/regex.py
--- a/hypothesis-python/src/hypothesis/searchstrategy/regex.py
+++ b/hypothesis-python/src/hypothesis/searchstrategy/regex.py
@@ -235,7 +235,7 @@ def base_regex_strategy(regex, parsed=None):
     ))
 
 
-def regex_strategy(regex):
+def regex_strategy(regex, fullmatch):
     if not hasattr(regex, 'pattern'):
         regex = re.compile(regex)
 
@@ -261,7 +261,9 @@ def regex_strategy(regex):
     right_pad = base_padding_strategy
     left_pad = base_padding_strategy
 
-    if parsed[-1][0] == sre.AT:
+    if fullmatch:
+        right_pad = empty
+    elif parsed[-1][0] == sre.AT:
         if parsed[-1][1] == sre.AT_END_STRING:
             right_pad = empty
         elif parsed[-1][1] == sre.AT_END:
@@ -272,7 +274,9 @@ def regex_strategy(regex):
                 )
             else:
                 right_pad = st.one_of(empty, newline)
-    if parsed[0][0] == sre.AT:
+    if fullmatch:
+        left_pad = empty
+    elif parsed[0][0] == sre.AT:
         if parsed[0][1] == sre.AT_BEGINNING_STRING:
             left_pad = empty
         elif parsed[0][1] == sre.AT_BEGINNING:
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
@@ -1002,8 +1002,8 @@ def text(
 
 @cacheable
 @defines_strategy
-def from_regex(regex):
-    # type: (Union[AnyStr, Pattern[AnyStr]]) -> SearchStrategy[AnyStr]
+def from_regex(regex, fullmatch=False):
+    # type: (Union[AnyStr, Pattern[AnyStr]], bool) -> SearchStrategy[AnyStr]
     """Generates strings that contain a match for the given regex (i.e. ones
     for which :func:`re.search` will return a non-None result).
 
@@ -1025,14 +1025,17 @@ def from_regex(regex):
     boundary markers. So e.g. ``r"\\A.\\Z"`` will return a single character
     string, while ``"."`` will return any string, and ``r"\\A.$"`` will return
     a single character optionally followed by a ``"\\n"``.
+    Alternatively, passing ``fullmatch=True`` will ensure that the whole
+    string is a match, as if you had used the ``\\A`` and ``\\Z`` markers.
 
     Examples from this strategy shrink towards shorter strings and lower
-    character values.
+    character values, with exact behaviour that may depend on the pattern.
     """
+    check_type(bool, fullmatch, 'fullmatch')
     # TODO: We would like to move this to the top level, but pending some major
     # refactoring it's hard to do without creating circular imports.
     from hypothesis.searchstrategy.regex import regex_strategy
-    return regex_strategy(regex)
+    return regex_strategy(regex, fullmatch)
 
 
 @cacheable


Test patch:
diff --git a/hypothesis-python/tests/cover/test_regex.py b/hypothesis-python/tests/cover/test_regex.py
--- a/hypothesis-python/tests/cover/test_regex.py
+++ b/hypothesis-python/tests/cover/test_regex.py
@@ -25,6 +25,7 @@
 
 import hypothesis.strategies as st
 from hypothesis import given, assume
+from hypothesis.errors import InvalidArgument
 from tests.common.debug import find_any, assert_no_examples, \
     assert_all_examples
 from hypothesis.internal.compat import PY3, hrange, hunichr
@@ -388,3 +389,38 @@ def test_issue_992_regression(data):
             \.    # the decimal point
             \d *  # some fractional digits""", re.VERBOSE))
     data.draw(strat)
+
+
+@pytest.mark.parametrize('pattern,matching_str', [
+    (u'a', u'a'),
+    (u'[Aa]', u'A'),
+    (u'[ab]*', u'abb'),
+    (b'[Aa]', b'A'),
+    (b'[ab]*', b'abb'),
+    (re.compile(u'[ab]*', re.IGNORECASE), u'aBb'),
+    (re.compile(b'[ab]', re.IGNORECASE), b'A'),
+])
+def test_fullmatch_generates_example(pattern, matching_str):
+    find_any(st.from_regex(pattern, fullmatch=True),
+             lambda s: s == matching_str)
+
+
+@pytest.mark.parametrize('pattern,eqiv_pattern', [
+    (u'a', u'\\Aa\\Z'),
+    (u'[Aa]', u'\\A[Aa]\\Z'),
+    (u'[ab]*', u'\\A[ab]*\\Z'),
+    (b'[Aa]', br'\A[Aa]\Z'),
+    (b'[ab]*', br'\A[ab]*\Z'),
+    (re.compile(u'[ab]*', re.IGNORECASE),
+     re.compile(u'\\A[ab]*\\Z', re.IGNORECASE)),
+    (re.compile(br'[ab]', re.IGNORECASE),
+     re.compile(br'\A[ab]\Z', re.IGNORECASE))
+])
+def test_fullmatch_matches(pattern, eqiv_pattern):
+    assert_all_examples(st.from_regex(pattern, fullmatch=True),
+                        lambda s: re.match(eqiv_pattern, s))
+
+
+def test_fullmatch_must_be_bool():
+    with pytest.raises(InvalidArgument):
+        st.from_regex('a', fullmatch=None).validate()
diff --git a/hypothesis-python/tests/cover/test_slippage.py b/hypothesis-python/tests/cover/test_slippage.py
--- a/hypothesis-python/tests/cover/test_slippage.py
+++ b/hypothesis-python/tests/cover/test_slippage.py
@@ -29,6 +29,7 @@
 def test_raises_multiple_failures_with_varying_type():
     target = [None]
 
+    @settings(database=None)
     @given(st.integers())
     def test(i):
         if abs(i) < 1000:
@@ -162,7 +163,8 @@ def test_shrinks_both_failures():
     duds = set()
     second_target = [None]
 
-    @given(st.integers())
+    @settings(database=None)
+    @given(st.integers(min_value=0))
     def test(i):
         if i >= 10000:
             first_has_failed[0] = True