strategies.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. # coding=utf-8
  2. #
  3. # This file is part of Hypothesis, which may be found at
  4. # https://github.com/HypothesisWorks/hypothesis-python
  5. #
  6. # Most of this work is copyright (C) 2013-2018 David R. MacIver
  7. # (david@drmaciver.com), but it contains contributions by others. See
  8. # CONTRIBUTING.rst for a full list of people who may hold copyright, and
  9. # consult the git log if you need to determine who owns an individual
  10. # contribution.
  11. #
  12. # This Source Code Form is subject to the terms of the Mozilla Public License,
  13. # v. 2.0. If a copy of the MPL was not distributed with this file, You can
  14. # obtain one at http://mozilla.org/MPL/2.0/.
  15. #
  16. # END HEADER
  17. from __future__ import division, print_function, absolute_import
  18. from collections import defaultdict
  19. import hypothesis.internal.conjecture.utils as cu
  20. from hypothesis.errors import NoExamples, NoSuchExample, Unsatisfiable, \
  21. UnsatisfiedAssumption
  22. from hypothesis.control import assume, reject, _current_build_context
  23. from hypothesis._settings import note_deprecation
  24. from hypothesis.internal.compat import hrange
  25. from hypothesis.utils.conventions import UniqueIdentifier
  26. from hypothesis.internal.lazyformat import lazyformat
  27. from hypothesis.internal.reflection import get_pretty_function_description
  28. calculating = UniqueIdentifier('calculating')
  29. def one_of_strategies(xs):
  30. """Helper function for unioning multiple strategies."""
  31. xs = tuple(xs)
  32. if not xs:
  33. raise ValueError('Cannot join an empty list of strategies')
  34. return OneOfStrategy(xs)
  35. class SearchStrategy(object):
  36. """A SearchStrategy is an object that knows how to explore data of a given
  37. type.
  38. Except where noted otherwise, methods on this class are not part of
  39. the public API and their behaviour may change significantly between
  40. minor version releases. They will generally be stable between patch
  41. releases.
  42. """
  43. supports_find = True
  44. validate_called = False
  45. def recursive_property(name, default):
  46. """Handle properties which may be mutually recursive among a set of
  47. strategies.
  48. These are essentially lazily cached properties, with the ability to set
  49. an override: If the property has not been explicitly set, we calculate
  50. it on first access and memoize the result for later.
  51. The problem is that for properties that depend on each other, a naive
  52. calculation strategy may hit infinite recursion. Consider for example
  53. the property is_empty. A strategy defined as x = st.deferred(lambda x)
  54. is certainly empty (in order ot draw a value from x we would have to
  55. draw a value from x, for which we would have to draw a value from x,
  56. ...), but in order to calculate it the naive approach would end up
  57. calling x.is_empty in order to calculate x.is_empty in order to etc.
  58. The solution is one of fixed point calculation. We start with a default
  59. value that is the value of the property in the absence of evidence to
  60. the contrary, and then update the values of the property for all
  61. dependent strategies until we reach a fixed point.
  62. The approach taken roughly follows that in section 4.2 of Adams,
  63. Michael D., Celeste Hollenbeck, and Matthew Might. "On the complexity
  64. and performance of parsing with derivatives." ACM SIGPLAN Notices 51.6
  65. (2016): 224-236.
  66. """
  67. cache_key = 'cached_' + name
  68. calculation = 'calc_' + name
  69. force_key = 'force_' + name
  70. def forced_value(target):
  71. try:
  72. return getattr(target, force_key)
  73. except AttributeError:
  74. return getattr(target, cache_key)
  75. def accept(self):
  76. try:
  77. return forced_value(self)
  78. except AttributeError:
  79. pass
  80. mapping = {}
  81. hit_recursion = [False]
  82. # For a first pass we do a direct recursive calculation of the
  83. # property, but we block recursively visiting a value in the
  84. # computation of its property: When that happens, we simply
  85. # note that it happened and return the default value.
  86. def recur(strat):
  87. try:
  88. return forced_value(strat)
  89. except AttributeError:
  90. pass
  91. try:
  92. result = mapping[strat]
  93. if result is calculating:
  94. hit_recursion[0] = True
  95. return default
  96. else:
  97. return result
  98. except KeyError:
  99. mapping[strat] = calculating
  100. mapping[strat] = getattr(strat, calculation)(recur)
  101. return mapping[strat]
  102. recur(self)
  103. # If we hit self-recursion in the computation of any strategy
  104. # value, our mapping at the end is imprecise - it may or may
  105. # not have the right values in it. We now need to proceed with
  106. # a more careful fixed point calculation to get the exact
  107. # values. Hopefully our mapping is still pretty good and it
  108. # won't take a large number of updates to reach a fixed point.
  109. if hit_recursion[0]:
  110. needs_update = set(mapping)
  111. # We track which strategies use which in the course of
  112. # calculating their property value. If A ever uses B in
  113. # the course of calculating its value, then whenveer the
  114. # value of B changes we might need to update the value of
  115. # A.
  116. listeners = defaultdict(set)
  117. else:
  118. needs_update = None
  119. count = 0
  120. seen = set()
  121. while needs_update:
  122. count += 1
  123. # If we seem to be taking a really long time to stabilize we
  124. # start tracking seen values to attempt to detect an infinite
  125. # loop. This should be impossible, and most code will never
  126. # hit the count, but having an assertion for it means that
  127. # testing is easier to debug and we don't just have a hung
  128. # test.
  129. # Note: This is actually covered, by test_very_deep_deferral
  130. # in tests/cover/test_deferred_strategies.py. Unfortunately it
  131. # runs into a coverage bug. See
  132. # https://bitbucket.org/ned/coveragepy/issues/605/
  133. # for details.
  134. if count > 50: # pragma: no cover
  135. key = frozenset(mapping.items())
  136. assert key not in seen, (key, name)
  137. seen.add(key)
  138. to_update = needs_update
  139. needs_update = set()
  140. for strat in to_update:
  141. def recur(other):
  142. try:
  143. return forced_value(other)
  144. except AttributeError:
  145. pass
  146. listeners[other].add(strat)
  147. try:
  148. return mapping[other]
  149. except KeyError:
  150. needs_update.add(other)
  151. mapping[other] = default
  152. return default
  153. new_value = getattr(strat, calculation)(recur)
  154. if new_value != mapping[strat]:
  155. needs_update.update(listeners[strat])
  156. mapping[strat] = new_value
  157. # We now have a complete and accurate calculation of the
  158. # property values for everything we have seen in the course of
  159. # running this calculation. We simultaneously update all of
  160. # them (not just the strategy we started out with).
  161. for k, v in mapping.items():
  162. setattr(k, cache_key, v)
  163. return getattr(self, cache_key)
  164. accept.__name__ = name
  165. return property(accept)
  166. # Returns True if this strategy can never draw a value and will always
  167. # result in the data being marked invalid.
  168. # The fact that this returns False does not guarantee that a valid value
  169. # can be drawn - this is not intended to be perfect, and is primarily
  170. # intended to be an optimisation for some cases.
  171. is_empty = recursive_property('is_empty', True)
  172. # Returns True if values from this strategy can safely be reused without
  173. # this causing unexpected behaviour.
  174. has_reusable_values = recursive_property('has_reusable_values', True)
  175. # Whether this strategy is suitable for holding onto in a cache.
  176. is_cacheable = recursive_property('is_cacheable', True)
  177. def calc_is_cacheable(self, recur):
  178. return True
  179. def calc_is_empty(self, recur):
  180. # Note: It is correct and significant that the default return value
  181. # from calc_is_empty is False despite the default value for is_empty
  182. # being true. The reason for this is that strategies should be treated
  183. # as empty absent evidence to the contrary, but most basic strategies
  184. # are trivially non-empty and it would be annoying to have to override
  185. # this method to show that.
  186. return False
  187. def calc_has_reusable_values(self, recur):
  188. return False
  189. def example(self, random=None):
  190. """Provide an example of the sort of value that this strategy
  191. generates. This is biased to be slightly simpler than is typical for
  192. values from this strategy, for clarity purposes.
  193. This method shouldn't be taken too seriously. It's here for interactive
  194. exploration of the API, not for any sort of real testing.
  195. This method is part of the public API.
  196. """
  197. context = _current_build_context.value
  198. if context is not None:
  199. if context.data is not None and context.data.depth > 0:
  200. note_deprecation(
  201. 'Using example() inside a strategy definition is a bad '
  202. 'idea. It will become an error in a future version of '
  203. "Hypothesis, but it's unlikely that it's doing what you "
  204. 'intend even now. Instead consider using '
  205. 'hypothesis.strategies.builds() or '
  206. '@hypothesis.strategies.composite to define your strategy.'
  207. ' See '
  208. 'https://hypothesis.readthedocs.io/en/latest/data.html'
  209. '#hypothesis.strategies.builds or '
  210. 'https://hypothesis.readthedocs.io/en/latest/data.html'
  211. '#composite-strategies for more details.'
  212. )
  213. else:
  214. note_deprecation(
  215. 'Using example() inside a test function is a bad '
  216. 'idea. It will become an error in a future version of '
  217. "Hypothesis, but it's unlikely that it's doing what you "
  218. 'intend even now. Instead consider using '
  219. 'hypothesis.strategies.data() to draw '
  220. 'more examples during testing. See '
  221. 'https://hypothesis.readthedocs.io/en/latest/data.html'
  222. '#drawing-interactively-in-tests for more details.'
  223. )
  224. from hypothesis import find, settings, Verbosity
  225. # Conjecture will always try the zero example first. This would result
  226. # in us producing the same example each time, which is boring, so we
  227. # deliberately skip the first example it feeds us.
  228. first = []
  229. def condition(x):
  230. if first:
  231. return True
  232. else:
  233. first.append(x)
  234. return False
  235. try:
  236. return find(
  237. self,
  238. condition,
  239. random=random,
  240. settings=settings(
  241. max_shrinks=0,
  242. max_iterations=1000,
  243. database=None,
  244. verbosity=Verbosity.quiet,
  245. )
  246. )
  247. except (NoSuchExample, Unsatisfiable):
  248. # This can happen when a strategy has only one example. e.g.
  249. # st.just(x). In that case we wanted the first example after all.
  250. if first:
  251. return first[0]
  252. raise NoExamples(
  253. u'Could not find any valid examples in 100 tries'
  254. )
  255. def map(self, pack):
  256. """Returns a new strategy that generates values by generating a value
  257. from this strategy and then calling pack() on the result, giving that.
  258. This method is part of the public API.
  259. """
  260. return MappedSearchStrategy(
  261. pack=pack, strategy=self
  262. )
  263. def flatmap(self, expand):
  264. """Returns a new strategy that generates values by generating a value
  265. from this strategy, say x, then generating a value from
  266. strategy(expand(x))
  267. This method is part of the public API.
  268. """
  269. from hypothesis.searchstrategy.flatmapped import FlatMapStrategy
  270. return FlatMapStrategy(
  271. expand=expand, strategy=self
  272. )
  273. def filter(self, condition):
  274. """Returns a new strategy that generates values from this strategy
  275. which satisfy the provided condition. Note that if the condition is too
  276. hard to satisfy this might result in your tests failing with
  277. Unsatisfiable.
  278. This method is part of the public API.
  279. """
  280. return FilteredStrategy(
  281. condition=condition,
  282. strategy=self,
  283. )
  284. @property
  285. def branches(self):
  286. return [self]
  287. def __or__(self, other):
  288. """Return a strategy which produces values by randomly drawing from one
  289. of this strategy or the other strategy.
  290. This method is part of the public API.
  291. """
  292. if not isinstance(other, SearchStrategy):
  293. raise ValueError('Cannot | a SearchStrategy with %r' % (other,))
  294. return one_of_strategies((self, other))
  295. def validate(self):
  296. """Throw an exception if the strategy is not valid.
  297. This can happen due to lazy construction
  298. """
  299. if self.validate_called:
  300. return
  301. try:
  302. self.validate_called = True
  303. self.do_validate()
  304. self.is_empty
  305. self.has_reusable_values
  306. except Exception:
  307. self.validate_called = False
  308. raise
  309. def do_validate(self):
  310. pass
  311. def do_draw(self, data):
  312. raise NotImplementedError('%s.do_draw' % (type(self).__name__,))
  313. def __init__(self):
  314. pass
  315. class OneOfStrategy(SearchStrategy):
  316. """Implements a union of strategies. Given a number of strategies this
  317. generates values which could have come from any of them.
  318. The conditional distribution draws uniformly at random from some
  319. non-empty subset of these strategies and then draws from the
  320. conditional distribution of that strategy.
  321. """
  322. def __init__(self, strategies, bias=None):
  323. SearchStrategy.__init__(self)
  324. strategies = tuple(strategies)
  325. self.original_strategies = list(strategies)
  326. self.__element_strategies = None
  327. self.bias = bias
  328. self.__in_branches = False
  329. if bias is not None:
  330. assert 0 < bias < 1
  331. self.sampler = cu.Sampler(
  332. [bias ** i for i in range(len(strategies))])
  333. else:
  334. self.sampler = None
  335. def calc_is_empty(self, recur):
  336. return all(recur(e) for e in self.original_strategies)
  337. def calc_has_reusable_values(self, recur):
  338. return all(recur(e) for e in self.original_strategies)
  339. def calc_is_cacheable(self, recur):
  340. return all(recur(e) for e in self.original_strategies)
  341. @property
  342. def element_strategies(self):
  343. from hypothesis.strategies import check_strategy
  344. if self.__element_strategies is None:
  345. strategies = []
  346. for arg in self.original_strategies:
  347. check_strategy(arg)
  348. if not arg.is_empty:
  349. strategies.extend(
  350. [s for s in arg.branches if not s.is_empty])
  351. pruned = []
  352. seen = set()
  353. for s in strategies:
  354. if s is self:
  355. continue
  356. if s in seen:
  357. continue
  358. seen.add(s)
  359. pruned.append(s)
  360. self.__element_strategies = pruned
  361. return self.__element_strategies
  362. def do_draw(self, data):
  363. n = len(self.element_strategies)
  364. assert n > 0
  365. if n == 1:
  366. return data.draw(self.element_strategies[0])
  367. elif self.sampler is None:
  368. i = cu.integer_range(data, 0, n - 1)
  369. else:
  370. i = self.sampler.sample(data)
  371. return data.draw(self.element_strategies[i])
  372. def __repr__(self):
  373. return ' | '.join(map(repr, self.original_strategies))
  374. def do_validate(self):
  375. for e in self.element_strategies:
  376. e.validate()
  377. @property
  378. def branches(self):
  379. if self.bias is None and not self.__in_branches:
  380. try:
  381. self.__in_branches = True
  382. return self.element_strategies
  383. finally:
  384. self.__in_branches = False
  385. else:
  386. return [self]
  387. class MappedSearchStrategy(SearchStrategy):
  388. """A strategy which is defined purely by conversion to and from another
  389. strategy.
  390. Its parameter and distribution come from that other strategy.
  391. """
  392. def __init__(self, strategy, pack=None):
  393. SearchStrategy.__init__(self)
  394. self.mapped_strategy = strategy
  395. if pack is not None:
  396. self.pack = pack
  397. def calc_is_empty(self, recur):
  398. return recur(self.mapped_strategy)
  399. def calc_is_cacheable(self, recur):
  400. return recur(self.mapped_strategy)
  401. def __repr__(self):
  402. if not hasattr(self, '_cached_repr'):
  403. self._cached_repr = '%r.map(%s)' % (
  404. self.mapped_strategy, get_pretty_function_description(
  405. self.pack)
  406. )
  407. return self._cached_repr
  408. def do_validate(self):
  409. self.mapped_strategy.validate()
  410. def pack(self, x):
  411. """Take a value produced by the underlying mapped_strategy and turn it
  412. into a value suitable for outputting from this strategy."""
  413. raise NotImplementedError(
  414. '%s.pack()' % (self.__class__.__name__))
  415. def do_draw(self, data):
  416. for _ in range(3):
  417. i = data.index
  418. try:
  419. data.start_example()
  420. result = self.pack(data.draw(self.mapped_strategy))
  421. data.stop_example()
  422. return result
  423. except UnsatisfiedAssumption:
  424. data.stop_example(discard=True)
  425. if data.index == i:
  426. raise
  427. reject()
  428. @property
  429. def branches(self):
  430. return [
  431. MappedSearchStrategy(pack=self.pack, strategy=strategy)
  432. for strategy in self.mapped_strategy.branches
  433. ]
  434. class FilteredStrategy(SearchStrategy):
  435. def __init__(self, strategy, condition):
  436. super(FilteredStrategy, self).__init__()
  437. self.condition = condition
  438. self.filtered_strategy = strategy
  439. def calc_is_empty(self, recur):
  440. return recur(self.filtered_strategy)
  441. def calc_is_cacheable(self, recur):
  442. return recur(self.filtered_strategy)
  443. def __repr__(self):
  444. if not hasattr(self, '_cached_repr'):
  445. self._cached_repr = '%r.filter(%s)' % (
  446. self.filtered_strategy, get_pretty_function_description(
  447. self.condition)
  448. )
  449. return self._cached_repr
  450. def do_validate(self):
  451. self.filtered_strategy.validate()
  452. def do_draw(self, data):
  453. for i in hrange(3):
  454. start_index = data.index
  455. value = data.draw(self.filtered_strategy)
  456. if self.condition(value):
  457. return value
  458. else:
  459. if i == 0:
  460. data.note_event(lazyformat(
  461. 'Retried draw from %r to satisfy filter', self,))
  462. # This is to guard against the case where we consume no data.
  463. # As long as we consume data, we'll eventually pass or raise.
  464. # But if we don't this could be an infinite loop.
  465. assume(data.index > start_index)
  466. data.note_event('Aborted test because unable to satisfy %r' % (
  467. self,
  468. ))
  469. data.mark_invalid()
  470. @property
  471. def branches(self):
  472. branches = [
  473. FilteredStrategy(strategy=strategy, condition=self.condition)
  474. for strategy in self.filtered_strategy.branches
  475. ]
  476. return branches