2018-04-10 22:00:57 +00:00
|
|
|
"""
|
|
|
|
Testing strategies for Hypothesis-based tests.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import keyword
|
|
|
|
import string
|
|
|
|
|
|
|
|
from collections import OrderedDict
|
|
|
|
|
|
|
|
from hypothesis import strategies as st
|
|
|
|
|
|
|
|
import attr
|
|
|
|
|
|
|
|
from .utils import make_class
|
|
|
|
|
|
|
|
|
|
|
|
def gen_attr_names():
|
|
|
|
"""
|
|
|
|
Generate names for attributes, 'a'...'z', then 'aa'...'zz'.
|
|
|
|
|
|
|
|
~702 different attribute names should be enough in practice.
|
|
|
|
|
|
|
|
Some short strings (such as 'as') are keywords, so we skip them.
|
|
|
|
"""
|
|
|
|
lc = string.ascii_lowercase
|
|
|
|
for c in lc:
|
|
|
|
yield c
|
|
|
|
for outer in lc:
|
|
|
|
for inner in lc:
|
|
|
|
res = outer + inner
|
|
|
|
if keyword.iskeyword(res):
|
|
|
|
continue
|
|
|
|
yield outer + inner
|
|
|
|
|
|
|
|
|
|
|
|
def maybe_underscore_prefix(source):
|
|
|
|
"""
|
|
|
|
A generator to sometimes prepend an underscore.
|
|
|
|
"""
|
|
|
|
to_underscore = False
|
|
|
|
for val in source:
|
2018-06-10 17:40:07 +00:00
|
|
|
yield val if not to_underscore else "_" + val
|
2018-04-10 22:00:57 +00:00
|
|
|
to_underscore = not to_underscore
|
|
|
|
|
|
|
|
|
|
|
|
def _create_hyp_class(attrs):
|
|
|
|
"""
|
|
|
|
A helper function for Hypothesis to generate attrs classes.
|
|
|
|
"""
|
2018-06-10 17:40:07 +00:00
|
|
|
return make_class("HypClass", dict(zip(gen_attr_names(), attrs)))
|
2018-04-10 22:00:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _create_hyp_nested_strategy(simple_class_strategy):
|
|
|
|
"""
|
|
|
|
Create a recursive attrs class.
|
|
|
|
|
|
|
|
Given a strategy for building (simpler) classes, create and return
|
|
|
|
a strategy for building classes that have as an attribute: either just
|
|
|
|
the simpler class, a list of simpler classes, a tuple of simpler classes,
|
|
|
|
an ordered dict or a dict mapping the string "cls" to a simpler class.
|
|
|
|
"""
|
|
|
|
# Use a tuple strategy to combine simple attributes and an attr class.
|
|
|
|
def just_class(tup):
|
|
|
|
combined_attrs = list(tup[0])
|
|
|
|
combined_attrs.append(attr.ib(default=attr.Factory(tup[1])))
|
|
|
|
return _create_hyp_class(combined_attrs)
|
|
|
|
|
|
|
|
def list_of_class(tup):
|
|
|
|
default = attr.Factory(lambda: [tup[1]()])
|
|
|
|
combined_attrs = list(tup[0])
|
|
|
|
combined_attrs.append(attr.ib(default=default))
|
|
|
|
return _create_hyp_class(combined_attrs)
|
|
|
|
|
|
|
|
def tuple_of_class(tup):
|
|
|
|
default = attr.Factory(lambda: (tup[1](),))
|
|
|
|
combined_attrs = list(tup[0])
|
|
|
|
combined_attrs.append(attr.ib(default=default))
|
|
|
|
return _create_hyp_class(combined_attrs)
|
|
|
|
|
|
|
|
def dict_of_class(tup):
|
|
|
|
default = attr.Factory(lambda: {"cls": tup[1]()})
|
|
|
|
combined_attrs = list(tup[0])
|
|
|
|
combined_attrs.append(attr.ib(default=default))
|
|
|
|
return _create_hyp_class(combined_attrs)
|
|
|
|
|
|
|
|
def ordereddict_of_class(tup):
|
|
|
|
default = attr.Factory(lambda: OrderedDict([("cls", tup[1]())]))
|
|
|
|
combined_attrs = list(tup[0])
|
|
|
|
combined_attrs.append(attr.ib(default=default))
|
|
|
|
return _create_hyp_class(combined_attrs)
|
|
|
|
|
|
|
|
# A strategy producing tuples of the form ([list of attributes], <given
|
|
|
|
# class strategy>).
|
|
|
|
attrs_and_classes = st.tuples(list_of_attrs, simple_class_strategy)
|
|
|
|
|
2018-06-10 17:40:07 +00:00
|
|
|
return st.one_of(
|
|
|
|
attrs_and_classes.map(just_class),
|
|
|
|
attrs_and_classes.map(list_of_class),
|
|
|
|
attrs_and_classes.map(tuple_of_class),
|
|
|
|
attrs_and_classes.map(dict_of_class),
|
|
|
|
attrs_and_classes.map(ordereddict_of_class),
|
|
|
|
)
|
2018-04-10 22:00:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
bare_attrs = st.builds(attr.ib, default=st.none())
|
|
|
|
int_attrs = st.integers().map(lambda i: attr.ib(default=i))
|
|
|
|
str_attrs = st.text().map(lambda s: attr.ib(default=s))
|
|
|
|
float_attrs = st.floats().map(lambda f: attr.ib(default=f))
|
2018-06-10 17:40:07 +00:00
|
|
|
dict_attrs = st.dictionaries(keys=st.text(), values=st.integers()).map(
|
|
|
|
lambda d: attr.ib(default=d)
|
|
|
|
)
|
2018-04-10 22:00:57 +00:00
|
|
|
|
2018-06-10 17:40:07 +00:00
|
|
|
simple_attrs_without_metadata = (
|
|
|
|
bare_attrs | int_attrs | str_attrs | float_attrs | dict_attrs
|
|
|
|
)
|
2018-04-10 22:00:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
@st.composite
|
|
|
|
def simple_attrs_with_metadata(draw):
|
|
|
|
"""
|
|
|
|
Create a simple attribute with arbitrary metadata.
|
|
|
|
"""
|
|
|
|
c_attr = draw(simple_attrs)
|
|
|
|
keys = st.booleans() | st.binary() | st.integers() | st.text()
|
|
|
|
vals = st.booleans() | st.binary() | st.integers() | st.text()
|
2018-06-10 17:40:07 +00:00
|
|
|
metadata = draw(
|
|
|
|
st.dictionaries(keys=keys, values=vals, min_size=1, max_size=5)
|
|
|
|
)
|
2018-04-10 22:00:57 +00:00
|
|
|
|
|
|
|
return attr.ib(
|
|
|
|
default=c_attr._default,
|
|
|
|
validator=c_attr._validator,
|
|
|
|
repr=c_attr.repr,
|
|
|
|
cmp=c_attr.cmp,
|
|
|
|
hash=c_attr.hash,
|
|
|
|
init=c_attr.init,
|
|
|
|
metadata=metadata,
|
|
|
|
type=None,
|
|
|
|
converter=c_attr.converter,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
simple_attrs = simple_attrs_without_metadata | simple_attrs_with_metadata()
|
|
|
|
|
|
|
|
# Python functions support up to 255 arguments.
|
|
|
|
list_of_attrs = st.lists(simple_attrs, max_size=9)
|
|
|
|
|
|
|
|
|
|
|
|
@st.composite
|
2018-08-25 07:01:02 +00:00
|
|
|
def simple_classes(
|
|
|
|
draw, slots=None, frozen=None, weakref_slot=None, private_attrs=None
|
|
|
|
):
|
2018-04-10 22:00:57 +00:00
|
|
|
"""
|
|
|
|
A strategy that generates classes with default non-attr attributes.
|
|
|
|
|
|
|
|
For example, this strategy might generate a class such as:
|
|
|
|
|
2018-08-25 07:01:02 +00:00
|
|
|
@attr.s(slots=True, frozen=True, weakref_slot=True)
|
2018-04-10 22:00:57 +00:00
|
|
|
class HypClass:
|
|
|
|
a = attr.ib(default=1)
|
|
|
|
_b = attr.ib(default=None)
|
|
|
|
c = attr.ib(default='text')
|
|
|
|
_d = attr.ib(default=1.0)
|
|
|
|
c = attr.ib(default={'t': 1})
|
|
|
|
|
2018-08-25 07:01:02 +00:00
|
|
|
By default, all combinations of slots, frozen, and weakref_slot classes
|
2019-02-25 15:09:57 +00:00
|
|
|
will be generated. If `slots=True` is passed in, only slotted classes will
|
2019-03-21 14:37:07 +00:00
|
|
|
be generated, and if `slots=False` is passed in, no slotted classes will be
|
2018-08-25 07:01:02 +00:00
|
|
|
generated. The same applies to `frozen` and `weakref_slot`.
|
2018-04-10 22:00:57 +00:00
|
|
|
|
|
|
|
By default, some attributes will be private (i.e. prefixed with an
|
|
|
|
underscore). If `private_attrs=True` is passed in, all attributes will be
|
|
|
|
private, and if `private_attrs=False`, no attributes will be private.
|
|
|
|
"""
|
|
|
|
attrs = draw(list_of_attrs)
|
|
|
|
frozen_flag = draw(st.booleans()) if frozen is None else frozen
|
|
|
|
slots_flag = draw(st.booleans()) if slots is None else slots
|
2018-08-25 07:01:02 +00:00
|
|
|
weakref_slot_flag = (
|
|
|
|
draw(st.booleans()) if weakref_slot is None else weakref_slot
|
|
|
|
)
|
2018-04-10 22:00:57 +00:00
|
|
|
|
|
|
|
if private_attrs is None:
|
|
|
|
attr_names = maybe_underscore_prefix(gen_attr_names())
|
|
|
|
elif private_attrs is True:
|
2018-06-10 17:40:07 +00:00
|
|
|
attr_names = ("_" + n for n in gen_attr_names())
|
2018-04-10 22:00:57 +00:00
|
|
|
elif private_attrs is False:
|
|
|
|
attr_names = gen_attr_names()
|
|
|
|
|
|
|
|
cls_dict = dict(zip(attr_names, attrs))
|
|
|
|
post_init_flag = draw(st.booleans())
|
|
|
|
if post_init_flag:
|
2018-06-10 17:40:07 +00:00
|
|
|
|
2018-04-10 22:00:57 +00:00
|
|
|
def post_init(self):
|
|
|
|
pass
|
2018-06-10 17:40:07 +00:00
|
|
|
|
2018-04-10 22:00:57 +00:00
|
|
|
cls_dict["__attrs_post_init__"] = post_init
|
|
|
|
|
|
|
|
return make_class(
|
2018-08-25 07:01:02 +00:00
|
|
|
"HypClass",
|
|
|
|
cls_dict,
|
|
|
|
slots=slots_flag,
|
|
|
|
frozen=frozen_flag,
|
|
|
|
weakref_slot=weakref_slot_flag,
|
2018-04-10 22:00:57 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# st.recursive works by taking a base strategy (in this case, simple_classes)
|
|
|
|
# and a special function. This function receives a strategy, and returns
|
|
|
|
# another strategy (building on top of the base strategy).
|
|
|
|
nested_classes = st.recursive(
|
2018-06-10 17:40:07 +00:00
|
|
|
simple_classes(), _create_hyp_nested_strategy, max_leaves=10
|
2018-04-10 22:00:57 +00:00
|
|
|
)
|