1""" 2Testing strategies for Hypothesis-based tests. 3""" 4 5import keyword 6import string 7 8from collections import OrderedDict 9 10from hypothesis import strategies as st 11 12import attr 13 14from .utils import make_class 15 16 17def gen_attr_names(): 18 """ 19 Generate names for attributes, 'a'...'z', then 'aa'...'zz'. 20 21 ~702 different attribute names should be enough in practice. 22 23 Some short strings (such as 'as') are keywords, so we skip them. 24 """ 25 lc = string.ascii_lowercase 26 for c in lc: 27 yield c 28 for outer in lc: 29 for inner in lc: 30 res = outer + inner 31 if keyword.iskeyword(res): 32 continue 33 yield outer + inner 34 35 36def maybe_underscore_prefix(source): 37 """ 38 A generator to sometimes prepend an underscore. 39 """ 40 to_underscore = False 41 for val in source: 42 yield val if not to_underscore else '_' + val 43 to_underscore = not to_underscore 44 45 46def _create_hyp_class(attrs): 47 """ 48 A helper function for Hypothesis to generate attrs classes. 49 """ 50 return make_class( 51 "HypClass", dict(zip(gen_attr_names(), attrs)) 52 ) 53 54 55def _create_hyp_nested_strategy(simple_class_strategy): 56 """ 57 Create a recursive attrs class. 58 59 Given a strategy for building (simpler) classes, create and return 60 a strategy for building classes that have as an attribute: either just 61 the simpler class, a list of simpler classes, a tuple of simpler classes, 62 an ordered dict or a dict mapping the string "cls" to a simpler class. 63 """ 64 # Use a tuple strategy to combine simple attributes and an attr class. 65 def just_class(tup): 66 combined_attrs = list(tup[0]) 67 combined_attrs.append(attr.ib(default=attr.Factory(tup[1]))) 68 return _create_hyp_class(combined_attrs) 69 70 def list_of_class(tup): 71 default = attr.Factory(lambda: [tup[1]()]) 72 combined_attrs = list(tup[0]) 73 combined_attrs.append(attr.ib(default=default)) 74 return _create_hyp_class(combined_attrs) 75 76 def tuple_of_class(tup): 77 default = attr.Factory(lambda: (tup[1](),)) 78 combined_attrs = list(tup[0]) 79 combined_attrs.append(attr.ib(default=default)) 80 return _create_hyp_class(combined_attrs) 81 82 def dict_of_class(tup): 83 default = attr.Factory(lambda: {"cls": tup[1]()}) 84 combined_attrs = list(tup[0]) 85 combined_attrs.append(attr.ib(default=default)) 86 return _create_hyp_class(combined_attrs) 87 88 def ordereddict_of_class(tup): 89 default = attr.Factory(lambda: OrderedDict([("cls", tup[1]())])) 90 combined_attrs = list(tup[0]) 91 combined_attrs.append(attr.ib(default=default)) 92 return _create_hyp_class(combined_attrs) 93 94 # A strategy producing tuples of the form ([list of attributes], <given 95 # class strategy>). 96 attrs_and_classes = st.tuples(list_of_attrs, simple_class_strategy) 97 98 return st.one_of(attrs_and_classes.map(just_class), 99 attrs_and_classes.map(list_of_class), 100 attrs_and_classes.map(tuple_of_class), 101 attrs_and_classes.map(dict_of_class), 102 attrs_and_classes.map(ordereddict_of_class)) 103 104 105bare_attrs = st.builds(attr.ib, default=st.none()) 106int_attrs = st.integers().map(lambda i: attr.ib(default=i)) 107str_attrs = st.text().map(lambda s: attr.ib(default=s)) 108float_attrs = st.floats().map(lambda f: attr.ib(default=f)) 109dict_attrs = (st.dictionaries(keys=st.text(), values=st.integers()) 110 .map(lambda d: attr.ib(default=d))) 111 112simple_attrs_without_metadata = (bare_attrs | int_attrs | str_attrs | 113 float_attrs | dict_attrs) 114 115 116@st.composite 117def simple_attrs_with_metadata(draw): 118 """ 119 Create a simple attribute with arbitrary metadata. 120 """ 121 c_attr = draw(simple_attrs) 122 keys = st.booleans() | st.binary() | st.integers() | st.text() 123 vals = st.booleans() | st.binary() | st.integers() | st.text() 124 metadata = draw(st.dictionaries( 125 keys=keys, values=vals, min_size=1, max_size=5)) 126 127 return attr.ib( 128 default=c_attr._default, 129 validator=c_attr._validator, 130 repr=c_attr.repr, 131 cmp=c_attr.cmp, 132 hash=c_attr.hash, 133 init=c_attr.init, 134 metadata=metadata, 135 type=None, 136 converter=c_attr.converter, 137 ) 138 139 140simple_attrs = simple_attrs_without_metadata | simple_attrs_with_metadata() 141 142# Python functions support up to 255 arguments. 143list_of_attrs = st.lists(simple_attrs, max_size=9) 144 145 146@st.composite 147def simple_classes(draw, slots=None, frozen=None, private_attrs=None): 148 """ 149 A strategy that generates classes with default non-attr attributes. 150 151 For example, this strategy might generate a class such as: 152 153 @attr.s(slots=True, frozen=True) 154 class HypClass: 155 a = attr.ib(default=1) 156 _b = attr.ib(default=None) 157 c = attr.ib(default='text') 158 _d = attr.ib(default=1.0) 159 c = attr.ib(default={'t': 1}) 160 161 By default, all combinations of slots and frozen classes will be generated. 162 If `slots=True` is passed in, only slots classes will be generated, and 163 if `slots=False` is passed in, no slot classes will be generated. The same 164 applies to `frozen`. 165 166 By default, some attributes will be private (i.e. prefixed with an 167 underscore). If `private_attrs=True` is passed in, all attributes will be 168 private, and if `private_attrs=False`, no attributes will be private. 169 """ 170 attrs = draw(list_of_attrs) 171 frozen_flag = draw(st.booleans()) if frozen is None else frozen 172 slots_flag = draw(st.booleans()) if slots is None else slots 173 174 if private_attrs is None: 175 attr_names = maybe_underscore_prefix(gen_attr_names()) 176 elif private_attrs is True: 177 attr_names = ('_' + n for n in gen_attr_names()) 178 elif private_attrs is False: 179 attr_names = gen_attr_names() 180 181 cls_dict = dict(zip(attr_names, attrs)) 182 post_init_flag = draw(st.booleans()) 183 if post_init_flag: 184 def post_init(self): 185 pass 186 cls_dict["__attrs_post_init__"] = post_init 187 188 return make_class( 189 "HypClass", 190 cls_dict, 191 slots=slots_flag, 192 frozen=frozen_flag, 193 ) 194 195 196# st.recursive works by taking a base strategy (in this case, simple_classes) 197# and a special function. This function receives a strategy, and returns 198# another strategy (building on top of the base strategy). 199nested_classes = st.recursive( 200 simple_classes(), 201 _create_hyp_nested_strategy, 202 max_leaves=10 203) 204