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 Parameter, Signature
12from weakref import WeakKeyDictionary
13
14from hypothesis.control import note, should_note
15from hypothesis.errors import InvalidState
16from hypothesis.internal.reflection import (
17 convert_positional_arguments,
18 get_signature,
19 nicerepr,
20 proxies,
21 repr_call,
22)
23from hypothesis.strategies._internal.lazy import unwrap_strategies
24from hypothesis.strategies._internal.strategies import (
25 RecurT,
26 SampledFromStrategy,
27 SearchStrategy,
28)
29from hypothesis.utils.conventions import UniqueIdentifier
30
31can_vary = UniqueIdentifier("can_vary")
32
33
34class FunctionStrategy(SearchStrategy):
35 def __init__(self, like, returns, pure):
36 super().__init__()
37 self.like = like
38 self.returns = returns
39 self.pure = pure
40 # If `returns` can only generate a single value - just(), none(), or a
41 # one-element sampled_from() - we know what our functions return before
42 # they are ever called, and show them as constant lambdas instead. We
43 # have to unwrap lazy wrappers to see that, and to bail out if any
44 # .map() or .filter() was applied, since those transformations could
45 # change or reject the value we'd otherwise report.
46 unwrapped = unwrap_strategies(returns)
47 if (
48 isinstance(unwrapped, SampledFromStrategy)
49 and len(unwrapped.elements) == 1
50 and not unwrapped._transformations
51 ):
52 self._constant = unwrapped.elements[0]
53 else:
54 self._constant = can_vary
55 # Using wekrefs-to-generated-functions means that the cache can be
56 # garbage-collected at the end of each example, reducing memory use.
57 self._cache = WeakKeyDictionary()
58
59 def _pretty_constant_function(self, p, cycle):
60 # Annotations are valid in a signature, but not in a lambda.
61 sig = get_signature(self.like, follow_wrapped=False)
62 params = str(
63 sig.replace(
64 parameters=[
65 param.replace(annotation=Parameter.empty)
66 for param in sig.parameters.values()
67 ],
68 return_annotation=Signature.empty,
69 )
70 )[1:-1]
71 p.text(f"lambda {params}: " if params else "lambda: ")
72 p.pretty(self._constant)
73
74 def calc_is_empty(self, recur: RecurT) -> bool:
75 return recur(self.returns)
76
77 def do_draw(self, data):
78 # If we know what the function returns, we show it as a constant lambda
79 # instead of noting every call - the notes would be redundant.
80 varies = self._constant is can_vary
81
82 @proxies(self.like)
83 def inner(*args, **kwargs):
84 if data.frozen:
85 raise InvalidState(
86 f"This generated {nicerepr(self.like)} function can only "
87 "be called within the scope of the @given that created it."
88 )
89 if self.pure:
90 args, kwargs = convert_positional_arguments(self.like, args, kwargs)
91 key = (args, frozenset(kwargs.items()))
92 cache = self._cache.setdefault(inner, {})
93 if key not in cache:
94 cache[key] = data.draw(self.returns)
95 # optimization to avoid needless repr_call
96 if varies and should_note():
97 rep = repr_call(self.like, args, kwargs, reorder=False)
98 note(f"Called function: {rep} -> {cache[key]!r}")
99 return cache[key]
100 else:
101 val = data.draw(self.returns)
102 if varies and should_note():
103 rep = repr_call(self.like, args, kwargs, reorder=False)
104 note(f"Called function: {rep} -> {val!r}")
105 return val
106
107 if not varies:
108 inner._repr_pretty_ = self._pretty_constant_function
109 return inner