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
11from collections.abc import MutableMapping, Sequence
12from inspect import signature
13from typing import Any, Callable, Optional
14from weakref import WeakKeyDictionary
15
16from hypothesis.configuration import check_sideeffect_during_initialization
17from hypothesis.internal.conjecture.data import ConjectureData
18from hypothesis.internal.reflection import (
19 convert_keyword_arguments,
20 convert_positional_arguments,
21 get_pretty_function_description,
22 repr_call,
23)
24from hypothesis.strategies._internal.deferred import DeferredStrategy
25from hypothesis.strategies._internal.strategies import Ex, RecurT, SearchStrategy
26
27unwrap_cache: MutableMapping[SearchStrategy, SearchStrategy] = WeakKeyDictionary()
28unwrap_depth = 0
29
30
31def unwrap_strategies(s):
32 global unwrap_depth
33
34 # optimization
35 if not isinstance(s, (LazyStrategy, DeferredStrategy)):
36 return s
37
38 try:
39 return unwrap_cache[s]
40 except KeyError:
41 pass
42
43 unwrap_cache[s] = s
44 unwrap_depth += 1
45
46 try:
47 result = unwrap_strategies(s.wrapped_strategy)
48 unwrap_cache[s] = result
49
50 try:
51 assert result.force_has_reusable_values == s.force_has_reusable_values
52 except AttributeError:
53 pass
54
55 try:
56 result.force_has_reusable_values = s.force_has_reusable_values
57 except AttributeError:
58 pass
59
60 return result
61 finally:
62 unwrap_depth -= 1
63 if unwrap_depth <= 0:
64 unwrap_cache.clear()
65 assert unwrap_depth >= 0
66
67
68class LazyStrategy(SearchStrategy[Ex]):
69 """A strategy which is defined purely by conversion to and from another
70 strategy.
71
72 Its parameter and distribution come from that other strategy.
73 """
74
75 def __init__(
76 self,
77 function: Callable[..., SearchStrategy[Ex]],
78 args: Sequence[object],
79 kwargs: dict[str, object],
80 *,
81 transforms: tuple[tuple[str, Callable[..., Any]], ...] = (),
82 force_repr: Optional[str] = None,
83 ):
84 super().__init__()
85 self.__wrapped_strategy: Optional[SearchStrategy[Ex]] = None
86 self.__representation: Optional[str] = force_repr
87 self.function = function
88 self.__args = args
89 self.__kwargs = kwargs
90 self._transformations = transforms
91
92 @property
93 def supports_find(self) -> bool:
94 return self.wrapped_strategy.supports_find
95
96 def calc_is_empty(self, recur: RecurT) -> bool:
97 return recur(self.wrapped_strategy)
98
99 def calc_has_reusable_values(self, recur: RecurT) -> bool:
100 return recur(self.wrapped_strategy)
101
102 def calc_is_cacheable(self, recur: RecurT) -> bool:
103 for source in (self.__args, self.__kwargs.values()):
104 for v in source:
105 if isinstance(v, SearchStrategy) and not v.is_cacheable:
106 return False
107 return True
108
109 def calc_label(self) -> int:
110 return self.wrapped_strategy.label
111
112 @property
113 def wrapped_strategy(self) -> SearchStrategy[Ex]:
114 if self.__wrapped_strategy is None:
115 check_sideeffect_during_initialization("lazy evaluation of {!r}", self)
116
117 unwrapped_args = tuple(unwrap_strategies(s) for s in self.__args)
118 unwrapped_kwargs = {
119 k: unwrap_strategies(v) for k, v in self.__kwargs.items()
120 }
121
122 base = self.function(*self.__args, **self.__kwargs)
123 if unwrapped_args == self.__args and unwrapped_kwargs == self.__kwargs:
124 self.__wrapped_strategy = base
125 else:
126 self.__wrapped_strategy = self.function(
127 *unwrapped_args, **unwrapped_kwargs
128 )
129 for method, fn in self._transformations:
130 self.__wrapped_strategy = getattr(self.__wrapped_strategy, method)(fn)
131 assert self.__wrapped_strategy is not None
132 return self.__wrapped_strategy
133
134 def __with_transform(self, method, fn):
135 repr_ = self.__representation
136 if repr_:
137 repr_ = f"{repr_}.{method}({get_pretty_function_description(fn)})"
138 return LazyStrategy(
139 self.function,
140 self.__args,
141 self.__kwargs,
142 transforms=(*self._transformations, (method, fn)),
143 force_repr=repr_,
144 )
145
146 def map(self, pack):
147 return self.__with_transform("map", pack)
148
149 def filter(self, condition):
150 return self.__with_transform("filter", condition)
151
152 def do_validate(self) -> None:
153 w = self.wrapped_strategy
154 assert isinstance(w, SearchStrategy), f"{self!r} returned non-strategy {w!r}"
155 w.validate()
156
157 def __repr__(self) -> str:
158 if self.__representation is None:
159 sig = signature(self.function)
160 pos = [p for p in sig.parameters.values() if "POSITIONAL" in p.kind.name]
161 if len(pos) > 1 or any(p.default is not sig.empty for p in pos):
162 _args, _kwargs = convert_positional_arguments(
163 self.function, self.__args, self.__kwargs
164 )
165 else:
166 _args, _kwargs = convert_keyword_arguments(
167 self.function, self.__args, self.__kwargs
168 )
169 kwargs_for_repr = {
170 k: v
171 for k, v in _kwargs.items()
172 if k not in sig.parameters or v is not sig.parameters[k].default
173 }
174 self.__representation = repr_call(
175 self.function, _args, kwargs_for_repr, reorder=False
176 ) + "".join(
177 f".{method}({get_pretty_function_description(fn)})"
178 for method, fn in self._transformations
179 )
180 return self.__representation
181
182 def do_draw(self, data: ConjectureData) -> Ex:
183 return data.draw(self.wrapped_strategy)