Skip to content
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

Using mutually recursive types causes AttributeError #2722

Closed
Varriount opened this issue Dec 29, 2020 · 1 comment · Fixed by #2724
Closed

Using mutually recursive types causes AttributeError #2722

Varriount opened this issue Dec 29, 2020 · 1 comment · Fixed by #2724
Assignees
Labels
bug something is clearly wrong here

Comments

@Varriount
Copy link

Varriount commented Dec 29, 2020

I'm currently testing some code that deals with a recursive tree structure. Since the code already has type definitions, I was hoping to reuse them in Hypothesis by way of from_type. While I was successful in finding a way forward, some of the failed attempts seemed to uncover errors within Hypothesis.

from typing import ForwardRef, Mapping, Sequence, Union

import hypothesis.strategies as st

if True:
    # Doesn't work
    A = Mapping[str, "B"]
    B = Union[Sequence[str], A]

    st.register_type_strategy(
        ForwardRef("B"),
        lambda x: st.deferred(lambda: b_strategy),
    )

    b_strategy = st.from_type(B)
    print(b_strategy)
    print(repr(b_strategy.example()))

else:
    # Works
    C = Union[Sequence[str], "D"]
    D = Mapping[str, C]

    st.register_type_strategy(
        ForwardRef("D"),
        lambda x: st.deferred(lambda: d_strategy),
    )

    d_strategy = st.from_type(D)
    print(d_strategy)
    print(repr(d_strategy.example()))

The version that doesn't work spits out a rather long traceback:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "...\hypothesis\strategies\_internal\strategies.py", line 314, in example
    example_generating_inner_function()
  File "...\hypothesis\strategies\_internal\strategies.py", line 302, in example_generating_inner_function
    @settings(
  File "...\hypothesis\core.py", line 1023, in wrapped_test
    processed_args = process_arguments_to_given(
  File "...\hypothesis\core.py", line 441, in process_arguments_to_given
    search_strategy.validate()
  File "...\hypothesis\strategies\_internal\strategies.py", line 377, in validate
    self.do_validate()
  File "...\hypothesis\strategies\_internal\collections.py", line 39, in do_validate
    s.validate()
  File "...\hypothesis\strategies\_internal\strategies.py", line 377, in validate
    self.do_validate()
  File "...\hypothesis\strategies\_internal\strategies.py", line 638, in do_validate
    self.mapped_strategy.validate()
  File "...\hypothesis\strategies\_internal\strategies.py", line 377, in validate
    self.do_validate()
  File "...\hypothesis\strategies\_internal\lazy.py", line 118, in do_validate
    w.validate()
  File "...\hypothesis\strategies\_internal\strategies.py", line 377, in validate
    self.do_validate()
  File "...\hypothesis\strategies\_internal\strategies.py", line 638, in do_validate
    self.mapped_strategy.validate()
  File "...\hypothesis\strategies\_internal\strategies.py", line 377, in validate
    self.do_validate()
  File "...\hypothesis\strategies\_internal\collections.py", line 39, in do_validate
    s.validate()
  File "...\hypothesis\strategies\_internal\strategies.py", line 377, in validate
    self.do_validate()
  File "...\hypothesis\strategies\_internal\strategies.py", line 595, in do_validate
    for e in self.element_strategies:
  File "...\hypothesis\strategies\_internal\strategies.py", line 570, in element_strategies
    if not arg.is_empty:
  File "...\hypothesis\strategies\_internal\strategies.py", line 125, in accept
    recur(self)
  File "...\hypothesis\strategies\_internal\strategies.py", line 121, in recur
    mapping[strat] = getattr(strat, calculation)(recur)
  File "...\hypothesis\strategies\_internal\lazy.py", line 86, in calc_is_empty
    return recur(self.wrapped_strategy)
  File "...\hypothesis\strategies\_internal\strategies.py", line 121, in recur
    mapping[strat] = getattr(strat, calculation)(recur)
  File "...\hypothesis\strategies\_internal\deferred.py", line 80, in calc_is_empty
    return recur(self.wrapped_strategy)
  File "...\hypothesis\strategies\_internal\deferred.py", line 55, in wrapped_strategy
    del self.__definition
AttributeError: _DeferredStrategy__definition
@Zac-HD Zac-HD self-assigned this Dec 29, 2020
@Zac-HD Zac-HD added the bug something is clearly wrong here label Dec 29, 2020
@Zac-HD
Copy link
Member

Zac-HD commented Dec 29, 2020

Thanks for reporting this! To be honest it's probably cleaner to register explicit st.recursive() strategies for each type, but I can see why you'd want to use st.from_type() - and I consider this a bug to be fixed either way 🙂


I also need to appreciate this symphony of lazy evaluation for a minute:

A = Dict[str, "B"]          # "B" is a forward reference, to avoid NameError.  Standard.
B = Union[List[str], A]     # Mypy doesn't support direct recursion, but mutual is fine! 🤔

# We're not actually using the "register a function" feature of inspecting the
# type instance... just using the function closure to *defer harder* because
# st.deferred() itself isn't enough inside the optionally-lazy st.from_type() 😕
st.register_type_strategy(ForwardRef("B"), lambda _: st.deferred(lambda: bs))

							# st.from_type(B) would fail here
bs = st.from_type(B)        # works
							# st.from_type(B) works here because bs is now in scope 🤯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something is clearly wrong here
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants