From beb18e33b4489b60a1dbcd2f4448fcf058c869b1 Mon Sep 17 00:00:00 2001 From: CI on behalf of the Hypothesis team Date: Sun, 26 Nov 2023 23:12:09 -0800 Subject: [PATCH 1/6] Update pinned dependencies --- hypothesis-python/RELEASE.rst | 4 ++++ .../vendor/tlds-alpha-by-domain.txt | 3 +-- requirements/coverage.txt | 6 ++--- requirements/fuzzing.txt | 14 +++++------ requirements/test.txt | 6 ++--- requirements/tools.txt | 24 +++++++++---------- tooling/src/hypothesistooling/__main__.py | 2 +- 7 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..cfbd34824b --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,4 @@ +RELEASE_TYPE: patch + +This patch updates our vendored `list of top-level domains `__, +which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. diff --git a/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt b/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt index 5402c8d9cb..98deba4045 100644 --- a/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt +++ b/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt @@ -1,4 +1,4 @@ -# Version 2023111800, Last Updated Sat Nov 18 07:07:01 2023 UTC +# Version 2023112500, Last Updated Sat Nov 25 07:07:01 2023 UTC AAA AARP ABB @@ -1234,7 +1234,6 @@ VIVO VLAANDEREN VN VODKA -VOLKSWAGEN VOLVO VOTE VOTING diff --git a/requirements/coverage.txt b/requirements/coverage.txt index d53cd3d5fc..e23c0a3ee1 100644 --- a/requirements/coverage.txt +++ b/requirements/coverage.txt @@ -20,7 +20,7 @@ coverage==7.3.2 # via -r requirements/coverage.in dpcontracts==0.6.0 # via -r requirements/coverage.in -exceptiongroup==1.1.3 ; python_version < "3.11" +exceptiongroup==1.2.0 ; python_version < "3.11" # via # hypothesis (hypothesis-python/setup.py) # pytest @@ -50,7 +50,7 @@ pandas==2.1.3 # via -r requirements/coverage.in pathspec==0.11.2 # via black -pexpect==4.8.0 +pexpect==4.9.0 # via -r requirements/test.in platformdirs==4.0.0 # via black @@ -62,7 +62,7 @@ pytest==7.4.3 # via # -r requirements/test.in # pytest-xdist -pytest-xdist==3.4.0 +pytest-xdist==3.5.0 # via -r requirements/test.in python-dateutil==2.8.2 # via diff --git a/requirements/fuzzing.txt b/requirements/fuzzing.txt index a2acba994c..b2096834ae 100644 --- a/requirements/fuzzing.txt +++ b/requirements/fuzzing.txt @@ -45,7 +45,7 @@ dash-table==5.0.0 # via dash dpcontracts==0.6.0 # via -r requirements/coverage.in -exceptiongroup==1.1.3 ; python_version < "3.11" +exceptiongroup==1.2.0 ; python_version < "3.11" # via # hypothesis # hypothesis (hypothesis-python/setup.py) @@ -58,11 +58,11 @@ flask==3.0.0 # via dash hypofuzz==23.7.1 # via -r requirements/fuzzing.in -hypothesis[cli]==6.89.0 +hypothesis[cli]==6.90.0 # via # hypofuzz # hypothesis -idna==3.4 +idna==3.6 # via requests importlib-metadata==6.8.0 # via dash @@ -107,7 +107,7 @@ pandas==2.1.3 # hypofuzz pathspec==0.11.2 # via black -pexpect==4.8.0 +pexpect==4.9.0 # via -r requirements/test.in platformdirs==4.0.0 # via black @@ -119,14 +119,14 @@ psutil==5.9.6 # via hypofuzz ptyprocess==0.7.0 # via pexpect -pygments==2.17.0 +pygments==2.17.2 # via rich pytest==7.4.3 # via # -r requirements/test.in # hypofuzz # pytest-xdist -pytest-xdist==3.4.0 +pytest-xdist==3.5.0 # via -r requirements/test.in python-dateutil==2.8.2 # via @@ -184,5 +184,5 @@ zipp==3.17.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==68.2.2 +setuptools==69.0.2 # via dash diff --git a/requirements/test.txt b/requirements/test.txt index 55d412ba49..efea209a06 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,7 +6,7 @@ # attrs==23.1.0 # via hypothesis (hypothesis-python/setup.py) -exceptiongroup==1.1.3 ; python_version < "3.11" +exceptiongroup==1.2.0 ; python_version < "3.11" # via # hypothesis (hypothesis-python/setup.py) # pytest @@ -16,7 +16,7 @@ iniconfig==2.0.0 # via pytest packaging==23.2 # via pytest -pexpect==4.8.0 +pexpect==4.9.0 # via -r requirements/test.in pluggy==1.3.0 # via pytest @@ -26,7 +26,7 @@ pytest==7.4.3 # via # -r requirements/test.in # pytest-xdist -pytest-xdist==3.4.0 +pytest-xdist==3.5.0 # via -r requirements/test.in sortedcontainers==2.4.0 # via hypothesis (hypothesis-python/setup.py) diff --git a/requirements/tools.txt b/requirements/tools.txt index 5d671f467a..7b6be6930e 100644 --- a/requirements/tools.txt +++ b/requirements/tools.txt @@ -63,7 +63,7 @@ docutils==0.18.1 # sphinx-rtd-theme dpcontracts==0.6.0 # via -r requirements/tools.in -exceptiongroup==1.1.3 ; python_version < "3.11" +exceptiongroup==1.2.0 ; python_version < "3.11" # via # hypothesis (hypothesis-python/setup.py) # ipython @@ -74,7 +74,7 @@ filelock==3.13.1 # via # tox # virtualenv -idna==3.4 +idna==3.6 # via requests imagesize==1.4.1 # via sphinx @@ -84,7 +84,7 @@ importlib-metadata==6.8.0 # twine iniconfig==2.0.0 # via pytest -ipython==8.17.2 +ipython==8.18.0 # via -r requirements/tools.in isort==5.12.0 # via shed @@ -116,7 +116,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.1.0 # via jaraco-classes -mypy==1.7.0 +mypy==1.7.1 # via -r requirements/tools.in mypy-extensions==1.0.0 # via @@ -139,13 +139,13 @@ parso==0.8.3 # via jedi pathspec==0.11.2 # via black -pexpect==4.8.0 +pexpect==4.9.0 # via ipython pip-tools==7.3.0 # via -r requirements/tools.in pkginfo==1.9.6 # via twine -platformdirs==3.11.0 +platformdirs==4.0.0 # via # black # tox @@ -164,7 +164,7 @@ pycparser==2.21 # via cffi pyflakes==3.1.0 # via autoflake -pygments==2.17.0 +pygments==2.17.2 # via # ipython # readme-renderer @@ -174,7 +174,7 @@ pyproject-api==1.6.1 # via tox pyproject-hooks==1.0.0 # via build -pyright==1.1.336 +pyright==1.1.337 # via -r requirements/tools.in pytest==7.4.3 # via -r requirements/tools.in @@ -285,7 +285,7 @@ types-pyopenssl==23.3.0.0 # via types-redis types-pytz==2023.3.1.1 # via -r requirements/tools.in -types-redis==4.6.0.10 +types-redis==4.6.0.11 # via -r requirements/tools.in typing-extensions==4.8.0 # via @@ -301,9 +301,9 @@ urllib3==2.1.0 # via # requests # twine -virtualenv==20.24.6 +virtualenv==20.24.7 # via tox -wcwidth==0.2.10 +wcwidth==0.2.12 # via prompt-toolkit wheel==0.41.3 # via pip-tools @@ -313,7 +313,7 @@ zipp==3.17.0 # The following packages are considered to be unsafe in a requirements file: pip==23.3.1 # via pip-tools -setuptools==68.2.2 +setuptools==69.0.2 # via # nodeenv # pip-tools diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py index 5087fff150..7df94b4d62 100644 --- a/tooling/src/hypothesistooling/__main__.py +++ b/tooling/src/hypothesistooling/__main__.py @@ -394,7 +394,7 @@ def run_tox(task, version, *args): "3.10": "3.10.13", "3.11": "3.11.6", "3.12": "3.12.0", - "3.13": "3.13.0a1", + "3.13": "3.13.0a2", "pypy3.8": "pypy3.8-7.3.11", "pypy3.9": "pypy3.9-7.3.13", "pypy3.10": "pypy3.10-7.3.13", From 39624425e3c604ae70da93e4e2ff6c7ad64eba13 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 26 Nov 2023 23:12:09 -0800 Subject: [PATCH 2/6] Note we support 3.12 now --- hypothesis-python/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hypothesis-python/setup.py b/hypothesis-python/setup.py index bf5242345b..5f5e82d1f0 100644 --- a/hypothesis-python/setup.py +++ b/hypothesis-python/setup.py @@ -117,6 +117,7 @@ def local_file(name): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Education :: Testing", From 8a67dc768ccf5a8adea41af11d975b3a7683a1e1 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 26 Nov 2023 23:12:09 -0800 Subject: [PATCH 3/6] refactor interesting_origin --- hypothesis-python/src/hypothesis/core.py | 4 +- .../src/hypothesis/internal/escalation.py | 53 ++++++++++++------- .../tests/cover/test_escalation.py | 20 ++++++- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py index 1c4cf9e144..723e3da646 100644 --- a/hypothesis-python/src/hypothesis/core.py +++ b/hypothesis-python/src/hypothesis/core.py @@ -80,10 +80,10 @@ from hypothesis.internal.conjecture.shrinker import sort_key from hypothesis.internal.entropy import deterministic_PRNG from hypothesis.internal.escalation import ( + InterestingOrigin, current_pytest_item, escalate_hypothesis_internal_error, format_exception, - get_interesting_origin, get_trimmed_traceback, ) from hypothesis.internal.healthcheck import fail_health_check @@ -970,7 +970,7 @@ def _execute_once_for_engine(self, data): self.failed_normally = True - interesting_origin = get_interesting_origin(e) + interesting_origin = InterestingOrigin.from_exception(e) if trace: # pragma: no cover # Trace collection is explicitly disabled under coverage. self.explain_traces[interesting_origin].add(trace) diff --git a/hypothesis-python/src/hypothesis/internal/escalation.py b/hypothesis-python/src/hypothesis/internal/escalation.py index 605ea52e97..24015e691e 100644 --- a/hypothesis-python/src/hypothesis/internal/escalation.py +++ b/hypothesis-python/src/hypothesis/internal/escalation.py @@ -11,10 +11,11 @@ import contextlib import os import sys +import textwrap import traceback from inspect import getframeinfo from pathlib import Path -from typing import Dict +from typing import Dict, NamedTuple, Type import hypothesis from hypothesis.errors import ( @@ -105,32 +106,46 @@ def get_trimmed_traceback(exception=None): return tb -def get_interesting_origin(exception): +class InterestingOrigin(NamedTuple): # The `interesting_origin` is how Hypothesis distinguishes between multiple # failures, for reporting and also to replay from the example database (even # if report_multiple_bugs=False). We traditionally use the exception type and # location, but have extracted this logic in order to see through `except ...:` # blocks and understand the __cause__ (`raise x from y`) or __context__ that # first raised an exception as well as PEP-654 exception groups. - tb = get_trimmed_traceback(exception) - if tb is None: + exc_type: Type[BaseException] + filename: str + lineno: int + context: "InterestingOrigin | tuple[()]" + group_elems: "tuple[InterestingOrigin, ...]" + + def __str__(self) -> str: + ctx = "" + if self.context: + ctx = textwrap.indent(f"\ncontext: {self.context}", prefix=" ") + group = "" + if self.group_elems: + chunks = "\n ".join(str(x) for x in self.group_elems) + group = textwrap.indent(f"\nchild exceptions:\n {chunks}", prefix=" ") + return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}" + + @classmethod + def from_exception(cls, exception: BaseException, /) -> "InterestingOrigin": filename, lineno = None, None - else: - filename, lineno, *_ = traceback.extract_tb(tb)[-1] - return ( - 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. - get_interesting_origin(exception.__context__) if exception.__context__ else (), - # We distinguish exception groups by the inner exceptions, as for __context__ - tuple( - map(get_interesting_origin, exception.exceptions) + if tb := get_trimmed_traceback(exception): + filename, lineno, *_ = traceback.extract_tb(tb)[-1] + 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 (), + # We distinguish exception groups by the inner exceptions, as for __context__ + tuple(map(cls.from_exception, exception.exceptions)) if isinstance(exception, BaseExceptionGroup) - else [] - ), - ) + else (), + ) current_pytest_item = DynamicVariable(None) diff --git a/hypothesis-python/tests/cover/test_escalation.py b/hypothesis-python/tests/cover/test_escalation.py index 212f462c51..2a176403f3 100644 --- a/hypothesis-python/tests/cover/test_escalation.py +++ b/hypothesis-python/tests/cover/test_escalation.py @@ -78,4 +78,22 @@ def test_errors_attribute_error(): def test_handles_null_traceback(): - esc.get_interesting_origin(Exception()) + esc.InterestingOrigin.from_exception(Exception()) + + +def test_handles_context(): + e = ValueError() + e.__context__ = KeyError() + origin = esc.InterestingOrigin.from_exception(e) + assert "ValueError at " in str(origin) + assert " context: " in str(origin) + assert "KeyError at " in str(origin) + + +def test_handles_groups(): + origin = esc.InterestingOrigin.from_exception( + BaseExceptionGroup("message", [ValueError("msg2")]) + ) + assert "ExceptionGroup at " in str(origin) + assert "child exception" in str(origin) + assert "ValueError at " in str(origin) From f032d008a98986dd15d3332a046462e64b0e82c2 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 26 Nov 2023 23:12:09 -0800 Subject: [PATCH 4/6] Additional coverage pattern --- hypothesis-python/.coveragerc | 1 + .../src/hypothesis/strategies/_internal/types.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/.coveragerc b/hypothesis-python/.coveragerc index 21234500ff..7732c620a2 100644 --- a/hypothesis-python/.coveragerc +++ b/hypothesis-python/.coveragerc @@ -28,3 +28,4 @@ exclude_lines = if TYPE_CHECKING: if sys\.version_info if "[\w\.]+" in sys\.modules: + if .+ := sys\.modules\.get\("[\w\.]+"\) diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/types.py b/hypothesis-python/src/hypothesis/strategies/_internal/types.py index d979081415..aba2e457a0 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/types.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/types.py @@ -275,7 +275,7 @@ def is_annotated_type(thing): def get_constraints_filter_map(): - if at := sys.modules.get("annotated_types"): # pragma: no branch + if at := sys.modules.get("annotated_types"): return { # Due to the order of operator.gt/ge/lt/le arguments, order is inversed: at.Gt: lambda constraint: partial(operator.lt, constraint.gt), @@ -290,7 +290,7 @@ def get_constraints_filter_map(): def _get_constraints(args: Tuple[Any, ...]) -> Iterator["at.BaseMetadata"]: - if at := sys.modules.get("annotated_types"): # pragma: no branch + if at := sys.modules.get("annotated_types"): for arg in args: if isinstance(arg, at.BaseMetadata): yield arg From 63c5298248aee86735b1ab216e660c79e4d95928 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 26 Nov 2023 23:12:09 -0800 Subject: [PATCH 5/6] get conjecturedata from runner --- hypothesis-python/src/hypothesis/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py index 723e3da646..b2298418c8 100644 --- a/hypothesis-python/src/hypothesis/core.py +++ b/hypothesis-python/src/hypothesis/core.py @@ -1037,7 +1037,9 @@ def run_engine(self): info = falsifying_example.extra_information fragments = [] - ran_example = ConjectureData.for_buffer(falsifying_example.buffer) + ran_example = runner.new_conjecture_data_for_buffer( + falsifying_example.buffer + ) ran_example.slice_comments = falsifying_example.slice_comments assert info.__expected_exception is not None try: From a3d96238be63409c845f9424a4e83df11dc417fc Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 26 Nov 2023 23:12:09 -0800 Subject: [PATCH 6/6] Events can have a payload now --- hypothesis-python/RELEASE.rst | 8 +++-- hypothesis-python/src/hypothesis/control.py | 29 +++++++++++++--- .../hypothesis/internal/conjecture/data.py | 10 ++---- .../hypothesis/internal/conjecture/engine.py | 21 ++---------- .../src/hypothesis/internal/escalation.py | 6 ++-- .../src/hypothesis/internal/lazyformat.py | 33 ------------------- .../strategies/_internal/datetime.py | 6 ++-- .../strategies/_internal/recursive.py | 13 ++------ .../strategies/_internal/strategies.py | 8 ++--- hypothesis-python/tests/common/debug.py | 22 +------------ .../tests/conjecture/test_engine.py | 5 --- .../tests/conjecture/test_test_data.py | 4 +-- hypothesis-python/tests/cover/test_control.py | 5 +++ .../tests/datetime/test_pytz_timezones.py | 25 +++++++------- .../recorded/hypothesis_module_magic.txt | 6 ++-- 15 files changed, 71 insertions(+), 130 deletions(-) delete mode 100644 hypothesis-python/src/hypothesis/internal/lazyformat.py diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst index cfbd34824b..a897390a67 100644 --- a/hypothesis-python/RELEASE.rst +++ b/hypothesis-python/RELEASE.rst @@ -1,4 +1,6 @@ -RELEASE_TYPE: patch +RELEASE_TYPE: minor -This patch updates our vendored `list of top-level domains `__, -which is used by the provisional :func:`~hypothesis.provisional.domains` strategy. +This release adds an optional ``payload`` argument to :func:`hypothesis.event`, +so that you can clearly express the difference between the label and the value +of an observation. :ref:`statistics` will still summarize it as a string, but +future observability options can preserve the distinction. diff --git a/hypothesis-python/src/hypothesis/control.py b/hypothesis-python/src/hypothesis/control.py index eb1768c565..c49dba2954 100644 --- a/hypothesis-python/src/hypothesis/control.py +++ b/hypothesis-python/src/hypothesis/control.py @@ -11,6 +11,7 @@ import math from collections import defaultdict from typing import NoReturn, Union +from weakref import WeakKeyDictionary from hypothesis import Verbosity, settings from hypothesis._settings import note_deprecation @@ -168,18 +169,38 @@ def note(value: str) -> None: report(value) -def event(value: str) -> None: - """Record an event that occurred this test. Statistics on number of test +def event(value: str, payload: Union[str, int, float] = "") -> None: + """Record an event that occurred during this test. Statistics on the number of test runs with each event will be reported at the end if you run Hypothesis in statistics reporting mode. - Events should be strings or convertible to them. + Event values should be strings or convertible to them. If an optional + payload is given, it will be included in the string for :ref:`statistics`. """ context = _current_build_context.value if context is None: raise InvalidArgument("Cannot make record events outside of a test") - context.data.note_event(value) + payload = _event_to_string(payload, (str, int, float)) + context.data.events[_event_to_string(value)] = payload + + +_events_to_strings: WeakKeyDictionary = WeakKeyDictionary() + + +def _event_to_string(event, allowed_types=str): + if isinstance(event, allowed_types): + return event + try: + return _events_to_strings[event] + except (KeyError, TypeError): + pass + result = str(event) + try: + _events_to_strings[event] = result + except TypeError: + pass + return result def target(observation: Union[int, float], *, label: str = "") -> Union[int, float]: diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 7eb5fbe080..2f086bf04f 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -20,7 +20,6 @@ Callable, Dict, FrozenSet, - Hashable, Iterable, Iterator, List, @@ -1367,7 +1366,7 @@ def __init__( self.testcounter = global_test_counter global_test_counter += 1 self.start_time = time.perf_counter() - self.events: "Union[Set[Hashable], FrozenSet[Hashable]]" = set() + self.events: Dict[str, Union[str, int, float]] = {} self.forced_indices: "Set[int]" = set() self.interesting_origin: Optional[InterestingOrigin] = None self.draw_times: "List[float]" = [] @@ -1615,10 +1614,6 @@ def stop_example(self, *, discard: bool = False) -> None: self.observer.kill_branch() - def note_event(self, event: Hashable) -> None: - assert isinstance(self.events, set) - self.events.add(event) - @property def examples(self) -> Examples: assert self.frozen @@ -1643,7 +1638,6 @@ def freeze(self) -> None: self.frozen = True self.buffer = bytes(self.buffer) - self.events = frozenset(self.events) self.observer.conclude_test(self.status, self.interesting_origin) def draw_bits(self, n: int, *, forced: Optional[int] = None) -> int: @@ -1729,7 +1723,7 @@ def mark_interesting( def mark_invalid(self, why: Optional[str] = None) -> NoReturn: if why is not None: - self.note_event(why) + self.events["invalid because"] = why self.conclude_test(Status.INVALID) def mark_overrun(self) -> NoReturn: diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py index 6944e127de..c5d33480e2 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py @@ -15,7 +15,6 @@ from datetime import timedelta from enum import Enum from random import Random, getrandbits -from weakref import WeakKeyDictionary import attr @@ -101,8 +100,6 @@ def __init__( self.statistics = {} self.stats_per_test_case = [] - self.events_to_strings = WeakKeyDictionary() - self.interesting_examples = {} # We use call_count because there may be few possible valid_examples. self.first_bug_found_at = None @@ -209,7 +206,9 @@ def test_function(self, data): "status": data.status.name.lower(), "runtime": data.finish_time - data.start_time, "drawtime": math.fsum(data.draw_times), - "events": sorted({self.event_to_string(e) for e in data.events}), + "events": sorted( + k if v == "" else f"{k}: {v}" for k, v in data.events.items() + ), } self.stats_per_test_case.append(call_stats) self.__data_cache[data.buffer] = data.as_result() @@ -1055,20 +1054,6 @@ def kill_branch(self): self.__data_cache[buffer] = result return result - def event_to_string(self, event): - if isinstance(event, str): - return event - try: - return self.events_to_strings[event] - except (KeyError, TypeError): - pass - result = str(event) - try: - self.events_to_strings[event] = result - except TypeError: - pass - return result - def passing_buffers(self, prefix=b""): """Return a collection of bytestrings which cause the test to pass. diff --git a/hypothesis-python/src/hypothesis/internal/escalation.py b/hypothesis-python/src/hypothesis/internal/escalation.py index 24015e691e..9261d2aefc 100644 --- a/hypothesis-python/src/hypothesis/internal/escalation.py +++ b/hypothesis-python/src/hypothesis/internal/escalation.py @@ -15,7 +15,7 @@ import traceback from inspect import getframeinfo from pathlib import Path -from typing import Dict, NamedTuple, Type +from typing import Dict, NamedTuple, Optional, Type import hypothesis from hypothesis.errors import ( @@ -114,8 +114,8 @@ class InterestingOrigin(NamedTuple): # blocks and understand the __cause__ (`raise x from y`) or __context__ that # first raised an exception as well as PEP-654 exception groups. exc_type: Type[BaseException] - filename: str - lineno: int + filename: Optional[str] + lineno: Optional[int] context: "InterestingOrigin | tuple[()]" group_elems: "tuple[InterestingOrigin, ...]" diff --git a/hypothesis-python/src/hypothesis/internal/lazyformat.py b/hypothesis-python/src/hypothesis/internal/lazyformat.py deleted file mode 100644 index 9a728c6380..0000000000 --- a/hypothesis-python/src/hypothesis/internal/lazyformat.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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/. - - -class lazyformat: - """A format string that isn't evaluated until it's needed.""" - - def __init__(self, format_string, *args): - self.__format_string = format_string - self.__args = args - - def __str__(self): - return self.__format_string % self.__args - - def __eq__(self, other): - return ( - isinstance(other, lazyformat) - and self.__format_string == other.__format_string - and self.__args == other.__args - ) - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self.__format_string) diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/datetime.py b/hypothesis-python/src/hypothesis/strategies/_internal/datetime.py index abf77c5026..f2c33fa8c5 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/datetime.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/datetime.py @@ -163,8 +163,10 @@ def draw_naive_datetime_and_combine(self, data, tz): try: return replace_tzinfo(dt.datetime(**result), timezone=tz) except (ValueError, OverflowError): - msg = "Failed to draw a datetime between %r and %r with timezone from %r." - data.mark_invalid(msg % (self.min_value, self.max_value, self.tz_strat)) + data.mark_invalid( + f"Failed to draw a datetime between {self.min_value!r} and " + f"{self.max_value!r} with timezone from {self.tz_strat!r}." + ) @defines_strategy(force_reusable_values=True) diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/recursive.py b/hypothesis-python/src/hypothesis/strategies/_internal/recursive.py index 7709b45460..cf7add9538 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/recursive.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/recursive.py @@ -12,7 +12,6 @@ from contextlib import contextmanager from hypothesis.errors import InvalidArgument -from hypothesis.internal.lazyformat import lazyformat from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.internal.validation import check_type from hypothesis.strategies._internal.strategies import ( @@ -112,13 +111,7 @@ def do_draw(self, data): with self.limited_base.capped(self.max_leaves): return data.draw(self.strategy) except LimitReached: - # Workaround for possible coverage bug - this branch is definitely - # covered but for some reason is showing up as not covered. - if count == 0: # pragma: no branch - data.note_event( - lazyformat( - "Draw for %r exceeded max_leaves and had to be retried", - self, - ) - ) + if count == 0: + msg = f"Draw for {self!r} exceeded max_leaves and had to be retried" + data.events[msg] = "" count += 1 diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py index 6b68a1d353..caf8d1ba9a 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py @@ -43,7 +43,6 @@ combine_labels, ) from hypothesis.internal.coverage import check_function -from hypothesis.internal.lazyformat import lazyformat from hypothesis.internal.reflection import ( get_pretty_function_description, is_identity_function, @@ -550,7 +549,7 @@ def do_filtered_draw(self, data): if element is not filter_not_satisfied: return element if not known_bad_indices: - FilteredStrategy.note_retried(self, data) + data.events[f"Retried draw from {self!r} to satisfy filter"] = "" known_bad_indices.add(i) # If we've tried all the possible elements, give up now. @@ -940,9 +939,6 @@ def do_draw(self, data: ConjectureData) -> Ex: data.mark_invalid(f"Aborted test because unable to satisfy {self!r}") raise NotImplementedError("Unreachable, for Mypy") - def note_retried(self, data): - data.note_event(lazyformat("Retried draw from %r to satisfy filter", self)) - def do_filtered_draw(self, data): for i in range(3): start_index = data.index @@ -954,7 +950,7 @@ def do_filtered_draw(self, data): else: data.stop_example(discard=True) if i == 0: - self.note_retried(data) + data.events[f"Retried draw from {self!r} to satisfy filter"] = "" # This is to guard against the case where we consume no data. # As long as we consume data, we'll eventually pass or raise. # But if we don't this could be an infinite loop. diff --git a/hypothesis-python/tests/common/debug.py b/hypothesis-python/tests/common/debug.py index 11dd9c2787..852119ec13 100644 --- a/hypothesis-python/tests/common/debug.py +++ b/hypothesis-python/tests/common/debug.py @@ -8,16 +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 hypothesis import ( - HealthCheck, - Phase, - Verbosity, - given, - settings as Settings, - strategies as st, -) +from hypothesis import HealthCheck, Phase, Verbosity, given, settings as Settings from hypothesis.errors import Found, NoSuchExample, Unsatisfiable -from hypothesis.internal.conjecture.data import ConjectureData, StopTest from hypothesis.internal.reflection import get_pretty_function_description from tests.common.utils import no_shrink @@ -107,15 +99,3 @@ def assert_examples(s): assert predicate(s), msg assert_examples() - - -def assert_can_trigger_event(strategy, predicate): - def test(buf): - data = ConjectureData.for_buffer(buf) - try: - data.draw(strategy) - except StopTest: - pass - return any(predicate(e) for e in data.events) - - find_any(st.binary(), test) diff --git a/hypothesis-python/tests/conjecture/test_engine.py b/hypothesis-python/tests/conjecture/test_engine.py index 1af2ad4bed..5f189fb6f8 100644 --- a/hypothesis-python/tests/conjecture/test_engine.py +++ b/hypothesis-python/tests/conjecture/test_engine.py @@ -1586,8 +1586,3 @@ def test(data): runner.cached_test_function([c]) assert runner.tree.is_exhausted - - -def test_can_convert_non_weakref_types_to_event_strings(): - runner = ConjectureRunner(lambda data: None) - runner.event_to_string(()) diff --git a/hypothesis-python/tests/conjecture/test_test_data.py b/hypothesis-python/tests/conjecture/test_test_data.py index 521ac95ed6..9f11331c74 100644 --- a/hypothesis-python/tests/conjecture/test_test_data.py +++ b/hypothesis-python/tests/conjecture/test_test_data.py @@ -111,7 +111,7 @@ def test_can_mark_invalid_with_why(): x.mark_invalid("some reason") assert x.frozen assert x.status == Status.INVALID - assert x.events == {"some reason"} + assert x.events == {"invalid because": "some reason"} class BoomStrategy(SearchStrategy): @@ -416,7 +416,7 @@ def test_trivial_before_force_agrees_with_trivial_after(): def test_events_are_noted(): d = ConjectureData.for_buffer(()) - d.note_event("hello") + d.events["hello"] = "" assert "hello" in d.events diff --git a/hypothesis-python/tests/cover/test_control.py b/hypothesis-python/tests/cover/test_control.py index 616595636f..95fd658e83 100644 --- a/hypothesis-python/tests/cover/test_control.py +++ b/hypothesis-python/tests/cover/test_control.py @@ -14,6 +14,7 @@ from hypothesis.control import ( BuildContext, _current_build_context, + _event_to_string, cleanup, current_build_context, currently_in_test_context, @@ -191,3 +192,7 @@ def step(self): test_currently_in_stateful_test = ContextMachine.TestCase + + +def test_can_convert_non_weakref_types_to_event_strings(): + _event_to_string(()) diff --git a/hypothesis-python/tests/datetime/test_pytz_timezones.py b/hypothesis-python/tests/datetime/test_pytz_timezones.py index 3a3db14bc6..aa0611c28d 100644 --- a/hypothesis-python/tests/datetime/test_pytz_timezones.py +++ b/hypothesis-python/tests/datetime/test_pytz_timezones.py @@ -15,16 +15,12 @@ import pytest from hypothesis import assume, given -from hypothesis.errors import InvalidArgument -from hypothesis.strategies import data, datetimes, just, sampled_from, times +from hypothesis.errors import InvalidArgument, StopTest +from hypothesis.internal.conjecture.data import ConjectureData +from hypothesis.strategies import binary, data, datetimes, just, sampled_from, times from hypothesis.strategies._internal.datetime import datetime_does_not_exist -from tests.common.debug import ( - assert_all_examples, - assert_can_trigger_event, - find_any, - minimal, -) +from tests.common.debug import assert_all_examples, find_any, minimal with warnings.catch_warnings(): if sys.version_info[:2] >= (3, 12): @@ -116,10 +112,15 @@ def test_time_bounds_must_be_naive(name, val): ], ) def test_can_trigger_error_in_draw_near_boundary(bound): - assert_can_trigger_event( - datetimes(**bound, timezones=timezones()), - lambda event: "Failed to draw a datetime" in event, - ) + def test(buf): + data = ConjectureData.for_buffer(buf) + try: + data.draw(datetimes(**bound, timezones=timezones())) + except StopTest: + pass + return "Failed to draw a datetime" in data.events.get("invalid because", "") + + find_any(binary(), test) @given(data(), datetimes(), datetimes()) diff --git a/hypothesis-python/tests/ghostwriter/recorded/hypothesis_module_magic.txt b/hypothesis-python/tests/ghostwriter/recorded/hypothesis_module_magic.txt index 3d14c2a62b..7f0c3ae2ec 100644 --- a/hypothesis-python/tests/ghostwriter/recorded/hypothesis_module_magic.txt +++ b/hypothesis-python/tests/ghostwriter/recorded/hypothesis_module_magic.txt @@ -14,9 +14,9 @@ def test_fuzz_assume(condition: object) -> None: hypothesis.assume(condition=condition) -@given(value=st.text()) -def test_fuzz_event(value: str) -> None: - hypothesis.event(value=value) +@given(value=st.text(), payload=st.one_of(st.floats(), st.integers(), st.text())) +def test_fuzz_event(value: str, payload: typing.Union[str, int, float]) -> None: + hypothesis.event(value=value, payload=payload) @given(