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
Argument copying technique in LazyStrategy does not preserve custom list subtypes #1017
Comments
Thanks for the report Josh - it's fantastic to get so much detail. I'll need to have a closer look when I have more time, but I think this is working as intended to avoid the mutable-default-argument problem - having arguments mutate between examples would break example shrinking, along with other things. If that's the case we should probably disallow mutable arguments instead of changing them though! Can you show a larger example of where this has come up? I think a concrete example would help me understand what you want to do 😄 |
No I don't think this is intended behaviour. I can't remember what the argument handling logic in lazy strategy does exactly, but as far as I remember it's meant mostly as an optimization with the intention of unwrapping nested strategies. It shouldn't have user visible behaviour other than that |
For more context on how this somewhat convoluted-looking example cropped up: I am in the process of incrementally porting a legacy third-party domain-specific randomized testing harness to use Hypothesis for test case data generation so that I can benefit from Hypothesis's example database and example shrinking (the port is going well so far and has already uncovered a new bug in the system under test). The existing test framework being ported has several internal data structures which have this pattern where they extend from @st.composite
def build_typed_thing(draw, types_list):
// [...]
condition = ...
type_of_thing = draw(st.sampled_from(types_list.valid_types(condition)))
// [... other generator calls ...]
return thing(type_of_thing, ...) where I'm not actually a fan of this pattern and my planned workaround is to refactor these wrapper classes so that they don't directly extend from I'm reporting this mostly just to aid in searchability (keywords: Regarding the mutable-default-argument problem: |
Digging into the history of |
I encountered an unexpected behavior when attempting to test a third-party library which makes use of custom
list
subtypes. Here's an example which reproduces the behavior:Consider a class which inherits from the built-in Python
list
type and adds additional methods:Because I've overridden both
__copy__
and__deepcopy__
, copying this list preserves its type:Let's say that I want to have a strategy which is parameterized by an instance of this class. This works as expected for some strategies:
However, I noticed that the argument type is not preserved when I use a composite strategy:
I believe that this behavior is due to how arguments are copied in
LazyStrategy
:https://github.com/HypothesisWorks/hypothesis-python/blob/3ee500943938d60a8a97b7d3d948522d65f23e84/src/hypothesis/searchstrategy/lazy.py#L88
Each argument is being copied by
tupelize()
, which is defined asI'm not sure whether it would be safe to replace
tupelize
withcopy
here: converting lists to tuples here guards against mutation from both code inside of the composite strategy's body as well as the code which calls / constructs the strategy, so safely usingcopy
here might also require additionalcopy
calls elsewhere to guard against mutation during/after invocation.I'm able to work around this behavior by wrapping my argument in an outer list (e.g.
[my_list]
) and unpacking the argument in my composite strategy.I'm therefore not blocked by this behavior but I found it confusing and figured it might be worth reporting / documenting.
The text was updated successfully, but these errors were encountered: