Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register builds() of recursive types with constraints #3164

Merged
merged 2 commits into from Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions hypothesis-python/RELEASE.rst
@@ -0,0 +1,19 @@
RELEASE_TYPE: minor

This release teaches :func:`~hypothesis.strategies.builds` to use
:func:`~hypothesis.strategies.deferred` when resolving unrecognised type hints,
so that you can conveniently register strategies for recursive types
with constraints on some arguments (:issue:`3026`):

.. code-block:: python

class RecursiveClass:
def __init__(self, value: int, next_node: typing.Optional["SomeClass"]):
assert value > 0
self.value = value
self.next_node = next_node


st.register_type_strategy(
RecursiveClass, st.builds(RecursiveClass, value=st.integers(min_value=1))
)
5 changes: 5 additions & 0 deletions hypothesis-python/src/hypothesis/extra/ghostwriter.py
Expand Up @@ -53,6 +53,11 @@
-e, --except OBJ_NAME dotted name of exception(s) to ignore
-h, --help Show this message and exit.

.. tip::

Using a light theme? Hypothesis respects `NO_COLOR <https://no-color.org/>`__
and :envvar:`DJANGO_COLORS=light <django:DJANGO_COLORS>`.

.. note::

The ghostwriter requires :pypi:`black`, but the generated code only
Expand Down
17 changes: 15 additions & 2 deletions hypothesis-python/src/hypothesis/strategies/_internal/core.py
Expand Up @@ -886,8 +886,21 @@ def builds(
raise InvalidArgument(
f"passed infer for {badargs}, but there is no type annotation"
)
for kw in set(hints) & (required | to_infer):
kwargs[kw] = from_type(hints[kw])
infer_for = {k: v for k, v in hints.items() if k in (required | to_infer)}
if infer_for:
from hypothesis.strategies._internal.types import _global_type_lookup

for kw, t in infer_for.items():
if (
getattr(t, "__module__", None) in ("builtins", "typing")
or t in _global_type_lookup
):
kwargs[kw] = from_type(t)
else:
# We defer resolution of these type annotations so that the obvious
# approach to registering recursive types just works. See
# https://github.com/HypothesisWorks/hypothesis/issues/3026
kwargs[kw] = deferred(lambda t=t: from_type(t)) # type: ignore
return BuildsStrategy(target, args, kwargs)


Expand Down
3 changes: 3 additions & 0 deletions hypothesis-python/src/hypothesis/utils/terminal.py
Expand Up @@ -23,6 +23,9 @@ def guess_background_color():
See also https://stackoverflow.com/questions/2507337/ and
https://unix.stackexchange.com/questions/245378/
"""
django_colors = os.getenv("DJANGO_COLORS")
if django_colors in ("light", "dark"):
return django_colors
# Guessing based on the $COLORFGBG environment variable
try:
fg, *_, bg = os.getenv("COLORFGBG").split(";")
Expand Down
18 changes: 18 additions & 0 deletions hypothesis-python/tests/cover/test_lookup.py
Expand Up @@ -547,6 +547,24 @@ def test_resolving_recursive_type():
assert isinstance(st.builds(Tree).example(), Tree)


class SomeClass:
def __init__(self, value: int, next_node: typing.Optional["SomeClass"]) -> None:
assert value > 0
self.value = value
self.next_node = next_node

def __repr__(self) -> str:
return f"SomeClass({self.value}, next_node={self.next_node})"


def test_resolving_recursive_type_with_registered_constraint():
with temp_registered(
SomeClass, st.builds(SomeClass, value=st.integers(min_value=1))
):
find_any(st.from_type(SomeClass), lambda s: s.next_node is None)
find_any(st.from_type(SomeClass), lambda s: s.next_node is not None)


@given(from_type(typing.Tuple[()]))
def test_resolves_empty_Tuple_issue_1583_regression(ex):
# See e.g. https://github.com/python/mypy/commit/71332d58
Expand Down