From 38c5208da4683f0fce2359422d78aca5bb9334e0 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Thu, 19 Sep 2024 15:53:09 +0200
Subject: [PATCH 01/10] first draft of handling marker exceptions wrapped in
exceptiongroup
---
hypothesis-python/src/hypothesis/core.py | 46 +++++++++++++++-
.../tests/cover/test_exceptiongroup.py | 54 +++++++++++++++++++
requirements/test.in | 1 +
3 files changed, 99 insertions(+), 2 deletions(-)
create mode 100644 hypothesis-python/tests/cover/test_exceptiongroup.py
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
index 304ba614d0..c696235e08 100644
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -61,6 +61,7 @@
FlakyFailure,
FlakyReplay,
Found,
+ Frozen,
HypothesisException,
HypothesisWarning,
InvalidArgument,
@@ -148,6 +149,8 @@
else: # pragma: no cover
EllipsisType = type(Ellipsis)
+if sys.version_info < (3, 11):
+ from exceptiongroup import ExceptionGroup
TestFunc = TypeVar("TestFunc", bound=Callable)
@@ -768,6 +771,45 @@ def execute(data, function):
return default_executor
+@contextlib.contextmanager
+def unwrap_exception_group():
+ def _flatten_group(excgroup: BaseExceptionGroup) -> list[BaseException]:
+ found_exceptions = []
+ for exc in excgroup.exceptions:
+ if isinstance(exc, BaseExceptionGroup):
+ found_exceptions.extend(_flatten_group(exc))
+ else:
+ found_exceptions.append(exc)
+ return found_exceptions
+
+ try:
+ yield
+ except BaseExceptionGroup as excgroup:
+ # Found? RewindRecursive? FlakyReplay?
+ marker_exceptions = (StopTest, UnsatisfiedAssumption)
+ frozen_exceptions, non_frozen_exceptions = excgroup.split(Frozen)
+ marker_exceptions, user_exceptions = non_frozen_exceptions.split(lambda e: isinstance(e, marker_exceptions))
+
+ # TODO: not a great variable name
+ if user_exceptions:
+ raise
+
+ if non_frozen_exceptions:
+ flattened_non_frozen_exceptions = _flatten_group(non_frozen_exceptions)
+ if len(flattened_non_frozen_exceptions) == 1:
+ raise flattened_non_frozen_exceptions[0] from None
+ # multiple non-frozen exceptions, re-raise the entire group
+ raise
+
+ flattened_frozen_exceptions = _flatten_group(frozen_exceptions)
+ # we only have frozen exceptions
+ if len(flattened_frozen_exceptions) == 1:
+ raise flattened_frozen_exceptions[0] from excgroup
+
+ # multiple frozen exceptions
+ # TODO: raise a group? The first one? None of them?
+ raise frozen_exceptions[0] from excgroup
+
class StateForActualGivenExecution:
def __init__(self, stuff, test, settings, random, wrapped_test):
self.test_runner = get_executor(stuff.selfy)
@@ -841,7 +883,7 @@ def execute_once(
@proxies(self.test)
def test(*args, **kwargs):
- with ensure_free_stackframes():
+ with unwrap_exception_group(), ensure_free_stackframes():
return self.test(*args, **kwargs)
else:
@@ -853,7 +895,7 @@ def test(*args, **kwargs):
arg_gctime = gc_cumulative_time()
start = time.perf_counter()
try:
- with ensure_free_stackframes():
+ with unwrap_exception_group(), ensure_free_stackframes():
result = self.test(*args, **kwargs)
finally:
finish = time.perf_counter()
diff --git a/hypothesis-python/tests/cover/test_exceptiongroup.py b/hypothesis-python/tests/cover/test_exceptiongroup.py
new file mode 100644
index 0000000000..ed4c6654f3
--- /dev/null
+++ b/hypothesis-python/tests/cover/test_exceptiongroup.py
@@ -0,0 +1,54 @@
+import sys
+import asyncio
+
+from trio.testing import RaisesGroup
+from hypothesis import errors, given, strategies as st
+import pytest
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import ExceptionGroup
+
+def test_exceptiongroup():
+ @given(st.data())
+ def test_function(data):
+ async def task(pred):
+ return data.draw(st.booleans().filter(pred))
+
+ async def _main():
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(task(bool))
+ tg.create_task(task(lambda _: False))
+ asyncio.run(_main())
+ with pytest.raises(errors.Unsatisfiable):
+ test_function()
+
+def test_exceptiongroup_nested():
+ @given(st.data())
+ def test_function(data):
+ async def task(pred):
+ return data.draw(st.booleans().filter(pred))
+
+ async def _main():
+ async with asyncio.TaskGroup():
+ async with asyncio.TaskGroup() as tg2:
+ tg2.create_task(task(bool))
+ tg2.create_task(task(lambda _: False))
+ asyncio.run(_main())
+ with pytest.raises(errors.Unsatisfiable):
+ test_function()
+
+
+def test_exceptiongroup_user_originated():
+ @given(st.data())
+ def test_function(data):
+ raise ExceptionGroup("foo", [ValueError(), ValueError()])
+ with RaisesGroup(ValueError, ValueError, match="foo"):
+ test_function()
+
+
+ @given(st.data())
+ def test_single_exc_group(data):
+ raise ExceptionGroup("important message for user", [ValueError()])
+
+ with RaisesGroup(ValueError, match="important message for user"):
+ test_single_exc_group()
diff --git a/requirements/test.in b/requirements/test.in
index 72e4385648..29920ff270 100644
--- a/requirements/test.in
+++ b/requirements/test.in
@@ -2,3 +2,4 @@ attrs==24.1.0 # too early for https://github.com/python-attrs/attrs/pull/1329
pexpect
pytest
pytest-xdist
+trio # for trio.testing.RaisesGroup, until pytest merges a version
From 057303a71e1c634eb897adc7addb472485688260 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 24 Sep 2024 16:58:58 +0200
Subject: [PATCH 02/10] progress, and new question marks
---
hypothesis-python/src/hypothesis/core.py | 38 +++---
.../tests/cover/test_exceptiongroup.py | 115 +++++++++++++++---
requirements/test.in | 1 -
3 files changed, 124 insertions(+), 30 deletions(-)
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
index c696235e08..405c5021a0 100644
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -149,9 +149,6 @@
else: # pragma: no cover
EllipsisType = type(Ellipsis)
-if sys.version_info < (3, 11):
- from exceptiongroup import ExceptionGroup
-
TestFunc = TypeVar("TestFunc", bound=Callable)
@@ -786,9 +783,19 @@ def _flatten_group(excgroup: BaseExceptionGroup) -> list[BaseException]:
yield
except BaseExceptionGroup as excgroup:
# Found? RewindRecursive? FlakyReplay?
- marker_exceptions = (StopTest, UnsatisfiedAssumption)
+ marker_exceptions = (StopTest, HypothesisException)
frozen_exceptions, non_frozen_exceptions = excgroup.split(Frozen)
- marker_exceptions, user_exceptions = non_frozen_exceptions.split(lambda e: isinstance(e, marker_exceptions))
+
+ # group only contains Frozen, reraise the group
+ # it doesn't matter what we raise, since any exceptions get disregarded
+ # and reraised as StopTest if data got frozen.
+ if non_frozen_exceptions is None:
+ raise
+ # in all other cases they are discarded
+
+ marker_exceptions, user_exceptions = non_frozen_exceptions.split(
+ lambda e: isinstance(e, marker_exceptions)
+ )
# TODO: not a great variable name
if user_exceptions:
@@ -798,17 +805,12 @@ def _flatten_group(excgroup: BaseExceptionGroup) -> list[BaseException]:
flattened_non_frozen_exceptions = _flatten_group(non_frozen_exceptions)
if len(flattened_non_frozen_exceptions) == 1:
raise flattened_non_frozen_exceptions[0] from None
- # multiple non-frozen exceptions, re-raise the entire group
- raise
-
- flattened_frozen_exceptions = _flatten_group(frozen_exceptions)
- # we only have frozen exceptions
- if len(flattened_frozen_exceptions) == 1:
- raise flattened_frozen_exceptions[0] from excgroup
+ # multiple marker exceptions. If we re-raise the whole group we break
+ # a bunch of logic so ....?
+ # is it possible to get multiple StopTest? Both a StopTest and
+ # HypothesisException? Multiple HypothesisException?
+ raise flattened_non_frozen_exceptions[0] from excgroup
- # multiple frozen exceptions
- # TODO: raise a group? The first one? None of them?
- raise frozen_exceptions[0] from excgroup
class StateForActualGivenExecution:
def __init__(self, stuff, test, settings, random, wrapped_test):
@@ -1128,6 +1130,9 @@ def _execute_once_for_engine(self, data: ConjectureData) -> None:
# This can happen if an error occurred in a finally
# block somewhere, suppressing our original StopTest.
# We raise a new one here to resume normal operation.
+ # TODO: this should maybe inspect the stacktrace to see that the above
+ # mentioned story is true? I.e. reraise as StopTest iff there is a
+ # StopTest somewhere in the tree of e.__context__
raise StopTest(data.testcounter) from e
else:
# The test failed by raising an exception, so we inform the
@@ -1291,6 +1296,9 @@ def run_engine(self):
ran_example.slice_comments = falsifying_example.slice_comments
tb = None
origin = None
+ # this assert is failing in test_exceptiongroup_multiple_stop and I have
+ # no clue why
+ assert info is not None
assert info._expected_exception is not None
try:
with with_reporter(fragments.append):
diff --git a/hypothesis-python/tests/cover/test_exceptiongroup.py b/hypothesis-python/tests/cover/test_exceptiongroup.py
index ed4c6654f3..f8935c65ca 100644
--- a/hypothesis-python/tests/cover/test_exceptiongroup.py
+++ b/hypothesis-python/tests/cover/test_exceptiongroup.py
@@ -1,14 +1,27 @@
-import sys
+# 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 asyncio
+from typing import Callable
-from trio.testing import RaisesGroup
-from hypothesis import errors, given, strategies as st
import pytest
-if sys.version_info < (3, 11):
- from exceptiongroup import ExceptionGroup
+from hypothesis import errors, given, strategies as st
+from hypothesis.internal.compat import ExceptionGroup
+from hypothesis.strategies import DataObject
+
+
+def test_exceptiongroup_discard_frozen():
+ """Basic test that raises Frozen+Unsatisfiable.
+ Frozen is thrown out, and Unsatisfiable is raised"""
-def test_exceptiongroup():
@given(st.data())
def test_function(data):
async def task(pred):
@@ -18,37 +31,111 @@ async def _main():
async with asyncio.TaskGroup() as tg:
tg.create_task(task(bool))
tg.create_task(task(lambda _: False))
+
asyncio.run(_main())
+
with pytest.raises(errors.Unsatisfiable):
test_function()
-def test_exceptiongroup_nested():
+
+def test_exceptiongroup_nested() -> None:
@given(st.data())
- def test_function(data):
- async def task(pred):
+ def test_function(data: DataObject) -> None:
+ async def task(pred: Callable[[bool], bool]) -> None:
return data.draw(st.booleans().filter(pred))
- async def _main():
+ async def _main() -> None:
async with asyncio.TaskGroup():
async with asyncio.TaskGroup() as tg2:
tg2.create_task(task(bool))
tg2.create_task(task(lambda _: False))
+
asyncio.run(_main())
+
with pytest.raises(errors.Unsatisfiable):
test_function()
-def test_exceptiongroup_user_originated():
+def test_exceptiongroup_user_originated() -> None:
@given(st.data())
def test_function(data):
raise ExceptionGroup("foo", [ValueError(), ValueError()])
- with RaisesGroup(ValueError, ValueError, match="foo"):
- test_function()
+ with pytest.raises(ExceptionGroup) as exc_info:
+ test_function()
+ e = exc_info.value
+ assert e.message == "foo"
+ assert isinstance(e, ExceptionGroup)
+ assert len(e.exceptions) == 2
+ assert all(isinstance(child_e, ValueError) for child_e in e.exceptions)
@given(st.data())
def test_single_exc_group(data):
raise ExceptionGroup("important message for user", [ValueError()])
- with RaisesGroup(ValueError, match="important message for user"):
+ with pytest.raises(ExceptionGroup) as exc_info:
test_single_exc_group()
+ e = exc_info.value
+ assert e.message == "important message for user"
+ assert isinstance(e, ExceptionGroup)
+ assert len(e.exceptions) == 1
+ assert isinstance(e.exceptions[0], ValueError)
+
+
+def test_exceptiongroup_multiple_stop() -> None:
+ # or well, I'm trying to get multiple StopTest, but instead I'm getting a strange
+ # AttributeError
+ @given(st.data())
+ def test_function(data):
+ async def task(d: DataObject) -> None:
+ d.conjecture_data.mark_interesting()
+
+ async def _main(d: DataObject) -> None:
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(task(d))
+ tg.create_task(task(d))
+
+ asyncio.run(_main(data))
+
+ test_function()
+
+
+# if intended, will probably remove this test, and either way probably belong somewhere else
+def test_frozen_things():
+ # Hypothesis reraises the TypeError as a StopTest, because the data is Frozen.
+ # Doesn't seem great, but I suppose it is intentional?
+ @given(st.data())
+ def foo(data):
+ data.conjecture_data.freeze()
+ raise TypeError("oops")
+
+ foo()
+
+
+# if above is intended, then this is supposedly also intended?
+def test_frozen_data_and_critical_user_exception():
+ @given(st.data())
+ def test_function(data):
+ data.conjecture_data.freeze()
+
+ async def task(d: DataObject) -> None:
+ d.draw(st.booleans())
+
+ async def task2() -> None:
+ raise TypeError("Critical error")
+
+ async def _main(d: DataObject) -> None:
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(task(d))
+ tg.create_task(task2())
+
+ asyncio.run(_main(data))
+
+ # does not raise anything, the TypeError is suppressed
+ test_function()
+ # with pytest.raises(ExceptionGroup) as exc_info:
+ # e = exc_info.value
+ # assert isinstance(e, ExceptionGroup)
+ # assert len(e.exceptions) == 2
+ # assert isinstance(e.exceptions[0], errors.Frozen)
+ # assert isinstance(e.exceptions[1], TypeError)
diff --git a/requirements/test.in b/requirements/test.in
index 29920ff270..72e4385648 100644
--- a/requirements/test.in
+++ b/requirements/test.in
@@ -2,4 +2,3 @@ attrs==24.1.0 # too early for https://github.com/python-attrs/attrs/pull/1329
pexpect
pytest
pytest-xdist
-trio # for trio.testing.RaisesGroup, until pytest merges a version
From dca18e4e8036af91cc074c444c97b4942007f010 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Wed, 25 Sep 2024 15:37:46 +0200
Subject: [PATCH 03/10] move test_exceptiongroup to nocover
---
hypothesis-python/tests/{cover => nocover}/test_exceptiongroup.py | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename hypothesis-python/tests/{cover => nocover}/test_exceptiongroup.py (100%)
diff --git a/hypothesis-python/tests/cover/test_exceptiongroup.py b/hypothesis-python/tests/nocover/test_exceptiongroup.py
similarity index 100%
rename from hypothesis-python/tests/cover/test_exceptiongroup.py
rename to hypothesis-python/tests/nocover/test_exceptiongroup.py
From 14be2023f5c6db94383c6d668a249befd4c7d345 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Wed, 25 Sep 2024 15:40:39 +0200
Subject: [PATCH 04/10] add cover tests that can run on list[BaseException]:
+def unwrap_exception_group() -> Generator[None, None, None]:
+ T = TypeVar("T", bound=BaseException)
+
+ def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]:
found_exceptions = []
for exc in excgroup.exceptions:
if isinstance(exc, BaseExceptionGroup):
@@ -782,8 +785,6 @@ def _flatten_group(excgroup: BaseExceptionGroup) -> list[BaseException]:
try:
yield
except BaseExceptionGroup as excgroup:
- # Found? RewindRecursive? FlakyReplay?
- marker_exceptions = (StopTest, HypothesisException)
frozen_exceptions, non_frozen_exceptions = excgroup.split(Frozen)
# group only contains Frozen, reraise the group
@@ -793,23 +794,34 @@ def _flatten_group(excgroup: BaseExceptionGroup) -> list[BaseException]:
raise
# in all other cases they are discarded
- marker_exceptions, user_exceptions = non_frozen_exceptions.split(
- lambda e: isinstance(e, marker_exceptions)
+ # Can RewindRecursive end up in this group?
+ _, user_exceptions = non_frozen_exceptions.split(
+ lambda e: isinstance(e, (StopTest, HypothesisException))
)
- # TODO: not a great variable name
- if user_exceptions:
+ # this might contain marker exceptions, or internal errors, but not frozen.
+ if user_exceptions is not None:
raise
- if non_frozen_exceptions:
- flattened_non_frozen_exceptions = _flatten_group(non_frozen_exceptions)
- if len(flattened_non_frozen_exceptions) == 1:
- raise flattened_non_frozen_exceptions[0] from None
- # multiple marker exceptions. If we re-raise the whole group we break
- # a bunch of logic so ....?
- # is it possible to get multiple StopTest? Both a StopTest and
- # HypothesisException? Multiple HypothesisException?
- raise flattened_non_frozen_exceptions[0] from excgroup
+ # single marker exception - reraise it
+ flattened_non_frozen_exceptions = _flatten_group(non_frozen_exceptions)
+ if len(flattened_non_frozen_exceptions) == 1:
+ raise flattened_non_frozen_exceptions[0] from None
+
+ # multiple marker exceptions. If we re-raise the whole group we break
+ # a bunch of logic so ....?
+ stoptests, non_stoptests = non_frozen_exceptions.split(StopTest)
+
+ # TODO: stoptest+hypothesisexception ...? Is it possible? If so, what do?
+
+ if non_stoptests:
+ # TODO: multiple marker exceptions is easy to produce, but the logic in the
+ # engine does not handle it... so we just reraise the first one for now.
+ raise _flatten_group(non_stoptests)[0] from None
+ assert stoptests is not None
+
+ # multiple stoptests: raising the one with the lowest testcounter
+ raise min(_flatten_group(stoptests), key=lambda s_e: s_e.testcounter)
class StateForActualGivenExecution:
@@ -1296,8 +1308,6 @@ def run_engine(self):
ran_example.slice_comments = falsifying_example.slice_comments
tb = None
origin = None
- # this assert is failing in test_exceptiongroup_multiple_stop and I have
- # no clue why
assert info is not None
assert info._expected_exception is not None
try:
diff --git a/hypothesis-python/src/hypothesis/internal/escalation.py b/hypothesis-python/src/hypothesis/internal/escalation.py
index 9c242ba0c2..2fd797882b 100644
--- a/hypothesis-python/src/hypothesis/internal/escalation.py
+++ b/hypothesis-python/src/hypothesis/internal/escalation.py
@@ -13,9 +13,10 @@
import sys
import textwrap
import traceback
+from functools import partial
from inspect import getframeinfo
from pathlib import Path
-from typing import Dict, NamedTuple, Optional, Type
+from typing import Dict, NamedTuple, Optional, Tuple, Type
import hypothesis
from hypothesis.errors import _Trimmable
@@ -107,20 +108,26 @@ def __str__(self) -> str:
return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}"
@classmethod
- def from_exception(cls, exception: BaseException, /) -> "InterestingOrigin":
+ def from_exception(cls, exception: BaseException, /, handled_exceptions: Tuple[BaseException,...] = ()) -> "InterestingOrigin":
+ if exception in handled_exceptions:
+ exception.__context__ = None
filename, lineno = None, None
if tb := get_trimmed_traceback(exception):
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
+
+ # Quick dirty fix for #4115
+ handled_exceptions=(*handled_exceptions, exception)
+
return cls(
type(exception),
filename,
lineno,
# Note that if __cause__ is set it is always equal to __context__, explicitly
# to support introspection when debugging, so we can use that unconditionally.
- cls.from_exception(exception.__context__) if exception.__context__ else (),
+ cls.from_exception(exception.__context__, handled_exceptions=handled_exceptions) if exception.__context__ else (),
# We distinguish exception groups by the inner exceptions, as for __context__
(
- tuple(map(cls.from_exception, exception.exceptions))
+ tuple(map(partial(cls.from_exception, handled_exceptions=handled_exceptions), exception.exceptions))
if isinstance(exception, BaseExceptionGroup)
else ()
),
diff --git a/hypothesis-python/tests/cover/test_exceptiongroup.py b/hypothesis-python/tests/cover/test_exceptiongroup.py
new file mode 100644
index 0000000000..58835b1965
--- /dev/null
+++ b/hypothesis-python/tests/cover/test_exceptiongroup.py
@@ -0,0 +1,102 @@
+# 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 pytest
+
+from hypothesis.internal.compat import BaseExceptionGroup, ExceptionGroup
+from hypothesis import errors, given, strategies as st
+from hypothesis.strategies import DataObject
+
+
+def test_discard_frozen() -> None:
+ @given(st.data())
+ def discard_frozen(data: DataObject) -> None:
+ # raising Frozen doesn't actually do anything, what matters is
+ # whether the data is frozen.
+ data.conjecture_data.freeze()
+ raise ExceptionGroup("", [errors.Frozen()])
+ discard_frozen()
+
+
+def test_discard_multiple_frozen() -> None:
+ @given(st.data())
+ def discard_multiple_frozen(data: DataObject) -> None:
+ data.conjecture_data.freeze()
+ raise ExceptionGroup("", [errors.Frozen(), errors.Frozen()])
+ discard_multiple_frozen()
+
+def test_user_error_and_frozen() -> None:
+ @given(st.data())
+ def user_error_and_frozen(data: DataObject) -> None:
+ raise ExceptionGroup("", [errors.Frozen(), TypeError()])
+ with pytest.raises(ExceptionGroup) as excinfo:
+ user_error_and_frozen()
+ e = excinfo.value
+ assert isinstance(e, ExceptionGroup)
+ assert len(e.exceptions) == 2
+ assert isinstance(e.exceptions[0], errors.Frozen)
+ assert isinstance(e.exceptions[1], TypeError)
+
+def test_user_error_and_stoptest() -> None:
+ # if the code base had "proper" handling of exceptiongroups, the StopTest would
+ # probably be handled by an except*.
+ # TODO: which I suppose is an argument in favor of stripping it??
+ @given(st.data())
+ def user_error_and_stoptest(data: DataObject) -> None:
+ raise BaseExceptionGroup("", [errors.StopTest(data.conjecture_data.testcounter), TypeError()])
+ with pytest.raises(BaseExceptionGroup) as excinfo:
+ user_error_and_stoptest()
+ e = excinfo.value
+ assert isinstance(e, BaseExceptionGroup)
+ assert len(e.exceptions) == 2
+ assert isinstance(e.exceptions[0], errors.StopTest)
+ assert isinstance(e.exceptions[1], TypeError)
+
+def test_lone_user_error() -> None:
+ # we don't want to unwrap exceptiongroups, since they might contain
+ # useful debugging info
+ @given(st.data())
+ def lone_user_error(data: DataObject) -> None:
+ raise ExceptionGroup("foo", [TypeError()])
+ with pytest.raises(ExceptionGroup) as excinfo:
+ lone_user_error()
+ e = excinfo.value
+ assert isinstance(e, ExceptionGroup)
+ assert len(e.exceptions) == 1
+ assert isinstance(e.exceptions[0], TypeError)
+
+def test_frozen_and_stoptest() -> None:
+ # frozen+stoptest => strip frozen and let engine handle StopTest
+ @given(st.data())
+ def frozen_and_stoptest(data: DataObject) -> None:
+ raise BaseExceptionGroup("", [errors.StopTest(data.conjecture_data.testcounter), errors.Frozen()])
+ frozen_and_stoptest()
+
+def test_multiple_stoptest_1() -> None:
+ # multiple stoptest, reraise the one with lowest testcounter
+ # I don't know if/how this can happen, nested tests perhaps??
+ @given(st.data())
+ def multiple_stoptest(data: DataObject) -> None:
+ c = data.conjecture_data.testcounter
+ raise BaseExceptionGroup("", [errors.StopTest(c), errors.StopTest(c+1)])
+ multiple_stoptest()
+
+def test_multiple_stoptest_2() -> None:
+ # the lower value is raised, which does not match data.conjecture_data.testcounter
+ # so it is not handled by the engine
+ @given(st.data())
+ def multiple_stoptest_2(data: DataObject) -> None:
+ c = data.conjecture_data.testcounter
+ raise BaseExceptionGroup("", [errors.StopTest(c), errors.StopTest(c-1)])
+
+ with pytest.raises(errors.StopTest) as excinfo:
+ multiple_stoptest_2()
+ e = excinfo.value
+ assert isinstance(e, errors.StopTest)
diff --git a/hypothesis-python/tests/nocover/test_exceptiongroup.py b/hypothesis-python/tests/nocover/test_exceptiongroup.py
index f8935c65ca..ba1c1b7495 100644
--- a/hypothesis-python/tests/nocover/test_exceptiongroup.py
+++ b/hypothesis-python/tests/nocover/test_exceptiongroup.py
@@ -9,14 +9,20 @@
# obtain one at https://mozilla.org/MPL/2.0/.
import asyncio
+import sys
from typing import Callable
import pytest
-from hypothesis import errors, given, strategies as st
+from hypothesis import errors, given, reject, strategies as st
from hypothesis.internal.compat import ExceptionGroup
from hypothesis.strategies import DataObject
+# this file is not typechecked by mypy, which only runs py310
+
+if sys.version_info < (3, 11):
+ pytest.skip("asyncio.TaskGroup not available on None:
@given(st.data())
def test_function(data: DataObject) -> None:
async def task(pred: Callable[[bool], bool]) -> None:
- return data.draw(st.booleans().filter(pred))
+ data.draw(st.booleans().filter(pred))
async def _main() -> None:
async with asyncio.TaskGroup():
@@ -83,12 +89,13 @@ def test_single_exc_group(data):
def test_exceptiongroup_multiple_stop() -> None:
- # or well, I'm trying to get multiple StopTest, but instead I'm getting a strange
- # AttributeError
@given(st.data())
def test_function(data):
async def task(d: DataObject) -> None:
- d.conjecture_data.mark_interesting()
+ ...
+ # idk how to intentionally raise StopTest here, without mucking
+ # around with internals *a lot* in a way that nobody would ever
+ # be expected to do
async def _main(d: DataObject) -> None:
async with asyncio.TaskGroup() as tg:
@@ -100,6 +107,53 @@ async def _main(d: DataObject) -> None:
test_function()
+def test_exceptiongroup_stop_and_hypothesisexception() -> None:
+ # idk how to intentionally raise StopTest
+ ...
+
+
+def test_exceptiongroup_multiple_hypothesisexception() -> None:
+ # multiple UnsatisfiedAssumption => first one is reraised => engine suppresses it
+
+ @given(st.integers(min_value=0, max_value=3))
+ def test_function(val: int) -> None:
+ async def task(value: int) -> None:
+ if value == 0:
+ reject()
+
+ async def _main(value: int) -> None:
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(task(value))
+ tg.create_task(task(value))
+
+ asyncio.run(_main(val))
+
+ test_function()
+
+
+def test_exceptiongroup_multiple_InvalidArgument() -> None:
+ # multiple InvalidArgument => only first one is reraised... which seems bad.
+ # But raising a group might break ghostwriter(?)
+
+ @given(st.data())
+ def test_function(data: DataObject) -> None:
+ async def task1(d: DataObject) -> None:
+ d.draw(st.integers(max_value=1, min_value=3))
+
+ async def task2(d: DataObject) -> None:
+ d.draw(st.integers(max_value=2, min_value=3))
+
+ async def _main(d: DataObject) -> None:
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(task1(d))
+ tg.create_task(task2(d))
+
+ asyncio.run(_main(data))
+
+ with pytest.raises(errors.InvalidArgument):
+ test_function()
+
+
# if intended, will probably remove this test, and either way probably belong somewhere else
def test_frozen_things():
# Hypothesis reraises the TypeError as a StopTest, because the data is Frozen.
@@ -139,3 +193,27 @@ async def _main(d: DataObject) -> None:
# assert len(e.exceptions) == 2
# assert isinstance(e.exceptions[0], errors.Frozen)
# assert isinstance(e.exceptions[1], TypeError)
+
+
+# FIXME: temporarily added while debugging #4115
+def test_recursive_exception():
+ @given(st.data())
+ def test_function(data):
+ try:
+ raise ExceptionGroup("", [ValueError()])
+ except ExceptionGroup as eg:
+ raise eg.exceptions[0] from None
+
+ with pytest.raises(ValueError):
+ test_function()
+
+
+def test_recursive_exception2():
+ @given(st.data())
+ def test_function(data):
+ k = ValueError()
+ k.__context__ = k
+ raise k
+
+ with pytest.raises(ValueError):
+ test_function()
From 1d73c19081d73d410c85eb9fb31238b313261b82 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 27 Sep 2024 12:43:12 +0200
Subject: [PATCH 05/10] fix coverage
---
.../src/hypothesis/internal/escalation.py | 26 ++++-
.../tests/cover/test_exceptiongroup.py | 95 +++++++++++++++++--
.../tests/nocover/test_exceptiongroup.py | 24 -----
3 files changed, 108 insertions(+), 37 deletions(-)
diff --git a/hypothesis-python/src/hypothesis/internal/escalation.py b/hypothesis-python/src/hypothesis/internal/escalation.py
index 2fd797882b..5fc48360fa 100644
--- a/hypothesis-python/src/hypothesis/internal/escalation.py
+++ b/hypothesis-python/src/hypothesis/internal/escalation.py
@@ -108,7 +108,12 @@ def __str__(self) -> str:
return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}"
@classmethod
- def from_exception(cls, exception: BaseException, /, handled_exceptions: Tuple[BaseException,...] = ()) -> "InterestingOrigin":
+ def from_exception(
+ cls,
+ exception: BaseException,
+ /,
+ handled_exceptions: Tuple[BaseException, ...] = (),
+ ) -> "InterestingOrigin":
if exception in handled_exceptions:
exception.__context__ = None
filename, lineno = None, None
@@ -116,7 +121,7 @@ def from_exception(cls, exception: BaseException, /, handled_exceptions: Tuple[B
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
# Quick dirty fix for #4115
- handled_exceptions=(*handled_exceptions, exception)
+ handled_exceptions = (*handled_exceptions, exception)
return cls(
type(exception),
@@ -124,10 +129,23 @@ def from_exception(cls, exception: BaseException, /, handled_exceptions: Tuple[B
lineno,
# Note that if __cause__ is set it is always equal to __context__, explicitly
# to support introspection when debugging, so we can use that unconditionally.
- cls.from_exception(exception.__context__, handled_exceptions=handled_exceptions) if exception.__context__ else (),
+ (
+ cls.from_exception(
+ exception.__context__, handled_exceptions=handled_exceptions
+ )
+ if exception.__context__
+ else ()
+ ),
# We distinguish exception groups by the inner exceptions, as for __context__
(
- tuple(map(partial(cls.from_exception, handled_exceptions=handled_exceptions), exception.exceptions))
+ tuple(
+ map(
+ partial(
+ cls.from_exception, handled_exceptions=handled_exceptions
+ ),
+ exception.exceptions,
+ )
+ )
if isinstance(exception, BaseExceptionGroup)
else ()
),
diff --git a/hypothesis-python/tests/cover/test_exceptiongroup.py b/hypothesis-python/tests/cover/test_exceptiongroup.py
index 58835b1965..d558d0c983 100644
--- a/hypothesis-python/tests/cover/test_exceptiongroup.py
+++ b/hypothesis-python/tests/cover/test_exceptiongroup.py
@@ -10,8 +10,8 @@
import pytest
-from hypothesis.internal.compat import BaseExceptionGroup, ExceptionGroup
from hypothesis import errors, given, strategies as st
+from hypothesis.internal.compat import BaseExceptionGroup, ExceptionGroup
from hypothesis.strategies import DataObject
@@ -22,6 +22,7 @@ def discard_frozen(data: DataObject) -> None:
# whether the data is frozen.
data.conjecture_data.freeze()
raise ExceptionGroup("", [errors.Frozen()])
+
discard_frozen()
@@ -30,12 +31,15 @@ def test_discard_multiple_frozen() -> None:
def discard_multiple_frozen(data: DataObject) -> None:
data.conjecture_data.freeze()
raise ExceptionGroup("", [errors.Frozen(), errors.Frozen()])
+
discard_multiple_frozen()
+
def test_user_error_and_frozen() -> None:
@given(st.data())
def user_error_and_frozen(data: DataObject) -> None:
raise ExceptionGroup("", [errors.Frozen(), TypeError()])
+
with pytest.raises(ExceptionGroup) as excinfo:
user_error_and_frozen()
e = excinfo.value
@@ -44,13 +48,17 @@ def user_error_and_frozen(data: DataObject) -> None:
assert isinstance(e.exceptions[0], errors.Frozen)
assert isinstance(e.exceptions[1], TypeError)
+
def test_user_error_and_stoptest() -> None:
# if the code base had "proper" handling of exceptiongroups, the StopTest would
# probably be handled by an except*.
# TODO: which I suppose is an argument in favor of stripping it??
@given(st.data())
def user_error_and_stoptest(data: DataObject) -> None:
- raise BaseExceptionGroup("", [errors.StopTest(data.conjecture_data.testcounter), TypeError()])
+ raise BaseExceptionGroup(
+ "", [errors.StopTest(data.conjecture_data.testcounter), TypeError()]
+ )
+
with pytest.raises(BaseExceptionGroup) as excinfo:
user_error_and_stoptest()
e = excinfo.value
@@ -59,12 +67,14 @@ def user_error_and_stoptest(data: DataObject) -> None:
assert isinstance(e.exceptions[0], errors.StopTest)
assert isinstance(e.exceptions[1], TypeError)
+
def test_lone_user_error() -> None:
# we don't want to unwrap exceptiongroups, since they might contain
# useful debugging info
@given(st.data())
def lone_user_error(data: DataObject) -> None:
raise ExceptionGroup("foo", [TypeError()])
+
with pytest.raises(ExceptionGroup) as excinfo:
lone_user_error()
e = excinfo.value
@@ -72,31 +82,98 @@ def lone_user_error(data: DataObject) -> None:
assert len(e.exceptions) == 1
assert isinstance(e.exceptions[0], TypeError)
+
+def test_nested_stoptest() -> None:
+ @given(st.data())
+ def nested_stoptest(data: DataObject) -> None:
+ raise BaseExceptionGroup(
+ "",
+ [
+ BaseExceptionGroup(
+ "", [errors.StopTest(data.conjecture_data.testcounter)]
+ )
+ ],
+ )
+
+ nested_stoptest()
+
+
def test_frozen_and_stoptest() -> None:
# frozen+stoptest => strip frozen and let engine handle StopTest
+ # actually.. I don't think I've got a live repo for this either.
@given(st.data())
def frozen_and_stoptest(data: DataObject) -> None:
- raise BaseExceptionGroup("", [errors.StopTest(data.conjecture_data.testcounter), errors.Frozen()])
+ raise BaseExceptionGroup(
+ "", [errors.StopTest(data.conjecture_data.testcounter), errors.Frozen()]
+ )
+
frozen_and_stoptest()
+
def test_multiple_stoptest_1() -> None:
# multiple stoptest, reraise the one with lowest testcounter
- # I don't know if/how this can happen, nested tests perhaps??
+ # TODO: I don't know if/how this can happen, nested tests perhaps??
@given(st.data())
def multiple_stoptest(data: DataObject) -> None:
c = data.conjecture_data.testcounter
- raise BaseExceptionGroup("", [errors.StopTest(c), errors.StopTest(c+1)])
+ raise BaseExceptionGroup("", [errors.StopTest(c), errors.StopTest(c + 1)])
+
multiple_stoptest()
+
def test_multiple_stoptest_2() -> None:
# the lower value is raised, which does not match data.conjecture_data.testcounter
# so it is not handled by the engine
@given(st.data())
def multiple_stoptest_2(data: DataObject) -> None:
c = data.conjecture_data.testcounter
- raise BaseExceptionGroup("", [errors.StopTest(c), errors.StopTest(c-1)])
+ raise BaseExceptionGroup("", [errors.StopTest(c), errors.StopTest(c - 1)])
- with pytest.raises(errors.StopTest) as excinfo:
+ with pytest.raises(errors.StopTest):
multiple_stoptest_2()
- e = excinfo.value
- assert isinstance(e, errors.StopTest)
+
+
+def test_stoptest_and_hypothesisexception() -> None:
+ # TODO: can this happen? If so what do?
+ # current code raises the first hypothesisexception and throws away stoptest
+ @given(st.data())
+ def stoptest_and_hypothesisexception(data: DataObject) -> None:
+ c = data.conjecture_data.testcounter
+ raise BaseExceptionGroup("", [errors.StopTest(c), errors.FlakyTest()])
+
+ with pytest.raises(errors.FlakyTest):
+ stoptest_and_hypothesisexception()
+
+
+def test_multiple_hypothesisexception() -> None:
+ # this can happen in several ways, see nocover/test_exceptiongroup.py
+ @given(st.data())
+ def stoptest_and_hypothesisexception(data: DataObject) -> None:
+ raise BaseExceptionGroup("", [errors.StopTest(c), errors.FlakyTest()])
+
+ with pytest.raises(errors.FlakyTest):
+ stoptest_and_hypothesisexception()
+
+
+# FIXME: temporarily added while debugging #4115
+def test_recursive_exception():
+ @given(st.data())
+ def test_function(data):
+ try:
+ raise ExceptionGroup("", [ValueError()])
+ except ExceptionGroup as eg:
+ raise eg.exceptions[0] from None
+
+ with pytest.raises(ValueError):
+ test_function()
+
+
+def test_recursive_exception2():
+ @given(st.data())
+ def test_function(data):
+ k = ValueError()
+ k.__context__ = k
+ raise k
+
+ with pytest.raises(ValueError):
+ test_function()
diff --git a/hypothesis-python/tests/nocover/test_exceptiongroup.py b/hypothesis-python/tests/nocover/test_exceptiongroup.py
index ba1c1b7495..0ed1e71fad 100644
--- a/hypothesis-python/tests/nocover/test_exceptiongroup.py
+++ b/hypothesis-python/tests/nocover/test_exceptiongroup.py
@@ -193,27 +193,3 @@ async def _main(d: DataObject) -> None:
# assert len(e.exceptions) == 2
# assert isinstance(e.exceptions[0], errors.Frozen)
# assert isinstance(e.exceptions[1], TypeError)
-
-
-# FIXME: temporarily added while debugging #4115
-def test_recursive_exception():
- @given(st.data())
- def test_function(data):
- try:
- raise ExceptionGroup("", [ValueError()])
- except ExceptionGroup as eg:
- raise eg.exceptions[0] from None
-
- with pytest.raises(ValueError):
- test_function()
-
-
-def test_recursive_exception2():
- @given(st.data())
- def test_function(data):
- k = ValueError()
- k.__context__ = k
- raise k
-
- with pytest.raises(ValueError):
- test_function()
From 4e06b62d51d3d05a8882d483209dc00c5d02bf2d Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 27 Sep 2024 13:04:01 +0200
Subject: [PATCH 06/10] uh, why didn't mypy complain about FlakyTest?
---
hypothesis-python/tests/cover/test_exceptiongroup.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/hypothesis-python/tests/cover/test_exceptiongroup.py b/hypothesis-python/tests/cover/test_exceptiongroup.py
index d558d0c983..42d6af70c5 100644
--- a/hypothesis-python/tests/cover/test_exceptiongroup.py
+++ b/hypothesis-python/tests/cover/test_exceptiongroup.py
@@ -139,9 +139,9 @@ def test_stoptest_and_hypothesisexception() -> None:
@given(st.data())
def stoptest_and_hypothesisexception(data: DataObject) -> None:
c = data.conjecture_data.testcounter
- raise BaseExceptionGroup("", [errors.StopTest(c), errors.FlakyTest()])
+ raise BaseExceptionGroup("", [errors.StopTest(c), errors.Flaky()])
- with pytest.raises(errors.FlakyTest):
+ with pytest.raises(errors.Flaky):
stoptest_and_hypothesisexception()
@@ -149,9 +149,10 @@ def test_multiple_hypothesisexception() -> None:
# this can happen in several ways, see nocover/test_exceptiongroup.py
@given(st.data())
def stoptest_and_hypothesisexception(data: DataObject) -> None:
- raise BaseExceptionGroup("", [errors.StopTest(c), errors.FlakyTest()])
+ c = data.conjecture_data.testcounter
+ raise BaseExceptionGroup("", [errors.StopTest(c), errors.Flaky()])
- with pytest.raises(errors.FlakyTest):
+ with pytest.raises(errors.Flaky):
stoptest_and_hypothesisexception()
From e7e09bde27654769453282d4888e86f7f60841a9 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 27 Sep 2024 15:12:22 +0200
Subject: [PATCH 07/10] add future annotations to unbreak py38
---
hypothesis-python/src/hypothesis/core.py | 52 ++++++++-----------
.../tests/cover/test_exceptiongroup.py | 2 +
.../tests/nocover/test_exceptiongroup.py | 2 +
3 files changed, 26 insertions(+), 30 deletions(-)
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
index 03ad3ef07b..bb9bd36346 100644
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -9,6 +9,7 @@
# obtain one at https://mozilla.org/MPL/2.0/.
"""This module provides the core primitives of Hypothesis, such as given."""
+from __future__ import annotations
import base64
import contextlib
@@ -34,12 +35,7 @@
Coroutine,
Generator,
Hashable,
- List,
- Optional,
- Tuple,
- Type,
TypeVar,
- Union,
overload,
)
from unittest import TestCase
@@ -179,7 +175,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
if not (args or kwargs):
raise InvalidArgument("An example must provide at least one argument")
- self.hypothesis_explicit_examples: List[Example] = []
+ self.hypothesis_explicit_examples: list[Example] = []
self._this_example = Example(tuple(args), kwargs)
def __call__(self, test: TestFunc) -> TestFunc:
@@ -193,10 +189,8 @@ def xfail(
condition: bool = True, # noqa: FBT002
*,
reason: str = "",
- raises: Union[
- Type[BaseException], Tuple[Type[BaseException], ...]
- ] = BaseException,
- ) -> "example":
+ raises: type[BaseException] | tuple[type[BaseException], ...] = BaseException,
+ ) -> example:
"""Mark this example as an expected failure, similarly to
:obj:`pytest.mark.xfail(strict=True) `.
@@ -265,7 +259,7 @@ def test(x):
)
return self
- def via(self, whence: str, /) -> "example":
+ def via(self, whence: str, /) -> example:
"""Attach a machine-readable label noting whence this example came.
The idea is that tools will be able to add ``@example()`` cases for you, e.g.
@@ -770,7 +764,7 @@ def execute(data, function):
@contextlib.contextmanager
-def unwrap_exception_group() -> Generator[None, None, None]:
+def unwrap_exception_group() -> Generator[None]:
T = TypeVar("T", bound=BaseException)
def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]:
@@ -1205,7 +1199,7 @@ def _execute_once_for_engine(self, data: ConjectureData) -> None:
self._timing_features = {}
def _deliver_information_message(
- self, *, type: str, title: str, content: Union[str, dict]
+ self, *, type: str, title: str, content: str | dict
) -> None:
deliver_json_blob(
{
@@ -1467,7 +1461,7 @@ class HypothesisHandle:
@property
def fuzz_one_input(
self,
- ) -> Callable[[Union[bytes, bytearray, memoryview, BinaryIO]], Optional[bytes]]:
+ ) -> Callable[[bytes | bytearray | memoryview | BinaryIO], bytes | None]:
"""Run the test as a fuzz target, driven with the `buffer` of bytes.
Returns None if buffer invalid for the strategy, canonical pruned
@@ -1488,7 +1482,7 @@ def fuzz_one_input(
def given(
_: EllipsisType, /
) -> Callable[
- [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[[], None]
+ [Callable[..., Coroutine[Any, Any, None] | None]], Callable[[], None]
]: # pragma: no cover
...
@@ -1497,26 +1491,24 @@ def given(
def given(
*_given_arguments: SearchStrategy[Any],
) -> Callable[
- [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
+ [Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None]
]: # pragma: no cover
...
@overload
def given(
- **_given_kwargs: Union[SearchStrategy[Any], EllipsisType],
+ **_given_kwargs: SearchStrategy[Any] | EllipsisType,
) -> Callable[
- [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
+ [Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None]
]: # pragma: no cover
...
def given(
- *_given_arguments: Union[SearchStrategy[Any], EllipsisType],
- **_given_kwargs: Union[SearchStrategy[Any], EllipsisType],
-) -> Callable[
- [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
-]:
+ *_given_arguments: SearchStrategy[Any] | EllipsisType,
+ **_given_kwargs: SearchStrategy[Any] | EllipsisType,
+) -> Callable[[Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None]]:
"""A decorator for turning a test function that accepts arguments into a
randomized test.
@@ -1785,7 +1777,7 @@ def wrapped_test(*arguments, **kwargs):
raise SKIP_BECAUSE_NO_EXAMPLES
def _get_fuzz_target() -> (
- Callable[[Union[bytes, bytearray, memoryview, BinaryIO]], Optional[bytes]]
+ Callable[[bytes | bytearray | memoryview | BinaryIO], bytes | None]
):
# Because fuzzing interfaces are very performance-sensitive, we use a
# somewhat more complicated structure here. `_get_fuzz_target()` is
@@ -1817,8 +1809,8 @@ def _get_fuzz_target() -> (
minimal_failures: dict = {}
def fuzz_one_input(
- buffer: Union[bytes, bytearray, memoryview, BinaryIO]
- ) -> Optional[bytes]:
+ buffer: bytes | bytearray | memoryview | BinaryIO,
+ ) -> bytes | None:
# This inner part is all that the fuzzer will actually run,
# so we keep it as small and as fast as possible.
if isinstance(buffer, io.IOBase):
@@ -1872,9 +1864,9 @@ def find(
specifier: SearchStrategy[Ex],
condition: Callable[[Any], bool],
*,
- settings: Optional[Settings] = None,
- random: Optional[Random] = None,
- database_key: Optional[bytes] = None,
+ settings: Settings | None = None,
+ random: Random | None = None,
+ database_key: bytes | None = None,
) -> Ex:
"""Returns the minimal example from the given strategy ``specifier`` that
matches the predicate function ``condition``."""
@@ -1897,7 +1889,7 @@ def find(
)
specifier.validate()
- last: List[Ex] = []
+ last: list[Ex] = []
@settings
@given(specifier)
diff --git a/hypothesis-python/tests/cover/test_exceptiongroup.py b/hypothesis-python/tests/cover/test_exceptiongroup.py
index 42d6af70c5..517974c82f 100644
--- a/hypothesis-python/tests/cover/test_exceptiongroup.py
+++ b/hypothesis-python/tests/cover/test_exceptiongroup.py
@@ -8,6 +8,8 @@
# 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/.
+from __future__ import annotations
+
import pytest
from hypothesis import errors, given, strategies as st
diff --git a/hypothesis-python/tests/nocover/test_exceptiongroup.py b/hypothesis-python/tests/nocover/test_exceptiongroup.py
index 0ed1e71fad..6d82c03a31 100644
--- a/hypothesis-python/tests/nocover/test_exceptiongroup.py
+++ b/hypothesis-python/tests/nocover/test_exceptiongroup.py
@@ -8,6 +8,8 @@
# 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/.
+from __future__ import annotations
+
import asyncio
import sys
from typing import Callable
From 51a6f752243446d72ba68f95cf1f3940b3bab18f Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 1 Oct 2024 15:26:43 +0200
Subject: [PATCH 08/10] undo future annotations import, not sure what to do
about __cause__/__context__
---
hypothesis-python/src/hypothesis/core.py | 60 ++++++++++++++----------
1 file changed, 36 insertions(+), 24 deletions(-)
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
index bb9bd36346..774798a5df 100644
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -9,8 +9,6 @@
# obtain one at https://mozilla.org/MPL/2.0/.
"""This module provides the core primitives of Hypothesis, such as given."""
-from __future__ import annotations
-
import base64
import contextlib
import datetime
@@ -35,7 +33,12 @@
Coroutine,
Generator,
Hashable,
+ List,
+ Optional,
+ Tuple,
+ Type,
TypeVar,
+ Union,
overload,
)
from unittest import TestCase
@@ -146,6 +149,7 @@
else: # pragma: no cover
EllipsisType = type(Ellipsis)
+
TestFunc = TypeVar("TestFunc", bound=Callable)
@@ -175,7 +179,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
if not (args or kwargs):
raise InvalidArgument("An example must provide at least one argument")
- self.hypothesis_explicit_examples: list[Example] = []
+ self.hypothesis_explicit_examples: List[Example] = []
self._this_example = Example(tuple(args), kwargs)
def __call__(self, test: TestFunc) -> TestFunc:
@@ -189,8 +193,10 @@ def xfail(
condition: bool = True, # noqa: FBT002
*,
reason: str = "",
- raises: type[BaseException] | tuple[type[BaseException], ...] = BaseException,
- ) -> example:
+ raises: Union[
+ Type[BaseException], Tuple[Type[BaseException], ...]
+ ] = BaseException,
+ ) -> "example":
"""Mark this example as an expected failure, similarly to
:obj:`pytest.mark.xfail(strict=True) `.
@@ -259,7 +265,7 @@ def test(x):
)
return self
- def via(self, whence: str, /) -> example:
+ def via(self, whence: str, /) -> "example":
"""Attach a machine-readable label noting whence this example came.
The idea is that tools will be able to add ``@example()`` cases for you, e.g.
@@ -800,7 +806,10 @@ def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]:
# single marker exception - reraise it
flattened_non_frozen_exceptions = _flatten_group(non_frozen_exceptions)
if len(flattened_non_frozen_exceptions) == 1:
- raise flattened_non_frozen_exceptions[0] from None
+ e = flattened_non_frozen_exceptions[0]
+ # preserve the cause of the original exception to not hinder debugging
+ # note that __context__ is still lost though
+ raise e from e.__cause__
# multiple marker exceptions. If we re-raise the whole group we break
# a bunch of logic so ....?
@@ -811,7 +820,8 @@ def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]:
if non_stoptests:
# TODO: multiple marker exceptions is easy to produce, but the logic in the
# engine does not handle it... so we just reraise the first one for now.
- raise _flatten_group(non_stoptests)[0] from None
+ e = _flatten_group(non_stoptests)[0]
+ raise e from e.__cause__
assert stoptests is not None
# multiple stoptests: raising the one with the lowest testcounter
@@ -1199,7 +1209,7 @@ def _execute_once_for_engine(self, data: ConjectureData) -> None:
self._timing_features = {}
def _deliver_information_message(
- self, *, type: str, title: str, content: str | dict
+ self, *, type: str, title: str, content: Union[str, dict]
) -> None:
deliver_json_blob(
{
@@ -1461,7 +1471,7 @@ class HypothesisHandle:
@property
def fuzz_one_input(
self,
- ) -> Callable[[bytes | bytearray | memoryview | BinaryIO], bytes | None]:
+ ) -> Callable[[Union[bytes, bytearray, memoryview, BinaryIO]], Optional[bytes]]:
"""Run the test as a fuzz target, driven with the `buffer` of bytes.
Returns None if buffer invalid for the strategy, canonical pruned
@@ -1482,7 +1492,7 @@ def fuzz_one_input(
def given(
_: EllipsisType, /
) -> Callable[
- [Callable[..., Coroutine[Any, Any, None] | None]], Callable[[], None]
+ [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[[], None]
]: # pragma: no cover
...
@@ -1491,24 +1501,26 @@ def given(
def given(
*_given_arguments: SearchStrategy[Any],
) -> Callable[
- [Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None]
+ [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
]: # pragma: no cover
...
@overload
def given(
- **_given_kwargs: SearchStrategy[Any] | EllipsisType,
+ **_given_kwargs: Union[SearchStrategy[Any], EllipsisType],
) -> Callable[
- [Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None]
+ [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
]: # pragma: no cover
...
def given(
- *_given_arguments: SearchStrategy[Any] | EllipsisType,
- **_given_kwargs: SearchStrategy[Any] | EllipsisType,
-) -> Callable[[Callable[..., Coroutine[Any, Any, None] | None]], Callable[..., None]]:
+ *_given_arguments: Union[SearchStrategy[Any], EllipsisType],
+ **_given_kwargs: Union[SearchStrategy[Any], EllipsisType],
+) -> Callable[
+ [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
+]:
"""A decorator for turning a test function that accepts arguments into a
randomized test.
@@ -1777,7 +1789,7 @@ def wrapped_test(*arguments, **kwargs):
raise SKIP_BECAUSE_NO_EXAMPLES
def _get_fuzz_target() -> (
- Callable[[bytes | bytearray | memoryview | BinaryIO], bytes | None]
+ Callable[[Union[bytes, bytearray, memoryview, BinaryIO]], Optional[bytes]]
):
# Because fuzzing interfaces are very performance-sensitive, we use a
# somewhat more complicated structure here. `_get_fuzz_target()` is
@@ -1809,8 +1821,8 @@ def _get_fuzz_target() -> (
minimal_failures: dict = {}
def fuzz_one_input(
- buffer: bytes | bytearray | memoryview | BinaryIO,
- ) -> bytes | None:
+ buffer: Union[bytes, bytearray, memoryview, BinaryIO]
+ ) -> Optional[bytes]:
# This inner part is all that the fuzzer will actually run,
# so we keep it as small and as fast as possible.
if isinstance(buffer, io.IOBase):
@@ -1864,9 +1876,9 @@ def find(
specifier: SearchStrategy[Ex],
condition: Callable[[Any], bool],
*,
- settings: Settings | None = None,
- random: Random | None = None,
- database_key: bytes | None = None,
+ settings: Optional[Settings] = None,
+ random: Optional[Random] = None,
+ database_key: Optional[bytes] = None,
) -> Ex:
"""Returns the minimal example from the given strategy ``specifier`` that
matches the predicate function ``condition``."""
@@ -1889,7 +1901,7 @@ def find(
)
specifier.validate()
- last: list[Ex] = []
+ last: List[Ex] = []
@settings
@given(specifier)
From d3fd6d21aa06b565fa2222c0532cb868b1475461 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 1 Oct 2024 15:41:51 +0200
Subject: [PATCH 09/10] and Generator
---
hypothesis-python/src/hypothesis/core.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
index 774798a5df..a98822c8bf 100644
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -770,7 +770,7 @@ def execute(data, function):
@contextlib.contextmanager
-def unwrap_exception_group() -> Generator[None]:
+def unwrap_exception_group() -> Generator[None, None, None]:
T = TypeVar("T", bound=BaseException)
def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]:
From 3144dddf54089afa9381122acb9eef5123c7b28b Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 1 Oct 2024 16:00:10 +0200
Subject: [PATCH 10/10] type checking/linting is a mess
---
hypothesis-python/src/hypothesis/core.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
index a98822c8bf..354c9615aa 100644
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -773,8 +773,8 @@ def execute(data, function):
def unwrap_exception_group() -> Generator[None, None, None]:
T = TypeVar("T", bound=BaseException)
- def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]:
- found_exceptions = []
+ def _flatten_group(excgroup: BaseExceptionGroup[T]) -> List[T]:
+ found_exceptions: List[T] = []
for exc in excgroup.exceptions:
if isinstance(exc, BaseExceptionGroup):
found_exceptions.extend(_flatten_group(exc))
@@ -804,7 +804,7 @@ def _flatten_group(excgroup: BaseExceptionGroup[T]) -> list[T]:
raise
# single marker exception - reraise it
- flattened_non_frozen_exceptions = _flatten_group(non_frozen_exceptions)
+ flattened_non_frozen_exceptions: List[BaseException] = _flatten_group(non_frozen_exceptions)
if len(flattened_non_frozen_exceptions) == 1:
e = flattened_non_frozen_exceptions[0]
# preserve the cause of the original exception to not hinder debugging