Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/hypothesis/strategies/_internal/utils.py: 60%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

102 statements  

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())