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
11import inspect
12from collections.abc import Callable, Sequence
13from typing import Any
14
15from hypothesis.configuration import check_sideeffect_during_initialization
16from hypothesis.errors import InvalidArgument
17from hypothesis.internal.conjecture.choice import ChoiceT
18from hypothesis.internal.conjecture.data import ConjectureData
19from hypothesis.internal.reflection import get_pretty_function_description
20from hypothesis.strategies._internal.strategies import (
21 Ex,
22 RecurT,
23 SearchStrategy,
24 check_strategy,
25)
26
27
28class DeferredStrategy(SearchStrategy[Ex]):
29 """A strategy which may be used before it is fully defined."""
30
31 def __init__(self, definition: Callable[[], SearchStrategy[Ex]]):
32 super().__init__()
33 self.__wrapped_strategy: SearchStrategy[Ex] | None = None
34 self.__in_repr: bool = False
35 self.__definition: Callable[[], SearchStrategy[Ex]] | None = definition
36
37 @property
38 def wrapped_strategy(self) -> SearchStrategy[Ex]:
39 # we assign this before entering the condition to avoid a race condition
40 # under threading. See issue #4523.
41 definition = self.__definition
42 if self.__wrapped_strategy is None:
43 check_sideeffect_during_initialization("deferred evaluation of {!r}", self)
44
45 if not inspect.isfunction(definition):
46 raise InvalidArgument(
47 f"Expected definition to be a function but got {definition!r} "
48 f"of type {type(definition).__name__} instead."
49 )
50 result = definition()
51 if result is self:
52 raise InvalidArgument("Cannot define a deferred strategy to be itself")
53 check_strategy(result, "definition()")
54 self.__wrapped_strategy = result
55 self.__definition = None
56 return self.__wrapped_strategy
57
58 @property
59 def branches(self) -> Sequence[SearchStrategy[Ex]]:
60 return self.wrapped_strategy.branches
61
62 def calc_label(self) -> int:
63 """Deferred strategies don't have a calculated label, because we would
64 end up having to calculate the fixed point of some hash function in
65 order to calculate it when they recursively refer to themself!
66
67 The label for the wrapped strategy will still appear because it
68 will be passed to draw.
69 """
70 # This is actually the same as the parent class implementation, but we
71 # include it explicitly here in order to document that this is a
72 # deliberate decision.
73 return self.class_label
74
75 def calc_is_empty(self, recur: RecurT) -> bool:
76 return recur(self.wrapped_strategy)
77
78 def calc_has_reusable_values(self, recur: RecurT) -> bool:
79 return recur(self.wrapped_strategy)
80
81 def __repr__(self) -> str:
82 if self.__wrapped_strategy is not None:
83 if self.__in_repr:
84 return f"(deferred@{id(self)!r})"
85 try:
86 self.__in_repr = True
87 return repr(self.__wrapped_strategy)
88 finally:
89 self.__in_repr = False
90 else:
91 description = get_pretty_function_description(self.__definition)
92 return f"deferred({description})"
93
94 def do_draw(self, data: ConjectureData) -> Ex:
95 return data.draw(self.wrapped_strategy)
96
97 def _invert(self, value: Any) -> tuple[ChoiceT, ...]:
98 return self.wrapped_strategy._invert(value)