Skip to content

Commit 4eb82da

Browse files
committed
stabilize observability
1 parent 7fd52ba commit 4eb82da

17 files changed

Lines changed: 288 additions & 109 deletions

File tree

hypothesis-python/RELEASE.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
RELEASE_TYPE: minor
2+
3+
This release adds |settings.observability|, which can be used to enable :ref:`observability <observability>`.
4+
5+
When observability is enabled, Hypothesis writes data about each test case to the ``.hypothesis/observed`` directory in an analysis-ready `jsonlines <https://jsonlines.org/>`_ format. This data is intended to help users who want to dive deep into understanding their tests. It's also intended for people building tools or research on top of Hypothesis.
6+
7+
Observability can be controlled in two ways:
8+
9+
* via the new |settings.observability| argument,
10+
* or via the ``HYPOTHESIS_OBSERVABILITY`` environment variable.
11+
12+
See :ref:`Configuring observability <observability-configuration>` for details.
13+
14+
If you use VSCode, we recommend the `Tyche <https://github.com/tyche-pbt/tyche-extension>`__ extension, a PBT-specific visualization tool designed for Hypothesis's observability interface.

hypothesis-python/docs/changelog.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -774,11 +774,11 @@ Further improve the performance of the constants-collection feature introduced i
774774
6.135.3 - 2025-06-08
775775
--------------------
776776

777-
This release adds the experimental and unstable |OBSERVABILITY_CHOICES| option for :ref:`observability <observability>`. If set, the choice sequence is included in ``metadata.choice_nodes``, and choice sequence spans are included in ``metadata.choice_spans``.
777+
This release adds the experimental and unstable ``OBSERVABILITY_CHOICES`` option for :ref:`observability <observability>`. If set, the choice sequence is included in ``metadata.choice_nodes``, and choice sequence spans are included in ``metadata.choice_spans``.
778778

779779
These are relatively low-level implementation detail of Hypothesis, and are exposed in observability for users building tools or research on top of Hypothesis. See |PrimitiveProvider| for more details about the choice sequence and choice spans.
780780

781-
We are actively working towards a better interface for this. Feel free to use |OBSERVABILITY_CHOICES| to experiment, but don't rely on it yet!
781+
We are actively working towards a better interface for this. Feel free to use ``OBSERVABILITY_CHOICES`` to experiment, but don't rely on it yet!
782782

783783
.. _v6.135.2:
784784

hypothesis-python/docs/prolog.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
.. |settings.suppress_health_check| replace:: :obj:`settings.suppress_health_check <hypothesis.settings.suppress_health_check>`
2727
.. |settings.stateful_step_count| replace:: :obj:`settings.stateful_step_count <hypothesis.settings.stateful_step_count>`
2828
.. |settings.backend| replace:: :obj:`settings.backend <hypothesis.settings.backend>`
29+
.. |settings.observability| replace:: :obj:`settings.observability <hypothesis.settings.observability>`
2930

3031
.. |~settings.max_examples| replace:: :obj:`~hypothesis.settings.max_examples`
3132
.. |~settings.database| replace:: :obj:`~hypothesis.settings.database`
@@ -38,6 +39,7 @@
3839
.. |~settings.suppress_health_check| replace:: :obj:`~hypothesis.settings.suppress_health_check`
3940
.. |~settings.stateful_step_count| replace:: :obj:`~hypothesis.settings.stateful_step_count`
4041
.. |~settings.backend| replace:: :obj:`~hypothesis.settings.backend`
42+
.. |~settings.observability| replace:: :obj:`settings.observability <hypothesis.settings.observability>`
4143

4244
.. |settings.register_profile| replace:: :func:`~hypothesis.settings.register_profile`
4345
.. |settings.get_profile| replace:: :func:`~hypothesis.settings.get_profile`
@@ -65,6 +67,8 @@
6567
.. |Verbosity.normal| replace:: :obj:`Verbosity.normal <hypothesis.Verbosity.normal>`
6668
.. |Verbosity.quiet| replace:: :obj:`Verbosity.quiet <hypothesis.Verbosity.quiet>`
6769

70+
.. |ObservabilitySettings| replace:: :obj:`ObservabilitySettings <hypothesis.ObservabilitySettings>`
71+
6872
.. |HypothesisException| replace:: :obj:`HypothesisException <hypothesis.errors.HypothesisException>`
6973
.. |HypothesisDeprecationWarning| replace:: :obj:`HypothesisDeprecationWarning <hypothesis.errors.HypothesisDeprecationWarning>`
7074
.. |Flaky| replace:: :obj:`Flaky <hypothesis.errors.Flaky>`
@@ -147,7 +151,6 @@
147151
.. |with_observability_callback| replace:: :data:`~hypothesis.internal.observability.with_observability_callback`
148152
.. |observability_enabled| replace:: :data:`~hypothesis.internal.observability.observability_enabled`
149153
.. |TESTCASE_CALLBACKS| replace:: :data:`~hypothesis.internal.observability.TESTCASE_CALLBACKS`
150-
.. |OBSERVABILITY_CHOICES| replace:: :data:`~hypothesis.internal.observability.OBSERVABILITY_CHOICES`
151154
.. |BUFFER_SIZE| replace:: :data:`~hypothesis.internal.conjecture.engine.BUFFER_SIZE`
152155
.. |MAX_SHRINKS| replace:: :data:`~hypothesis.internal.conjecture.engine.MAX_SHRINKS`
153156
.. |MAX_SHRINKING_SECONDS| replace:: :data:`~hypothesis.internal.conjecture.engine.MAX_SHRINKING_SECONDS`

hypothesis-python/docs/reference/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ Settings
174174
.. autoclass:: hypothesis.Verbosity
175175
:members:
176176

177+
.. autoclass:: hypothesis.ObservabilitySettings
178+
177179
.. autoclass:: hypothesis.HealthCheck
178180
:undoc-members:
179181
:inherited-members:

hypothesis-python/docs/reference/integrations.rst

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,10 @@ If you're interested in similar questions, `drop me an email`_!
7575
Observability
7676
-------------
7777

78-
.. note::
78+
.. tip::
7979

8080
The `Tyche <https://github.com/tyche-pbt/tyche-extension>`__ VSCode extension provides an in-editor UI for observability results generated by Hypothesis. If you want to *view* observability results, rather than programmatically consume or display them, we recommend using Tyche.
8181

82-
.. warning::
83-
84-
This feature is experimental, and could have breaking changes or even be removed
85-
without notice. Try it out, let us know what you think, but don't rely on it
86-
just yet!
87-
88-
8982
Motivation
9083
~~~~~~~~~~
9184

@@ -108,24 +101,23 @@ debuggers such as `rr <https://rr-project.org/>`__ or `pytrace <https://pytrace.
108101
because there's no good way to compare multiple traces from these tools and their
109102
Python support is relatively immature.
110103

104+
.. _observability-configuration:
111105

112-
Configuration
113-
~~~~~~~~~~~~~
106+
Configuring observability
107+
~~~~~~~~~~~~~~~~~~~~~~~~~
114108

115-
If you set the ``HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY`` environment variable,
116-
Hypothesis will log various observations to jsonlines files in the
109+
The standard way to configure observability is with |settings.observability|.
110+
111+
Alternatively, observability can be configured by setting the ``HYPOTHESIS_OBSERVABILITY`` environment variable. If ``HYPOTHESIS_OBSERVABILITY`` is set to one of ``True``, ``true``, or ``1``, |settings.observability| defaults to ``True``. Note that unlike |settings.observability|, ``HYPOTHESIS_OBSERVABILITY`` only configures whether observability is enabled or disabled, not additional options like |ObservabilitySettings|.
112+
113+
When observability is enabled, Hypothesis will log various observations to jsonlines files in the
117114
``.hypothesis/observed/`` directory. You can load and explore these with e.g.
118115
:func:`pd.read_json(".hypothesis/observed/*_testcases.jsonl", lines=True) <pandas.read_json>`,
119116
or by using the :pypi:`sqlite-utils` and :pypi:`datasette` libraries::
120117

121118
sqlite-utils insert testcases.db testcases .hypothesis/observed/*_testcases.jsonl --nl --flatten
122119
datasette serve testcases.db
123120

124-
If you are experiencing a significant slow-down, you can try setting
125-
``HYPOTHESIS_EXPERIMENTAL_OBSERVABILITY_NOCOVER`` instead; this will disable coverage information
126-
collection. This should not be necessary on Python 3.12 or later, where coverage collection is very fast.
127-
128-
129121
Collecting more information
130122
^^^^^^^^^^^^^^^^^^^^^^^^^^^
131123

@@ -181,7 +173,7 @@ While the observability format is agnostic to the property-based testing library
181173
Choices metadata
182174
++++++++++++++++
183175

184-
These additional metadata elements are included in ``metadata`` (as e.g. ``metadata["choice_nodes"]`` or ``metadata["choice_spans"]``), if and only if |OBSERVABILITY_CHOICES| is set.
176+
These additional metadata elements are included in ``metadata`` (as e.g. ``metadata["choice_nodes"]`` or ``metadata["choice_spans"]``), if and only if observability is configured to include choices (see |ObservabilitySettings|).
185177

186178
.. jsonschema:: ./schema_metadata_choices.json
187179
:hide_key: /additionalProperties, /type

hypothesis-python/docs/reference/internals.rst

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ Observability
3636
.. autofunction:: hypothesis.internal.observability.observability_enabled
3737

3838
.. autodata:: hypothesis.internal.observability.TESTCASE_CALLBACKS
39-
.. autodata:: hypothesis.internal.observability.OBSERVABILITY_COLLECT_COVERAGE
40-
.. autodata:: hypothesis.internal.observability.OBSERVABILITY_CHOICES
4139

4240
Engine constants
4341
----------------

hypothesis-python/docs/reference/schema_metadata_choices.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"properties": {
44
"choice_nodes": {
55
"type": ["array", "null"],
6-
"description": ".. warning::\n\n EXPERIMENTAL AND UNSTABLE. This attribute may change format or disappear without warning.\n\nThe sequence of choices made during this test case. This includes the choice value, as well as its constraints and whether it was forced or not.\n\nOnly present if |OBSERVABILITY_CHOICES| is ``True``.\n\n.. note::\n\n The choice sequence is a relatively low-level implementation detail of Hypothesis, and is exposed in observability for users building tools or research on top of Hypothesis. See |PrimitiveProvider| for more details about the choice sequence.",
6+
"description": ".. warning::\n\n EXPERIMENTAL AND UNSTABLE. This attribute may change format without warning.\n\nThe sequence of choices made during this test case. This includes the choice value, as well as its constraints and whether it was forced or not.\n\n.. note::\n\n Only present if observability is configured to include choices (see |ObservabilitySettings|).\n\n.. note::\n\n The choice sequence is a relatively low-level implementation detail of Hypothesis, and is exposed in observability for users building tools or research on top of Hypothesis. See |PrimitiveProvider| for more details about the choice sequence.",
77
"items": {
88
"type": "object",
99
"properties": {
@@ -31,7 +31,7 @@
3131
"choice_spans": {
3232
"type": "array",
3333
"items": {"type": "array"},
34-
"description": ".. warning::\n\n EXPERIMENTAL AND UNSTABLE. This attribute may change format or disappear without warning.\n\nThe semantically-meaningful spans of the choice sequence of this test case.\n\nEach span has the format ``[label, start, end, discarded]``, where:\n\n* ``label`` is an opaque integer-value string shared by all spans drawn from a particular strategy.\n* ``start`` and ``end`` are indices into the choice sequence for this span, such that ``choices[start:end]`` are the corresponding choices.\n* ``discarded`` is a boolean indicating whether this span was discarded (see |PrimitiveProvider.span_end|).\n\nOnly present if |OBSERVABILITY_CHOICES| is ``True``.\n\n.. note::\n\n Spans are a relatively low-level implementation detail of Hypothesis, and are exposed in observability for users building tools or research on top of Hypothesis. See |PrimitiveProvider| (and particularly |PrimitiveProvider.span_start| and |PrimitiveProvider.span_end|) for more details about spans."
34+
"description": ".. warning::\n\n EXPERIMENTAL AND UNSTABLE. This attribute may change format without warning.\n\nThe semantically-meaningful spans of the choice sequence of this test case.\n\nEach span has the format ``[label, start, end, discarded]``, where:\n\n* ``label`` is an opaque integer-value string shared by all spans drawn from a particular strategy.\n* ``start`` and ``end`` are indices into the choice sequence for this span, such that ``choices[start:end]`` are the corresponding choices.\n* ``discarded`` is a boolean indicating whether this span was discarded (see |PrimitiveProvider.span_end|).\n\n.. note::\n\n Only present if observability is configured to include choices (see |ObservabilitySettings|).\n\n.. note::\n\n Spans are a relatively low-level implementation detail of Hypothesis, and are exposed in observability for users building tools or research on top of Hypothesis. See |PrimitiveProvider| (and particularly |PrimitiveProvider.span_start| and |PrimitiveProvider.span_end|) for more details about spans."
3535
}
3636
},
3737
"required": ["traceback", "reproduction_decorator", "predicates", "backend", "sys.argv", "os.getpid()", "imported_at", "data_status", "interesting_origin", "choice_nodes", "choice_spans"],

hypothesis-python/docs/reference/schema_observations.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
},
3737
"coverage": {
3838
"type": ["object", "null"],
39-
"description": "Mapping of filename to list of covered line numbers, if coverage information is available, or None if not. Hypothesis deliberately omits stdlib and site-packages code.",
39+
"description": "Mapping of filename to list of covered line numbers, if coverage information is available, or None if not. Hypothesis deliberately omits stdlib and site-packages code.\n\n.. note::\n\n Only present if observability is configured to include coverage (see |ObservabilitySettings|).",
4040
"additionalProperties": {
4141
"type": "array",
4242
"items": {"type": "integer", "minimum": 1},

hypothesis-python/src/hypothesis/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717

1818
import _hypothesis_globals
1919

20-
from hypothesis._settings import HealthCheck, Phase, Verbosity, settings
20+
from hypothesis._settings import (
21+
HealthCheck,
22+
Phase,
23+
Verbosity,
24+
settings,
25+
)
2126
from hypothesis.control import (
2227
assume,
2328
currently_in_test_context,
@@ -30,11 +35,13 @@
3035
from hypothesis.entry_points import run
3136
from hypothesis.internal.detection import is_hypothesis_test
3237
from hypothesis.internal.entropy import register_random
38+
from hypothesis.internal.observability import ObservabilitySettings
3339
from hypothesis.utils.conventions import infer
3440
from hypothesis.version import __version__, __version_info__
3541

3642
__all__ = [
3743
"HealthCheck",
44+
"ObservabilitySettings",
3845
"Phase",
3946
"Verbosity",
4047
"__version__",

hypothesis-python/src/hypothesis/_settings.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
InvalidArgument,
3636
)
3737
from hypothesis.internal.conjecture.providers import AVAILABLE_PROVIDERS
38+
from hypothesis.internal.observability import (
39+
ObservabilitySettings,
40+
_ObservabilitySettings,
41+
envvar_observability,
42+
)
3843
from hypothesis.internal.reflection import get_pretty_function_description
3944
from hypothesis.internal.validation import check_type, try_convert
4045
from hypothesis.utils.conventions import not_set
@@ -58,6 +63,7 @@
5863
"deadline",
5964
"print_blob",
6065
"backend",
66+
"observability",
6167
]
6268

6369

@@ -529,6 +535,14 @@ def _validate_backend(backend: str) -> str:
529535
return backend
530536

531537

538+
def _validate_observability(observability: Any) -> _ObservabilitySettings:
539+
check_type((bool, ObservabilitySettings), observability, name="observability")
540+
541+
if isinstance(observability, bool):
542+
return _ObservabilitySettings(enabled=observability)
543+
return _ObservabilitySettings(enabled=True, options=observability)
544+
545+
532546
class settingsMeta(type):
533547
def __init__(cls, *args, **kwargs):
534548
super().__init__(*args, **kwargs)
@@ -569,7 +583,8 @@ class settings(metaclass=settingsMeta):
569583
|~settings.max_examples|, |~settings.derandomize|, |~settings.database|,
570584
|~settings.verbosity|, |~settings.phases|, |~settings.stateful_step_count|,
571585
|~settings.report_multiple_bugs|, |~settings.suppress_health_check|,
572-
|~settings.deadline|, |~settings.print_blob|, and |~settings.backend|.
586+
|~settings.deadline|, |~settings.print_blob|, |~settings.backend|, and
587+
|~settings.observability|.
573588
574589
A settings object can be applied as a decorator to a test function, in which
575590
case that test function will use those settings. A test may only have one
@@ -618,7 +633,7 @@ class settings(metaclass=settingsMeta):
618633
"default",
619634
max_examples=100,
620635
derandomize=False,
621-
database=not_set, # see settings.database for the default database
636+
database=not_set, # see settings.database for default behavior
622637
verbosity=Verbosity.normal,
623638
phases=tuple(Phase),
624639
stateful_step_count=50,
@@ -627,6 +642,7 @@ class settings(metaclass=settingsMeta):
627642
deadline=duration(milliseconds=200),
628643
print_blob=False,
629644
backend="hypothesis",
645+
observability=False,
630646
)
631647
632648
ci = settings.register_profile(
@@ -673,6 +689,7 @@ def __init__(
673689
deadline: int | float | datetime.timedelta | None = not_set, # type: ignore
674690
print_blob: bool = not_set, # type: ignore
675691
backend: str = not_set, # type: ignore
692+
observability: bool | ObservabilitySettings = not_set, # type: ignore
676693
) -> None:
677694
self._in_definition = True
678695

@@ -699,10 +716,12 @@ def __init__(
699716
if derandomize is not_set # type: ignore
700717
else _validate_choices("derandomize", derandomize, choices=[True, False])
701718
)
719+
702720
if database is not not_set: # type: ignore
703721
database = _validate_database(database)
704722
self._database = database
705723
self._cached_database = None
724+
706725
self._verbosity = (
707726
self._fallback.verbosity # type: ignore
708727
if verbosity is not_set # type: ignore
@@ -746,6 +765,15 @@ def __init__(
746765
else _validate_backend(backend)
747766
)
748767

768+
if observability is not_set: # type: ignore
769+
self._observability = (
770+
envvar_observability
771+
if self._fallback is None
772+
else self._fallback.observability
773+
)
774+
else:
775+
self._observability = _validate_observability(observability)
776+
749777
self._in_definition = False
750778

751779
@property
@@ -1027,6 +1055,35 @@ def backend(self):
10271055
"""
10281056
return self._backend
10291057

1058+
@property
1059+
def observability(self):
1060+
"""
1061+
Controls the :ref:`observability <observability>` behavior of Hypothesis.
1062+
1063+
Observability may be enabled or disabled by passing ``True`` or ``False``.
1064+
You can also pass |ObservabilitySettings| instead to further configure
1065+
observability behavior.
1066+
1067+
For example:
1068+
1069+
.. code-block:: python
1070+
1071+
from hypothesis import settings, ObservabilitySettings
1072+
1073+
# enables observability
1074+
settings(observability=True)
1075+
1076+
# enables observability, with choice sequence data
1077+
settings(observability=ObservabilitySettings(choices=True))
1078+
1079+
If not set, |settings.observability| is inherited from the
1080+
:ref:`HYPOTHESIS_OBSERVABILITY <observability-configuration>`
1081+
environment variable.
1082+
1083+
|settings.observability| is ``False`` by default.
1084+
"""
1085+
return self._observability
1086+
10301087
def __call__(self, test: T) -> T:
10311088
"""Make the settings object (self) an attribute of the test.
10321089
@@ -1199,6 +1256,7 @@ def note_deprecation(
11991256
deadline=duration(milliseconds=200),
12001257
print_blob=False,
12011258
backend="hypothesis",
1259+
observability=not_set, # type: ignore
12021260
)
12031261
settings.register_profile("default", default)
12041262
settings.load_profile("default")

0 commit comments

Comments
 (0)