New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue with recursive build strategy? #3026
Comments
(Please apologize, wrong click.) |
I dug a bit deeper; I didn't understand strategy = st.deferred(
lambda:
st.fixed_dictionaries(
{
'value': st.integers(min_value=1),
'next_node': st.one_of(st.none(), strategy)
}
).map(lambda kwargs: SomeClass(**kwargs))
) However, now I am pretty lost how to automate this behavior in icontract-hypothesis. Ideally, I'd like to do something like: strategy = st.deferred(
lambda:
(
st.register_type_strategy(strategy),
# Here I'd like to make use of the registered strategy
icontract_hypothesis.infer_strategy(some_cls)
)[1]
) where Unfortunately, this causes an endless recursion. Do you happen to have any ideas how this behavior can be achieved? |
I think the trick you're looking for is build_strategy_for_some_class = st.builds(
SomeClass,
value=st.integers(min_value=1),
next_node=st.none() | st.deferred(lambda: st.from_type(SomeClass)),
)
st.register_type_strategy(SomeClass, build_strategy_for_some_class) And it's safe (if a little less efficient) to wrap |
Hi @Zac-HD , I tried the following solution: # STEP 1
st.register_type_strategy(
SomeClass,
st.deferred(lambda: st.from_type(SomeClass)))
# STEP 2
strategy = st.builds(
SomeClass,
label=st.integers(min_value=1),
next_node=st.one_of(
st.none(), st.from_type(SomeClass))
st.register_type_strategy(
SomeClass,
strategy
)
# STEP 3
strategy_str = str(strategy) This gives an endless recursion (at
Thank you very much for any pointers! |
Please let me also add some more context why I register the strategy twice (step 1 and step 2) as this might have been lost. The strategies are constructed programmatically. In that use case, I rely on Hypothesis and its For example, |
I also tried playing with placeholders in the lambda. As far as I understand, Python binds the variables in lambda by reference; consider the following snippet: x = 1
some_lambda = lambda: x
print(f"some_lambda() is {some_lambda()!r}")
# some_lambda() is 1
x = 2
print(f"some_lambda() is {some_lambda()!r}")
# some_lambda() is 2 However, when I try the same "trick" with the strategies, I get unexpected result: placeholder = st.just(1977)
strategy = st.deferred(lambda: placeholder)
print(f"Step 1: strategy is {strategy!r}")
# Step 1: strategy is deferred(lambda: placeholder)
st.register_type_strategy(
SomeClass,
strategy)
strategy = st.from_type(SomeClass)
print(f"Step 2: strategy is {strategy!r}")
# Step 2: strategy is just(1977)
placeholder = st.just(2021)
strategy = st.from_type(SomeClass)
print(f"Step 3: strategy is {strategy!r}")
# Step 3: strategy is just(1977) <<< UNEXPECTED (I marked the unexpected spot with I tried to figure out what is going on by reading the source code of Hypothesis. In def _from_type(thing: Type[Ex]) -> SearchStrategy[Ex]:
...
def as_strategy(strat_or_callable, thing, final=True): where you check whether a strategy is empty (Line 978): if strategy.is_empty:
... |
Hmm, maybe dropping a
kwargs[k] = just(p.default) | deferred(lambda t=hints[k]: _from_type(t)) , to capture the loop variable.
(I'll come back for a deeper investigation later) |
@Zac-HD I don't have the understanding deep enough about that part of the code you posted, but I have a feeling that such a step would be too general? In other words, wouldn't it cause all kinds of unintended behaviors? I most probably miss something, but isn't there a bug that the deferred is actually de-referenced when you supply it to |
With the patch I wrote to defer resolution of type hints in class RecursiveClass:
def __init__(self, value: int, next_node: typing.Optional["SomeClass"]):
assert value > 0
self.value = value
self.next_node = next_node
st.register_type_strategy(
RecursiveClass,
st.builds(RecursiveClass, value=st.integers(min_value=1)) # next_node inferred from type annotation
) |
Hi,
I observed that
hypothesis.strategy.builds
does not work on the recursive structures. Here is a short program, whereSomeClass
represents a data structure whose property is also of typeSomeClass
(e.g., a node in a linked list):While the strategy has been correctly registered (see the print statement), the
builds
strategy does not work as I'd expect it. The strategybuilds
infers the arguments forSomeClass
directly from its__init__
instead of checking whether the type has been registered.If I change:
to
I get the same assertion error.
Is this a bug or just a misunderstanding on my part?
Some related issues and articles:
This issue is not related to Issues with recursive data classes #3016 , which concerns the type annotations.
The article https://hypothesis.works/articles/recursive-data/ talks about generating recursive built-in structures. We need to use a non-built-in class
SomeClass
here.The article in the documentation https://hypothesis.readthedocs.io/en/latest/data.html#recursive-data talks about pitfalls when generating recursive data, but the issue here is that
hypothesis.strategy.builds(.)
disregard the arguments.In the current example, the focus is on the recursivity in terms of classes (a class has a property of the same class, not in terms of actual data structures). While
any_of
strategy might result in an endless linked list, it is actually not probable (the length of the linked list follows geometric distribution, if I am not mistaken).(Edit: added proper syntax highlighting)
The text was updated successfully, but these errors were encountered: