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