Scott Stevens
Scott Stevens

Reputation: 2651

Hypothesis (Python): Omit argument

I have a function like so (it's actually a class, but that's not relevant given Python's duck typing):

def myfunc(a=None, b=None):
    <snip>

Now I want to write a Hypothesis test which always supplies a, but only sometimes b.

I tried

from hypothesis import given, strategies as strat

@given(a=strat.booleans())
@given(b=strat.integers(min_value=1) | strat.nothing())
def test_model_properties(self, **kwargs):
    myval = myfunc(**kwargs)
    <snip>

But it seems that when it gets strat.nothing() just skips that test run (I get hypothesis.errors.FailedHealthCheck: It looks like your strategy is filtering out a lot of data. when using that as the sole strategy for b).

How can I only sometimes supply an argument with a Hypothesis test? Do I need to write two tests, one with b and one without?

Upvotes: 1

Views: 924

Answers (4)

Zac Hatfield-Dodds
Zac Hatfield-Dodds

Reputation: 3003

It looks like you want none() instead of nothing():

from hypothesis import given, strategies as strat

@given(a=strat.booleans(), b=strat.none() | strat.integers(min_value=1))
def test_model_properties(self, **kwargs):
    myval = myfunc(**kwargs)
    ...

This is simpler than generating dictionaries to use as **kwargs and a little more efficient too. Ordering of the strategies for b is also important - putting none() first ensures that the minimal example will be a=False, b=None instead of a=False, b=1.

Also note that applying @given multiple times is very inefficient compared to a single use, and actually deprecated since version 3.34.0.

Upvotes: 1

Scott Stevens
Scott Stevens

Reputation: 2651

jacq's answer put me on the right track - the selection of keywords needs to be its own strategy.

With the standard dictionary

std = {'a': strat.booleans()}

and the optional dictionary

opt = {
    'b': strat.integers(),
    'c': strat.integers(),
}

I can then use chained list comprehension for all the possible "optional argument combinations":

# chain.from_iterable may or may not be faster; it doesn't matter for me.
optional = [combo
            for n in range(len(opt.items()))
            for combo in itertools.combinations(opt.items(), n+1)]

That generates the key-value tuples for b, c, and (b, c).

In order to draw a set of values, we need to get one of those options, which can be done with sampled_from(optional). With the obtained tuples, we must draw from the strategies within, in addition to those in the std dictionary.

strat.sampled_from(optional).flatmap(
    lambda x: strat.fixed_dictionaries(
        {**std, **dict(x)}
    )
)

This can all be wrapped in a function, let's call it valid_values(). You can't use @given(valid_values()) if you specify *args or **kwargs in the signature of the wrapped function.

As a result, test_model_properties(self, **kwargs) becomes test_model_properties(self, kwargs) (and you can use @given(kwargs=valid_values())) - by calling the dictionary kwargs, the rest of the function remains unchanged.

Note: This will not include an empty tuple if you want the possibility of no optional parameters, but that can be appended to the optional list easily. Alternatively, have range(n+1) instead of combinations(..., n+1), hence including a length of 0.

Upvotes: 2

jacg
jacg

Reputation: 2120

Your approach is guaranteed to fail, because, as the hypothesis docs imply

hypothesis.strategies.nothing()[source]

    This strategy never successfully draws a value and will always reject on an attempt to draw.

your attempt not to provide a value for b will always fail.

How about this:

from hypothesis.strategies import tuples, integers, booleans, one_of
B = booleans()
I = integers(min_value=0, max_value=10)
one_of(tuples(B), tuples(B, I)).example()

which, over a bunch of trials, gave me outputs such as (True,), (False, 9), (False, 4), (True, 5) and (False,).

You would, of course, use this with *args rather than **kwargs.

Upvotes: 2

Paul Rubenstein
Paul Rubenstein

Reputation: 342

How about:

def myfunc(a=None, b=None):
    if b is None:
        b=strat.nothing() 
        # Or whatever you would like b to be when you don't supply an argument
    else:
        <snip>

So you let b being the default value (in this case, None) trigger an 'if' condition inside myfunc() that sets it to something else.

Upvotes: -1

Related Questions