Repository: HypothesisWorks/hypothesis

Issue:
`st.uuids()` does not generate the NIL UUID
Looking at the [`strategies.uuids()`](https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.uuids) code

https://github.com/HypothesisWorks/hypothesis/blob/2fbd75080015fb0f27b6d69419fbbc62d44ab739/hypothesis-python/src/hypothesis/strategies/_internal/core.py#L1618-L1639

I noticed that the [Nil UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Nil_UUID) is not included. It would make sense to add it into the mix, perhaps using a default argument that disables it to ensure backward compatibility, e.g.
```python
def uuids(*, version: Optional[int] = None, include_nil: bool = False) -> SearchStrategy[UUID]:
```
Happy to provide a PR, if you’re interested…


Discussion:
So long as it remains true that "All returned values from this will be unique, so e.g. if you do `lists(uuids())` the resulting list will never contain duplicates." (at least with the default arguments), I'd be delighted to accept such a PR 🙂 

@Zac-HD a little help would be nice 😉 

The `uuids()` function returns a [`SearchStrategy`](https://github.com/HypothesisWorks/hypothesis/blob/2fbd75080015fb0f27b6d69419fbbc62d44ab739/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py#L209) for [UUID](https://docs.python.org/3/library/uuid.html#uuid.UUID) objects. Now we want to add a specific value into that stream of random draws.

We could do that by [chaining](https://hypothesis.readthedocs.io/en/latest/data.html#chaining-strategies-together) the current `uuids()` strategy with e.g. `one_of(uuid.UUID("00000000-0000-0000-0000-000000000000"))` but I think there’s a slim chance that the underlying random numbers produce `0` as well in which case we’d chain two Nil UUIDs.

The other way might be to extend/subclass the [`RandomStrategy`](https://github.com/HypothesisWorks/hypothesis/blob/2fbd75080015fb0f27b6d69419fbbc62d44ab739/hypothesis-python/src/hypothesis/strategies/_internal/random.py#L426) with an argument for its `__init__` which allows us to add one (or  more?) constants. That way, we can ensure that some values are guaranteed returned by the `RandomStrategy` thus making it not so random anymore.

What’s your suggestion?
Code attached - if you write tests, add a `RELEASE.rst` for a `minor` release (see examples in other PRs), add yourself to the `AUTHORS.rst` list, and open a PR, I'll probably merge it.

```diff
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
index 96d9a8160..0e3c01d0b 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
@@ -1615,28 +1615,46 @@ def shared(
     return SharedStrategy(base, key)


+@composite
+def _maybe_nil_uuids(draw, random_uuids):
+    # Equivalent to `random_uuids | just(...)`, with a stronger bias to the former.
+    uuid = draw(random_uuids)  # Make shrinking easier by unconditionally drawing this.
+    if draw(data()).conjecture_data.draw_bits(6) == 63:
+        return UUID("00000000-0000-0000-0000-000000000000")
+    return uuid
+
+
 @cacheable
 @defines_strategy(force_reusable_values=True)
-def uuids(*, version: Optional[int] = None) -> SearchStrategy[UUID]:
+def uuids(
+    *, version: Optional[int] = None, allow_nil: bool = False
+) -> SearchStrategy[UUID]:
     """Returns a strategy that generates :class:`UUIDs <uuid.UUID>`.

     If the optional version argument is given, value is passed through
     to :class:`~python:uuid.UUID` and only UUIDs of that version will
     be generated.

-    All returned values from this will be unique, so e.g. if you do
+    If ``allow_nil` is True, generate the nil UUID much more often.
+    Otherwise, all returned values from this will be unique, so e.g. if you do
     ``lists(uuids())`` the resulting list will never contain duplicates.

     Examples from this strategy don't have any meaningful shrink order.
     """
+    check_type(bool, allow_nil, "allow_nil")
     if version not in (None, 1, 2, 3, 4, 5):
         raise InvalidArgument(
             f"version={version!r}, but version must be in "
             "(None, 1, 2, 3, 4, 5) to pass to the uuid.UUID constructor."
         )
-    return shared(
+    random_uuids = shared(
         randoms(use_true_random=True), key="hypothesis.strategies.uuids.generator"
     ).map(lambda r: UUID(version=version, int=r.getrandbits(128)))
+    if allow_nil:
+        if version is not None:
+            raise InvalidArgument("The nil UUID is not of any version")
+        return random_uuids.flatmap(_maybe_nil_uuids)
+    return random_uuids


 class RunnerStrategy(SearchStrategy):
```
@Zac-HD, you did all the work and much more elegantly than I could’ve (not enough experience yet with Hypothesis). Thank you so much! I’ll send the PR later today.

Interesting detail you have there: the Nil UUID will be generated only if _no version_ is specified. Which makes sense, I guess — according to RFC 4122 the [version bits](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.3) are never zero such that the [RFC Nil UUID](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.7) is of no version. So that means that the above strategy will _not_ generate a Nil UUID if I ask for e.g. v4 (random) UUIDs… 🤔
Haha, you'll be doing plenty of work in writing the tests etc too!  Happy to help, it's nice when all the practice is good for something.

And yeah, the nil UUID *isn't* version four, because by defininition all the version bits are zeroed out.  Weird, but for Hypothesis I always try to err on the side of "unbearably pedantic but technically correct" - the last thing we want is to make someone's test fail, and then have them point out that it's actually a bug in Hypothesis...
Ping @jenstroeger - can I give you a hand with that PR?
@Zac-HD, I haven’t forgotten but was a bit busy. Is the [test_uuids.py](https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/tests/nocover/test_uuids.py) the place to start?
Yep!  Feel free to open an incomplete PR as soon as you get started, it's much easier to comment that way. 

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/strategies/__init__.py b/hypothesis-python/src/hypothesis/strategies/__init__.py
--- a/hypothesis-python/src/hypothesis/strategies/__init__.py
+++ b/hypothesis-python/src/hypothesis/strategies/__init__.py
@@ -121,6 +121,9 @@ def _check_exports(_public):
 
     # Verify that all exported strategy functions were registered with
     # @declares_strategy.
+
+    existing_strategies = set(_strategies) - {"_maybe_nil_uuids"}
+
     exported_strategies = set(__all__) - {
         "DataObject",
         "DrawFn",
@@ -128,9 +131,9 @@ def _check_exports(_public):
         "composite",
         "register_type_strategy",
     }
-    assert set(_strategies) == exported_strategies, (
-        set(_strategies) - exported_strategies,
-        exported_strategies - set(_strategies),
+    assert existing_strategies == exported_strategies, (
+        existing_strategies - exported_strategies,
+        exported_strategies - existing_strategies,
     )
 
 
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
@@ -1639,29 +1639,47 @@ def shared(
     return SharedStrategy(base, key)
 
 
+@composite
+def _maybe_nil_uuids(draw, uuid):
+    # Equivalent to `random_uuids | just(...)`, with a stronger bias to the former.
+    if draw(data()).conjecture_data.draw_bits(6) == 63:
+        return UUID("00000000-0000-0000-0000-000000000000")
+    return uuid
+
+
 @cacheable
 @defines_strategy(force_reusable_values=True)
-def uuids(*, version: Optional[int] = None) -> SearchStrategy[UUID]:
+def uuids(
+    *, version: Optional[int] = None, allow_nil: bool = False
+) -> SearchStrategy[UUID]:
     """Returns a strategy that generates :class:`UUIDs <uuid.UUID>`.
 
     If the optional version argument is given, value is passed through
     to :class:`~python:uuid.UUID` and only UUIDs of that version will
     be generated.
 
-    All returned values from this will be unique, so e.g. if you do
+    If ``allow_nil` is True, generate the nil UUID much more often.
+    Otherwise, all returned values from this will be unique, so e.g. if you do
     ``lists(uuids())`` the resulting list will never contain duplicates.
 
     Examples from this strategy don't have any meaningful shrink order.
     """
+    check_type(bool, allow_nil, "allow_nil")
     if version not in (None, 1, 2, 3, 4, 5):
         raise InvalidArgument(
             f"version={version!r}, but version must be in "
             "(None, 1, 2, 3, 4, 5) to pass to the uuid.UUID constructor."
         )
-    return shared(
+    random_uuids = shared(
         randoms(use_true_random=True), key="hypothesis.strategies.uuids.generator"
     ).map(lambda r: UUID(version=version, int=r.getrandbits(128)))
 
+    if allow_nil:
+        if version is not None:
+            raise InvalidArgument("The nil UUID is not of any version")
+        return random_uuids.flatmap(_maybe_nil_uuids)
+    return random_uuids
+
 
 class RunnerStrategy(SearchStrategy):
     def __init__(self, default):


Test patch:
diff --git a/hypothesis-python/tests/cover/test_uuids.py b/hypothesis-python/tests/cover/test_uuids.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/cover/test_uuids.py
@@ -0,0 +1,34 @@
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Copyright the Hypothesis Authors.
+# Individual contributors are listed in AUTHORS.rst and the git log.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at https://mozilla.org/MPL/2.0/.
+
+import uuid
+
+import pytest
+
+from hypothesis import strategies as st
+from hypothesis.errors import InvalidArgument
+
+from tests.common.debug import assert_no_examples, find_any
+
+
+def test_no_nil_uuid_by_default():
+    assert_no_examples(st.uuids(), lambda x: x == uuid.UUID(int=0))
+
+
+def test_can_generate_nil_uuid():
+    find_any(st.uuids(allow_nil=True), lambda x: x == uuid.UUID(int=0))
+
+
+def test_can_only_allow_nil_uuid_with_none_version():
+    st.uuids(version=None, allow_nil=True).example()
+    with pytest.raises(InvalidArgument):
+        st.uuids(version=4, allow_nil=True).example()
+    with pytest.raises(InvalidArgument):
+        st.uuids(version=None, allow_nil="not a bool").example()
diff --git a/whole-repo-tests/test_mypy.py b/whole-repo-tests/test_mypy.py
--- a/whole-repo-tests/test_mypy.py
+++ b/whole-repo-tests/test_mypy.py
@@ -39,7 +39,10 @@ def get_mypy_output(fname, *extra_args):
 
 
 def get_mypy_analysed_type(fname, val):
-    out = get_mypy_output(fname)
+    out = get_mypy_output(fname).rstrip()
+    msg = "Success: no issues found in 1 source file"
+    if out.endswith(msg):
+        out = out[: -len(msg)]
     assert len(out.splitlines()) == 1
     # See https://mypy.readthedocs.io/en/latest/common_issues.html#reveal-type
     # The shell output for `reveal_type([1, 2, 3])` looks like a literal: