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