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
11"""This module implements various useful common functions for shrinking tasks."""
12
13
14class Shrinker:
15 """A Shrinker object manages a single value and a predicate it should
16 satisfy, and attempts to improve it in some direction, making it smaller
17 and simpler."""
18
19 def __init__(
20 self,
21 initial,
22 predicate,
23 *,
24 full=False,
25 debug=False,
26 name=None,
27 **kwargs,
28 ):
29 self.setup(**kwargs)
30 self.current = self.make_immutable(initial)
31 self.initial = self.current
32 self.full = full
33 self.changes = 0
34 self.name = name
35
36 self.__predicate = predicate
37 self.__seen = {self.make_canonical(self.current)}
38 self.debugging_enabled = debug
39
40 @property
41 def calls(self) -> int:
42 return len(self.__seen)
43
44 def __repr__(self) -> str:
45 return "{}({}initial={!r}, current={!r})".format(
46 type(self).__name__,
47 "" if self.name is None else f"{self.name!r}, ",
48 self.initial,
49 self.current,
50 )
51
52 def setup(self, **kwargs):
53 """Runs initial setup code.
54
55 Convenience function for children that doesn't require messing
56 with the signature of init.
57 """
58
59 def delegate(self, other_class, convert_to, convert_from, **kwargs):
60 """Delegates shrinking to another shrinker class, by converting the
61 current value to and from it with provided functions."""
62 self.call_shrinker(
63 other_class,
64 convert_to(self.current),
65 lambda v: self.consider(convert_from(v)),
66 **kwargs,
67 )
68
69 def call_shrinker(self, other_class, initial, predicate, **kwargs):
70 """Calls another shrinker class, passing through the relevant context
71 variables.
72
73 Note we explicitly do not pass through full.
74 """
75
76 return other_class.shrink(initial, predicate, **kwargs)
77
78 def debug(self, *args: object) -> None:
79 if self.debugging_enabled:
80 print("DEBUG", self, *args)
81
82 @classmethod
83 def shrink(cls, initial, predicate, **kwargs):
84 """Shrink the value ``initial`` subject to the constraint that it
85 satisfies ``predicate``.
86
87 Returns the shrunk value.
88 """
89 shrinker = cls(initial, predicate, **kwargs)
90 shrinker.run()
91 return shrinker.current
92
93 def run(self):
94 """Run for an appropriate number of steps to improve the current value.
95
96 If self.full is True, will run until no further improvements can
97 be found.
98 """
99 if self.short_circuit():
100 return
101 if self.full:
102 prev = -1
103 while self.changes != prev:
104 prev = self.changes
105 self.run_step()
106 else:
107 self.run_step()
108 self.debug("COMPLETE")
109
110 def consider(self, value):
111 """Try using ``value`` as a possible candidate improvement.
112
113 Return True if self.current is canonically equal to value after the call, either because
114 the value was incorporated as an improvement or because it had that value already.
115 """
116 value = self.make_immutable(value)
117 self.debug(f"considering {value!r}")
118 canonical = self.make_canonical(value)
119 if canonical == self.make_canonical(self.current):
120 return True
121 if canonical in self.__seen:
122 return False
123 self.__seen.add(canonical)
124 self.check_invariants(value)
125 if not self.left_is_better(value, self.current):
126 self.debug(f"Rejected {value!r} as no better than {self.current=}")
127 return False
128 if self.__predicate(value):
129 self.debug(f"shrinking to {value!r}")
130 self.changes += 1
131 self.current = value
132 return True
133 else:
134 self.debug(f"Rejected {value!r} not satisfying predicate")
135 return False
136
137 def make_canonical(self, value):
138 """Convert immutable value into a canonical and hashable, but not necessarily equal,
139 representation of itself.
140
141 This representation is used only for tracking already-seen values, not passed to the
142 shrinker.
143
144 Defaults to just returning the (immutable) input value.
145 """
146 return value
147
148 def make_immutable(self, value):
149 """Convert value into an immutable representation of itself.
150
151 It is these immutable versions that the shrinker will work on.
152
153 Defaults to just returning the value.
154 """
155 return value
156
157 def check_invariants(self, value):
158 """Make appropriate assertions about the value to ensure that it is
159 valid for this shrinker.
160
161 Does nothing by default.
162 """
163
164 def short_circuit(self):
165 """Possibly attempt to do some shrinking.
166
167 If this returns True, the ``run`` method will terminate early
168 without doing any more work.
169 """
170 return False
171
172 def left_is_better(self, left, right):
173 """Returns True if the left is strictly simpler than the right
174 according to the standards of this shrinker."""
175 raise NotImplementedError
176
177 def run_step(self):
178 """Run a single step of the main shrink loop, attempting to improve the
179 current value."""
180 raise NotImplementedError