Skip to content

Commit

Permalink
Merge pull request #3372 from Zac-HD/using-exceptiongroups
Browse files Browse the repository at this point in the history
Simple initial uses of `ExceptionGroup`
  • Loading branch information
Zac-HD authored Jun 12, 2022
2 parents 9461c7e + c45abda commit 2ccf867
Show file tree
Hide file tree
Showing 10 changed files with 62 additions and 63 deletions.
5 changes: 5 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RELEASE_TYPE: patch

We now use the :pep:`654` `ExceptionGroup <https://docs.python.org/3.11/library/exceptions.html#ExceptionGroup>`__
type - provided by the :pypi:`exceptiongroup` backport on older Pythons -
to ensure that if multiple errors are raised in teardown, they will all propagate.
6 changes: 5 additions & 1 deletion hypothesis-python/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ def local_file(name):
description="A library for property-based testing",
zip_safe=False,
extras_require=extras,
install_requires=["attrs>=19.2.0", "sortedcontainers>=2.1.0,<3.0.0"],
install_requires=[
"attrs>=19.2.0",
"exceptiongroup>=1.0.0rc8 ; python_version<'3.11.0b1'",
"sortedcontainers>=2.1.0,<3.0.0",
],
python_requires=">=3.7",
classifiers=[
"Development Status :: 5 - Production/Stable",
Expand Down
20 changes: 9 additions & 11 deletions hypothesis-python/src/hypothesis/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
# obtain one at https://mozilla.org/MPL/2.0/.

import math
import traceback
from typing import NoReturn, Union

from hypothesis import Verbosity, settings
from hypothesis.errors import CleanupFailed, InvalidArgument, UnsatisfiedAssumption
from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption
from hypothesis.internal.compat import BaseExceptionGroup
from hypothesis.internal.conjecture.data import ConjectureData
from hypothesis.internal.validation import check_type
from hypothesis.reporting import report, verbose_report
Expand Down Expand Up @@ -74,18 +74,16 @@ def __enter__(self):

def __exit__(self, exc_type, exc_value, tb):
self.assign_variable.__exit__(exc_type, exc_value, tb)
if self.close() and exc_type is None:
raise CleanupFailed()

def close(self):
any_failed = False
errors = []
for task in self.tasks:
try:
task()
except BaseException:
any_failed = True
report(traceback.format_exc())
return any_failed
except BaseException as err:
errors.append(err)
if errors:
if len(errors) == 1:
raise errors[0] from exc_value
raise BaseExceptionGroup("Cleanup failed", errors) from exc_value


def cleanup(teardown):
Expand Down
4 changes: 0 additions & 4 deletions hypothesis-python/src/hypothesis/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ class _Trimmable(HypothesisException):
"""Hypothesis can trim these tracebacks even if they're raised internally."""


class CleanupFailed(HypothesisException):
"""At least one cleanup task failed and no other exception was raised."""


class UnsatisfiedAssumption(HypothesisException):
"""An internal error raised by assume.
Expand Down
11 changes: 6 additions & 5 deletions hypothesis-python/src/hypothesis/internal/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

try:
BaseExceptionGroup = BaseExceptionGroup
except NameError: # pragma: no cover
try:
from exceptiongroup import BaseExceptionGroup as BaseExceptionGroup # for mypy
except ImportError:
BaseExceptionGroup = () # valid in isinstance and except clauses!
ExceptionGroup = ExceptionGroup # pragma: no cover
except NameError:
from exceptiongroup import (
BaseExceptionGroup as BaseExceptionGroup,
ExceptionGroup as ExceptionGroup,
)

PYPY = platform.python_implementation() == "PyPy"
WINDOWS = platform.system() == "Windows"
Expand Down
27 changes: 11 additions & 16 deletions hypothesis-python/src/hypothesis/strategies/_internal/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

from hypothesis import strategies as st
from hypothesis.errors import InvalidArgument, ResolutionFailed
from hypothesis.internal.compat import PYPY
from hypothesis.internal.compat import PYPY, BaseExceptionGroup, ExceptionGroup
from hypothesis.internal.conjecture.utils import many as conjecture_utils_many
from hypothesis.strategies._internal.datetime import zoneinfo # type: ignore
from hypothesis.strategies._internal.ipaddress import (
Expand Down Expand Up @@ -554,6 +554,16 @@ def _networks(bits):
UnicodeTranslateError: st.builds(
UnicodeTranslateError, st.text(), st.just(0), st.just(0), st.just("reason")
),
BaseExceptionGroup: st.builds(
BaseExceptionGroup,
st.text(),
st.lists(st.from_type(BaseException), min_size=1),
),
ExceptionGroup: st.builds(
ExceptionGroup,
st.text(),
st.lists(st.from_type(Exception), min_size=1),
),
enumerate: st.builds(enumerate, st.just(())),
filter: st.builds(filter, st.just(lambda _: None), st.just(())),
map: st.builds(map, st.just(lambda _: None), st.just(())),
Expand All @@ -569,21 +579,6 @@ def _networks(bits):
_global_type_lookup[zoneinfo.ZoneInfo] = st.timezones()
if PYPY:
_global_type_lookup[builtins.sequenceiterator] = st.builds(iter, st.tuples()) # type: ignore
try:
BaseExceptionGroup # type: ignore # noqa
except NameError:
pass
else: # pragma: no cover
_global_type_lookup[BaseExceptionGroup] = st.builds( # type: ignore
BaseExceptionGroup, # type: ignore
st.text(),
st.lists(st.from_type(BaseException), min_size=1),
)
_global_type_lookup[ExceptionGroup] = st.builds( # type: ignore
ExceptionGroup, # type: ignore
st.text(),
st.lists(st.from_type(Exception), min_size=1),
)


_global_type_lookup[type] = st.sampled_from(
Expand Down
48 changes: 23 additions & 25 deletions hypothesis-python/tests/cover/test_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
event,
note,
)
from hypothesis.errors import CleanupFailed, InvalidArgument
from hypothesis.errors import InvalidArgument
from hypothesis.internal.compat import ExceptionGroup
from hypothesis.internal.conjecture.data import ConjectureData as TD
from hypothesis.stateful import RuleBasedStateMachine, rule
from hypothesis.strategies import integers
Expand Down Expand Up @@ -73,44 +74,41 @@ def test_does_not_suppress_exceptions():


def test_suppresses_exceptions_in_teardown():
with capture_out() as o:
with pytest.raises(AssertionError):
with bc():
with pytest.raises(ValueError) as err:
with bc():

def foo():
raise ValueError()
def foo():
raise ValueError

cleanup(foo)
raise AssertionError
cleanup(foo)
raise AssertionError

assert "ValueError" in o.getvalue()
assert _current_build_context.value is None
assert isinstance(err.value, ValueError)
assert isinstance(err.value.__cause__, AssertionError)


def test_runs_multiple_cleanup_with_teardown():
with capture_out() as o:
with pytest.raises(AssertionError):
with bc():

def foo():
raise ValueError()
with pytest.raises(ExceptionGroup) as err:
with bc():

cleanup(foo)
def foo():
raise ValueError

def bar():
raise TypeError()
def bar():
raise TypeError

cleanup(foo)
cleanup(bar)
raise AssertionError
cleanup(foo)
cleanup(bar)
raise AssertionError

assert "ValueError" in o.getvalue()
assert "TypeError" in o.getvalue()
assert isinstance(err.value, ExceptionGroup)
assert isinstance(err.value.__cause__, AssertionError)
assert {type(e) for e in err.value.exceptions} == {ValueError, TypeError}
assert _current_build_context.value is None


def test_raises_error_if_cleanup_fails_but_block_does_not():
with pytest.raises(CleanupFailed):
with pytest.raises(ValueError):
with bc():

def foo():
Expand Down
2 changes: 2 additions & 0 deletions hypothesis-python/tests/cover/test_float_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def test_next_float_equal(func, val):
assert func(val) == val


# invalid order -> clamper is None:
@example(2.0, 1.0, 3.0)
# exponent comparisons:
@example(1, float_info.max, 0)
@example(1, float_info.max, 1)
Expand Down
1 change: 1 addition & 0 deletions hypothesis-python/tests/cover/test_stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,7 @@ def fail_eventually(self):

def test_steps_printed_despite_pytest_fail(capsys):
# Test for https://github.com/HypothesisWorks/hypothesis/issues/1372
@Settings(print_blob=False)
class RaisesProblem(RuleBasedStateMachine):
@rule()
def oops(self):
Expand Down
1 change: 0 additions & 1 deletion hypothesis-python/tests/cover/test_verbosity.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ def test_includes_progress_in_verbose_mode():
out = o.getvalue()
assert out
assert "Trying example: " in out
assert "Falsifying example: " in out


def test_prints_initial_attempts_on_find():
Expand Down

0 comments on commit 2ccf867

Please sign in to comment.