1# This file is part of Hypothesis, which may be found at
2# https://github.com/HypothesisWorks/hypothesis/
3#
4# Copyright the Hypothesis Authors.
5# Individual contributors are listed in AUTHORS.rst and the git log.
6#
7# This Source Code Form is subject to the terms of the Mozilla Public License,
8# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9# obtain one at https://mozilla.org/MPL/2.0/.
10
11import dataclasses
12import sys
13from collections.abc import Callable
14from functools import partial
15from typing import Literal, TypeAlias, TypeVar
16from weakref import WeakValueDictionary
17
18from hypothesis.errors import InvalidArgument
19from hypothesis.internal.cache import LRUReusedCache
20from hypothesis.internal.floats import clamp, float_to_int
21from hypothesis.internal.reflection import proxies
22from hypothesis.vendor.pretty import pretty
23
24T = TypeVar("T")
25ValueKey: TypeAlias = tuple[type, object]
26# (fn, args, kwargs)
27StrategyCacheKey: TypeAlias = tuple[
28 object, tuple[ValueKey, ...], frozenset[tuple[str, ValueKey]]
29]
30
31_all_strategies: WeakValueDictionary[str, Callable] = WeakValueDictionary()
32# note: LRUReusedCache is already thread-local internally
33_STRATEGY_CACHE = LRUReusedCache[StrategyCacheKey, object](1024)
34
35
36def _value_key(value: object) -> ValueKey:
37 if isinstance(value, float):
38 return (float, float_to_int(value))
39 return (type(value), value)
40
41
42def clear_strategy_cache() -> None:
43 _STRATEGY_CACHE.clear()
44
45
46def cacheable(fn: T) -> T:
47 from hypothesis.control import _current_build_context
48 from hypothesis.strategies._internal.strategies import SearchStrategy
49
50 @proxies(fn)
51 def cached_strategy(*args, **kwargs):
52 context = _current_build_context.value
53 if context is not None and context.data.provider.avoid_realization:
54 return fn(*args, **kwargs)
55
56 try:
57 kwargs_cache_key = {(k, _value_key(v)) for k, v in kwargs.items()}
58 except TypeError:
59 return fn(*args, **kwargs)
60
61 cache_key = (
62 fn,
63 tuple(_value_key(v) for v in args),
64 frozenset(kwargs_cache_key),
65 )
66 try:
67 return _STRATEGY_CACHE[cache_key]
68 except KeyError:
69 pass
70 except TypeError:
71 return fn(*args, **kwargs)
72
73 result = fn(*args, **kwargs)
74 if not isinstance(result, SearchStrategy) or result.is_cacheable:
75 _STRATEGY_CACHE[cache_key] = result
76 return result
77
78 # note that calling this clears the full _STRATEGY_CACHE for all strategies,
79 # not just the cache for this strategy.
80 cached_strategy.__clear_cache = clear_strategy_cache # type: ignore
81 return cached_strategy
82
83
84def defines_strategy(
85 *,
86 force_reusable_values: bool = False,
87 eager: bool | Literal["try"] = False,
88) -> Callable[[T], T]:
89 """
90 Each standard strategy function provided to users by Hypothesis should be
91 decorated with @defines_strategy. This registers the strategy with _all_strategies,
92 which is used in our own test suite to check that e.g. we document all strategies
93 in sphinx.
94
95 If you're reading this and are the author of a third-party strategy library:
96 don't worry, third-party strategies don't need to be decorated with
97 @defines_strategy. This function is internal to Hypothesis and not intended
98 for outside use.
99
100 Parameters
101 ----------
102 force_reusable_values : bool
103 If ``True``, strategies returned from the strategy function will have
104 ``.has_reusable_values == True`` set, even if it uses maps/filters or
105 non-reusable strategies internally. This tells our numpy/pandas strategies
106 that they can implicitly use such strategies as background values.
107 eager : bool | "try"
108 If ``True``, strategies returned by the strategy function are returned
109 as-is, and not wrapped in LazyStrategy.
110
111 If "try", we first attempt to call the strategy function and return the
112 resulting strategy. If this throws an exception, we treat it the same as
113 ``eager = False``, by returning the strategy function wrapped in a
114 LazyStrategy.
115 """
116
117 if eager is not False and force_reusable_values: # pragma: no cover
118 # We could support eager + force_reusable_values with a suitable wrapper,
119 # but there are currently no callers that request this combination.
120 raise InvalidArgument(
121 f"Passing both eager={eager} and force_reusable_values=True is "
122 "currently not supported"
123 )
124
125 def decorator(strategy_definition):
126 _all_strategies[strategy_definition.__name__] = strategy_definition
127
128 if eager is True:
129 return strategy_definition
130
131 @proxies(strategy_definition)
132 def accept(*args, **kwargs):
133 from hypothesis.strategies._internal.lazy import LazyStrategy
134
135 if eager == "try":
136 # Why not try this unconditionally? Because we'd end up with very
137 # deep nesting of recursive strategies - better to be lazy unless we
138 # *know* that eager evaluation is the right choice.
139 try:
140 return strategy_definition(*args, **kwargs)
141 except Exception:
142 # If invoking the strategy definition raises an exception,
143 # wrap that up in a LazyStrategy so it happens again later.
144 pass
145 result = LazyStrategy(strategy_definition, args, kwargs)
146 if force_reusable_values:
147 # Setting `force_has_reusable_values` here causes the recursive
148 # property code to set `.has_reusable_values == True`.
149 result.force_has_reusable_values = True
150 assert result.has_reusable_values
151 return result
152
153 accept.is_hypothesis_strategy_function = True
154 return accept
155
156 return decorator
157
158
159def _to_jsonable(obj: object, *, avoid_realization: bool, seen: set[int]) -> object:
160 if isinstance(obj, (str, int, float, bool, type(None))):
161 # We convert integers of 2**63 to floats, to avoid crashing external
162 # utilities with a 64 bit integer cap (notable, sqlite). See
163 # https://github.com/HypothesisWorks/hypothesis/pull/3797#discussion_r1413425110
164 # and https://github.com/simonw/sqlite-utils/issues/605.
165 if isinstance(obj, int) and not isinstance(obj, bool) and abs(obj) >= 2**63:
166 # Silently clamp very large ints to max_float, to avoid OverflowError when
167 # casting to float. (but avoid adding more constraints to symbolic values)
168 if avoid_realization:
169 return "<symbolic>"
170 obj = clamp(-sys.float_info.max, obj, sys.float_info.max)
171 return float(obj)
172 return obj
173 if avoid_realization:
174 return "<symbolic>"
175
176 obj_id = id(obj)
177 if obj_id in seen:
178 return pretty(obj, cycle=True)
179
180 recur = partial(
181 _to_jsonable, avoid_realization=avoid_realization, seen=seen | {obj_id}
182 )
183 if isinstance(obj, (list, tuple, set, frozenset)):
184 if isinstance(obj, tuple) and hasattr(obj, "_asdict"):
185 return recur(obj._asdict()) # treat namedtuples as dicts
186 return [recur(x) for x in obj]
187 if isinstance(obj, dict):
188 return {
189 k if isinstance(k, str) else pretty(k): recur(v) for k, v in obj.items()
190 }
191
192 # Hey, might as well try calling a .to_json() method - it works for Pandas!
193 # We try this before the below general-purpose handlers to give folks a
194 # chance to control this behavior on their custom classes.
195 try:
196 return recur(obj.to_json()) # type: ignore
197 except Exception:
198 pass
199
200 # Special handling for dataclasses, attrs, and pydantic classes
201 if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
202 # Avoid dataclasses.asdict here to ensure that inner to_json overrides
203 # can get called as well
204 return {
205 field.name: recur(getattr(obj, field.name))
206 for field in dataclasses.fields(obj)
207 }
208 if (attr := sys.modules.get("attr")) is not None and attr.has(type(obj)):
209 return recur(attr.asdict(obj, recurse=False))
210 if (pyd := sys.modules.get("pydantic")) and isinstance(obj, pyd.BaseModel):
211 return recur(obj.model_dump())
212
213 # If all else fails, we'll just pretty-print as a string.
214 return pretty(obj)
215
216
217def to_jsonable(obj: object, *, avoid_realization: bool) -> object:
218 """Recursively convert an object to json-encodable form.
219
220 This is not intended to round-trip, but rather provide an analysis-ready
221 format for observability. To avoid side affects, we pretty-print all but
222 known types.
223 """
224 return _to_jsonable(obj, avoid_realization=avoid_realization, seen=set())