Skip to content

Commit 33094e2

Browse files
committed
add PrimitiveProvider.on_observation
1 parent 8b0a35b commit 33094e2

File tree

5 files changed

+110
-4
lines changed

5 files changed

+110
-4
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
RELEASE_TYPE: patch
2+
3+
Add |PrimitiveProvider.on_observation| to the internal :ref:`alternative backends <alternative-backends-internals>` interface.

hypothesis-python/docs/conf.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,18 @@ def setup(app):
276276
.. |.example()| replace:: :func:`.example() <hypothesis.strategies.SearchStrategy.example>`
277277
278278
.. |PrimitiveProvider| replace:: :class:`~hypothesis.internal.conjecture.providers.PrimitiveProvider`
279-
.. |PrimitiveProvider.realize| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.realize`
280279
.. |PrimitiveProvider.draw_integer| replace:: \
281280
:func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_integer`
282281
.. |PrimitiveProvider.draw_boolean| replace:: \
283282
:func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_boolean`
284283
.. |PrimitiveProvider.draw_float| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_float`
285284
.. |PrimitiveProvider.draw_string| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_string`
286285
.. |PrimitiveProvider.draw_bytes| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.draw_bytes`
286+
.. |PrimitiveProvider.realize| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.realize`
287+
.. |PrimitiveProvider.on_observation| replace:: \
288+
:func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.on_observation`
289+
.. |PrimitiveProvider.per_test_case_context_manager| replace:: \
290+
:func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.per_test_case_context_manager`
287291
288292
.. |AVAILABLE_PROVIDERS| replace:: :data:`~hypothesis.internal.conjecture.providers.AVAILABLE_PROVIDERS`
289293
.. |TESTCASE_CALLBACKS| replace:: :data:`~hypothesis.internal.observability.TESTCASE_CALLBACKS`

hypothesis-python/src/hypothesis/internal/conjecture/engine.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import time
1616
from collections import defaultdict
1717
from collections.abc import Generator, Sequence
18-
from contextlib import contextmanager, suppress
18+
from contextlib import AbstractContextManager, contextmanager, nullcontext, suppress
1919
from dataclasses import dataclass, field
2020
from datetime import timedelta
2121
from enum import Enum
@@ -68,6 +68,7 @@
6868
from hypothesis.internal.conjecture.shrinker import Shrinker, ShrinkPredicateT, sort_key
6969
from hypothesis.internal.escalation import InterestingOrigin
7070
from hypothesis.internal.healthcheck import fail_health_check
71+
from hypothesis.internal.observability import Observation, with_observation_callback
7172
from hypothesis.reporting import base_report, report
7273

7374
#: The maximum number of times the shrinker will reduce the complexity of a failing
@@ -830,8 +831,34 @@ def debug_data(self, data: Union[ConjectureData, ConjectureResult]) -> None:
830831
f"{', ' + data.output if data.output else ''}"
831832
)
832833

834+
def observe_for_provider(self) -> AbstractContextManager:
835+
def on_observation(observation: Observation) -> None:
836+
assert isinstance(self.provider, PrimitiveProvider)
837+
# only fire if we actually used that provider to generate this observation
838+
if not self._switch_to_hypothesis_provider:
839+
self.provider.on_observation(observation)
840+
841+
# adding this callback enables observability. Use a nullcontext
842+
# if the backend won't use observability.
843+
return (
844+
with_observation_callback(on_observation)
845+
if (
846+
self.settings.backend != "hypothesis"
847+
# only for lifetime = "test_function" providers
848+
and isinstance(self.provider, PrimitiveProvider)
849+
# and the provider class overrode the default
850+
# (see https://github.com/python/mypy/issues/14123 for type ignore)
851+
and self.provider.on_observation.__func__ # type: ignore
852+
is not PrimitiveProvider.on_observation
853+
)
854+
else nullcontext()
855+
)
856+
833857
def run(self) -> None:
834-
with local_settings(self.settings):
858+
with (
859+
local_settings(self.settings),
860+
self.observe_for_provider(),
861+
):
835862
try:
836863
self._run()
837864
except RunIsComplete:

hypothesis-python/src/hypothesis/internal/conjecture/providers.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
next_up,
6363
)
6464
from hypothesis.internal.intervalsets import IntervalSet
65-
from hypothesis.internal.observability import InfoObservationType
65+
from hypothesis.internal.observability import InfoObservationType, Observation
6666

6767
if TYPE_CHECKING:
6868
from typing import TypeAlias
@@ -544,6 +544,37 @@ def observe_information_messages(
544544
assert lifetime in ("test_case", "test_function")
545545
yield from []
546546

547+
def on_observation(self, observation: Observation) -> None: # noqa: B027
548+
"""
549+
Called at the end of each test case which uses this provider, with the same
550+
``observation["type"] == "test_case"`` observation that is passed to
551+
other callbacks in |TESTCASE_CALLBACKS|. This method is not called with
552+
``observation["type"] in {"info", "alert", "error"}`` observations.
553+
554+
Calls to this method are guaranteed to alternate with calls to
555+
|PrimitiveProvider.per_test_case_context_manager|. For example:
556+
557+
.. code-block:: python
558+
559+
# test function starts
560+
per_test_case_context_manager()
561+
on_observation()
562+
per_test_case_context_manager()
563+
on_observation()
564+
...
565+
# test function ends
566+
567+
Note that |PrimitiveProvider.on_observation| will not be called for test
568+
cases which did not use this provider during generation, for example
569+
during |Phase.reuse| or |Phase.shrink|, or because Hypothesis switched
570+
to the standard Hypothesis backend after this backend raised too many
571+
|BackendCannotProceed| exceptions.
572+
573+
By default, observability will not be enabled for backends which do not
574+
override this method. By overriding this method, any test which sets
575+
|settings.backend| to this provider will automatically enable observability.
576+
"""
577+
547578
def span_start(self, label: int, /) -> None: # noqa: B027 # non-abstract noop
548579
"""Marks the beginning of a semantically meaningful span of choices.
549580

hypothesis-python/tests/conjecture/test_alt_backend.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
)
4747
from hypothesis.internal.floats import SIGNALING_NAN
4848
from hypothesis.internal.intervalsets import IntervalSet
49+
from hypothesis.internal.observability import TESTCASE_CALLBACKS, Observation
4950

5051
from tests.common.debug import minimal
5152
from tests.common.utils import (
@@ -718,3 +719,43 @@ def test_replay_choices():
718719
# trivial covering test
719720
provider = TrivialProvider(None)
720721
provider.replay_choices([1])
722+
723+
724+
class ObservationProvider(TrivialProvider):
725+
def __init__(self, conjecturedata: "ConjectureData", /) -> None:
726+
super().__init__(conjecturedata)
727+
# calls to per_test_case_context_manager and on_observation alternate,
728+
# starting with per_test_case_context_manager
729+
self.expected = "per_test_case_context_manager"
730+
731+
@contextmanager
732+
def per_test_case_context_manager(self):
733+
assert self.expected == "per_test_case_context_manager"
734+
self.expected = "on_observation"
735+
yield
736+
737+
def on_observation(self, observation: Observation) -> None:
738+
assert self.expected == "on_observation"
739+
self.expected = "per_test_case_context_manager"
740+
741+
742+
def test_on_observation_alternates():
743+
with temp_register_backend("observation", ObservationProvider):
744+
745+
@given(st.integers())
746+
@settings(backend="observation")
747+
def f(n):
748+
pass
749+
750+
f()
751+
752+
753+
def test_on_observation_no_override():
754+
with temp_register_backend("observation", TrivialProvider):
755+
756+
@given(st.integers())
757+
@settings(backend="observation")
758+
def f(n):
759+
assert TESTCASE_CALLBACKS == []
760+
761+
f()

0 commit comments

Comments
 (0)