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

Small cleanups to use of ... and string predicates #3481

Merged
merged 3 commits into from
Oct 17, 2022
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
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

This patch teaches :func:`~hypothesis.strategies.text` to rewrite a few more
filter predicates (:issue:`3134`). You're unlikely to notice any change.
21 changes: 10 additions & 11 deletions hypothesis-python/src/hypothesis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,15 @@
MappedSearchStrategy,
SearchStrategy,
)
from hypothesis.utils.conventions import infer
from hypothesis.vendor.pretty import RepresentationPrinter
from hypothesis.version import __version__

if sys.version_info >= (3, 10): # pragma: no cover
from types import EllipsisType as InferType
from types import EllipsisType as EllipsisType
elif TYPE_CHECKING:
from builtins import ellipsis as InferType
from builtins import ellipsis as EllipsisType
else:
InferType = type(Ellipsis)
EllipsisType = type(Ellipsis)


TestFunc = TypeVar("TestFunc", bound=Callable)
Expand Down Expand Up @@ -292,7 +291,7 @@ def is_invalid_test(test, original_sig, given_arguments, given_kwargs):
f"arguments, but got {len(given_arguments)} {given_arguments!r}"
)

if infer in given_arguments:
if ... in given_arguments:
return invalid(
"... was passed as a positional argument to @given, but may only be "
"passed as a keyword argument or as the sole argument of @given"
Expand Down Expand Up @@ -998,7 +997,7 @@ def fuzz_one_input(

@overload
def given(
*_given_arguments: Union[SearchStrategy[Any], InferType],
*_given_arguments: Union[SearchStrategy[Any], EllipsisType],
) -> Callable[
[Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
]: # pragma: no cover
Expand All @@ -1007,16 +1006,16 @@ def given(

@overload
def given(
**_given_kwargs: Union[SearchStrategy[Any], InferType],
**_given_kwargs: Union[SearchStrategy[Any], EllipsisType],
) -> Callable[
[Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
]: # pragma: no cover
...


def given(
*_given_arguments: Union[SearchStrategy[Any], InferType],
**_given_kwargs: Union[SearchStrategy[Any], InferType],
*_given_arguments: Union[SearchStrategy[Any], EllipsisType],
**_given_kwargs: Union[SearchStrategy[Any], EllipsisType],
) -> Callable[
[Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
]:
Expand Down Expand Up @@ -1069,9 +1068,9 @@ def run_test_as_given(test):
new_signature = new_given_signature(original_sig, given_kwargs)

# Use type information to convert "infer" arguments into appropriate strategies.
if infer in given_kwargs.values():
if ... in given_kwargs.values():
hints = get_type_hints(test)
for name in [name for name, value in given_kwargs.items() if value is infer]:
for name in [name for name, value in given_kwargs.items() if value is ...]:
if name not in hints:
return _invalid(
f"passed {name}=... for {test.__name__}, but {name} has "
Expand Down
15 changes: 7 additions & 8 deletions hypothesis-python/src/hypothesis/extra/django/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@
from hypothesis.extra.django._fields import from_field
from hypothesis.internal.reflection import define_function_signature
from hypothesis.strategies._internal.utils import defines_strategy
from hypothesis.utils.conventions import infer

if sys.version_info >= (3, 10): # pragma: no cover
from types import EllipsisType as InferType
from types import EllipsisType as EllipsisType
elif TYPE_CHECKING:
from builtins import ellipsis as InferType
from builtins import ellipsis as EllipsisType
else:
InferType = type(Ellipsis)
EllipsisType = type(Ellipsis)


class HypothesisTestCase:
Expand Down Expand Up @@ -67,7 +66,7 @@ class StaticLiveServerTestCase(HypothesisTestCase, dst.StaticLiveServerTestCase)

@defines_strategy()
def from_model(
*model: Type[dm.Model], **field_strategies: Union[st.SearchStrategy, InferType]
*model: Type[dm.Model], **field_strategies: Union[st.SearchStrategy, EllipsisType]
) -> st.SearchStrategy:
"""Return a strategy for examples of ``model``.

Expand Down Expand Up @@ -106,7 +105,7 @@ def from_model(

fields_by_name = {f.name: f for f in m_type._meta.concrete_fields}
for name, value in sorted(field_strategies.items()):
if value is infer:
if value is ...:
field_strategies[name] = from_field(fields_by_name[name])
for name, field in sorted(fields_by_name.items()):
if (
Expand Down Expand Up @@ -157,7 +156,7 @@ def _models_impl(draw, strat):
def from_form(
form: Type[df.Form],
form_kwargs: Optional[dict] = None,
**field_strategies: Union[st.SearchStrategy, InferType],
**field_strategies: Union[st.SearchStrategy, EllipsisType],
) -> st.SearchStrategy[df.Form]:
"""Return a strategy for examples of ``form``.

Expand Down Expand Up @@ -214,7 +213,7 @@ def from_form(
else:
fields_by_name[name] = field
for name, value in sorted(field_strategies.items()):
if value is infer:
if value is ...:
field_strategies[name] = from_field(fields_by_name[name])

for name, field in sorted(fields_by_name.items()):
Expand Down
20 changes: 8 additions & 12 deletions hypothesis-python/src/hypothesis/extra/ghostwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,13 @@
SampledFromStrategy,
)
from hypothesis.strategies._internal.types import _global_type_lookup, is_generic_type
from hypothesis.utils.conventions import infer

if sys.version_info >= (3, 10): # pragma: no cover
from types import EllipsisType as InferType
from types import EllipsisType as EllipsisType
elif TYPE_CHECKING:
from builtins import ellipsis as InferType
from builtins import ellipsis as EllipsisType
else:
InferType = type(Ellipsis)
EllipsisType = type(Ellipsis)


IMPORT_SECTION = """
Expand Down Expand Up @@ -252,10 +251,7 @@ def _type_from_doc_fragment(token: str) -> Optional[type]:
return getattr(sys.modules.get(mod, None), name, None)


def _strategy_for(
param: inspect.Parameter,
docstring: str,
) -> Union[st.SearchStrategy, InferType]:
def _strategy_for(param: inspect.Parameter, docstring: str) -> st.SearchStrategy:
# Example types in docstrings:
# - `:type a: sequence of integers`
# - `b (list, tuple, or None): ...`
Expand Down Expand Up @@ -532,7 +528,7 @@ def _get_strategies(
hints = get_type_hints(f)
docstring = getattr(f, "__doc__", None) or ""
builder_args = {
k: infer if k in hints else _strategy_for(v, docstring)
k: ... if k in hints else _strategy_for(v, docstring)
for k, v in params.items()
}
with _with_any_registered():
Expand Down Expand Up @@ -1323,7 +1319,7 @@ def binary_operation(
*,
associative: bool = True,
commutative: bool = True,
identity: Union[X, InferType, None] = infer,
identity: Union[X, EllipsisType, None] = ...,
distributes_over: Optional[Callable[[X, X], X]] = None,
except_: Except = (),
style: str = "pytest",
Expand Down Expand Up @@ -1384,7 +1380,7 @@ def _make_binop_body(
*,
associative: bool = True,
commutative: bool = True,
identity: Union[X, InferType, None] = infer,
identity: Union[X, EllipsisType, None] = ...,
distributes_over: Optional[Callable[[X, X], X]] = None,
except_: Tuple[Type[Exception], ...],
style: str,
Expand Down Expand Up @@ -1454,7 +1450,7 @@ def maker(
# Guess that the identity element is the minimal example from our operands
# strategy. This is correct often enough to be worthwhile, and close enough
# that it's a good starting point to edit much of the rest.
if identity is infer:
if identity is ...:
try:
identity = find(operands, lambda x: True, settings=_quietly_settings)
except Exception:
Expand Down
20 changes: 10 additions & 10 deletions hypothesis-python/src/hypothesis/strategies/_internal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@
TextStrategy,
)
from hypothesis.strategies._internal.utils import cacheable, defines_strategy
from hypothesis.utils.conventions import infer, not_set
from hypothesis.utils.conventions import not_set

if sys.version_info >= (3, 10): # pragma: no cover
from types import EllipsisType as InferType
from types import EllipsisType as EllipsisType
elif typing.TYPE_CHECKING: # pragma: no cover
from builtins import ellipsis as InferType
from builtins import ellipsis as EllipsisType
else:
InferType = type(Ellipsis)
EllipsisType = type(Ellipsis)


try:
Expand Down Expand Up @@ -863,7 +863,7 @@ def __repr__(self):
@defines_strategy()
def builds(
*callable_and_args: Union[Callable[..., Ex], SearchStrategy[Any]],
**kwargs: Union[SearchStrategy[Any], InferType],
**kwargs: Union[SearchStrategy[Any], EllipsisType],
) -> SearchStrategy[Ex]:
"""Generates values by drawing from ``args`` and ``kwargs`` and passing
them to the callable (provided as the first positional argument) in the
Expand Down Expand Up @@ -898,14 +898,14 @@ def builds(
"target to construct."
)

if infer in args: # type: ignore # we only annotated the allowed types
if ... in args: # type: ignore # we only annotated the allowed types
# Avoid an implementation nightmare juggling tuples and worse things
raise InvalidArgument(
"... was passed as a positional argument to "
"builds(), but is only allowed as a keyword arg"
)
required = required_args(target, args, kwargs)
to_infer = {k for k, v in kwargs.items() if v is infer}
to_infer = {k for k, v in kwargs.items() if v is ...}
if required or to_infer:
if isinstance(target, type) and attr.has(target):
# Use our custom introspection for attrs classes
Expand Down Expand Up @@ -1993,7 +1993,7 @@ def _functions(*, like, returns, pure):
"The first argument to functions() must be a callable to imitate, "
f"but got non-callable like={nicerepr(like)!r}"
)
if returns is None or returns is infer:
if returns in (None, ...):
# Passing `None` has never been *documented* as working, but it still
# did from May 2020 to Jan 2022 so we'll avoid breaking it without cause.
hints = get_type_hints(like)
Expand Down Expand Up @@ -2036,7 +2036,7 @@ def functions(
...

@defines_strategy()
def functions(*, like=lambda: None, returns=infer, pure=False):
def functions(*, like=lambda: None, returns=..., pure=False):
# We shouldn't need overloads here, but mypy disallows default args for
# generics: https://github.com/python/mypy/issues/3737
"""functions(*, like=lambda: None, returns=..., pure=False)
Expand Down Expand Up @@ -2067,7 +2067,7 @@ def functions(*, like=lambda: None, returns=infer, pure=False):
def functions(
*,
like: Callable[..., Any] = lambda: None,
returns: Union[SearchStrategy[Any], InferType] = infer,
returns: Union[SearchStrategy[Any], EllipsisType] = ...,
pure: bool = False,
) -> SearchStrategy[Callable[..., Any]]:
"""functions(*, like=lambda: None, returns=..., pure=False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,14 @@ def __repr__(self):
# See https://docs.python.org/3/library/stdtypes.html#string-methods
# These methods always return Truthy values for any nonempty string.
_nonempty_filters = ListStrategy._nonempty_filters + (
str,
str.capitalize,
str.casefold,
str.encode,
str.expandtabs,
str.join,
str.lower,
str.rsplit,
str.split,
str.splitlines,
str.swapcase,
Expand All @@ -144,6 +147,9 @@ def __repr__(self):
str.isnumeric,
str.isspace,
str.istitle,
str.lstrip,
str.rstrip,
str.strip,
)

def filter(self, condition):
Expand All @@ -156,9 +162,6 @@ def filter(self, condition):
# We use ListStrategy filter logic for the conditions that *only* imply
# the string is nonempty. Here, we increment the min_size but still apply
# the filter for conditions that imply nonempty *and specific contents*.
#
# TODO: we may eventually rewrite the elements_strategy for some of these,
# avoiding rejection sampling and making them much more efficient.
if condition in self._nonempty_and_content_filters:
assert self.max_size >= 1, "Always-empty is special cased in st.text()"
self = copy.copy(self)
Expand Down
3 changes: 2 additions & 1 deletion hypothesis-python/tests/cover/test_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ def test(self):

SUBTEST_SUITE = """
import unittest
from hypothesis import given, strategies as st
from hypothesis import given, settings, strategies as st

class MyTest(unittest.TestCase):
@given(s=st.text())
@settings(deadline=None)
def test_subtest(self, s):
with self.subTest(text=s):
self.assertIsInstance(s, str)
Expand Down