Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 56 additions & 12 deletions Lib/test/support/_hypothesis_stubs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import dataclass, replace
from enum import Enum
import functools
import unittest
Expand All @@ -18,21 +19,52 @@
from . import stateful, strategies


@dataclass(frozen=True)
class _Example:
args: tuple
kwargs: dict
# Set by example.xfail(): the exception types the example must raise.
raises: type[BaseException] | tuple[type[BaseException], ...] | None = None
reason: str = ""

def __repr__(self):
bits = [repr(a) for a in self.args]
bits += [f"{k}={v!r}" for k, v in self.kwargs.items()]
return f"@example({', '.join(bits)})"


def _run_example(f, self, ex):
if ex.raises is None:
f(self, *ex.args, **ex.kwargs)
return
try:
f(self, *ex.args, **ex.kwargs)
except Exception as exc:
if not isinstance(exc, ex.raises):
raise
return
reason = f" because {ex.reason}" if ex.reason else ""
raise AssertionError(
f"Expected an exception from {ex!r}{reason}, "
"but no exception was raised"
)


def given(*_args, **_kwargs):
def decorator(f):
if examples := getattr(f, "_examples", []):

@functools.wraps(f)
def test_function(self):
for example_args, example_kwargs in examples:
if len(example_args) < 2:
subtest_args = example_args
for ex in examples:
if len(ex.args) < 2:
subtest_args = ex.args
else:
# subTest takes up to one positional argument.
# When there are more, display them as a tuple
subtest_args = [example_args]
with self.subTest(*subtest_args, **example_kwargs):
f(self, *example_args, **example_kwargs)
subtest_args = [ex.args]
with self.subTest(*subtest_args, **ex.kwargs):
_run_example(f, self, ex)

else:
# If we have found no examples, we must skip the test. If @example
Expand All @@ -49,16 +81,18 @@ def test_function(self):
return decorator


def example(*args, **kwargs):
if bool(args) == bool(kwargs):
raise ValueError("Must specify exactly one of *args or **kwargs")
class example:
def __init__(self, *args, **kwargs):
if bool(args) == bool(kwargs):
raise ValueError("Must specify exactly one of *args or **kwargs")
self._example = _Example(args, kwargs)

def decorator(f):
def __call__(self, f):
base_func = getattr(f, "__wrapped__", f)
if not hasattr(base_func, "_examples"):
base_func._examples = []

base_func._examples.append((args, kwargs))
base_func._examples.append(self._example)

if getattr(f, "_given", False):
# If the given decorator is below all the example decorators,
Expand All @@ -68,7 +102,17 @@ def decorator(f):

return f

return decorator
def xfail(self, condition=True, *, reason="", raises=BaseException):
# Like real Hypothesis, an expected failure is strict: the example
# must raise one of the given exceptions, or the test fails.
if condition:
self._example = replace(self._example, raises=raises,
reason=reason)
return self

def via(self, whence, /):
# A label for tooling; it does not change what runs.
return self


def assume(condition):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add ``example.xfail()`` and ``example.via()`` to the Hypothesis stubs used
when Hypothesis is not installed, so that property tests can mark an explicit
example as an expected failure in both modes.
Loading