attrs/tests/strategies.py

217 lines
6.0 KiB
Python

# SPDX-License-Identifier: MIT
"""
Testing strategies for Hypothesis-based tests.
"""
import functools
import keyword
import string
from collections import OrderedDict
from hypothesis import strategies as st
import attr
from .utils import make_class
optional_bool = st.one_of(st.none(), st.booleans())
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
yield from lc
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:
yield val if not to_underscore else "_" + val
to_underscore = not to_underscore
@st.composite
def _create_hyp_nested_strategy(draw, 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.
"""
cls = draw(simple_class_strategy)
factories = [
cls,
lambda: [cls()],
lambda: (cls(),),
lambda: {"cls": cls()},
lambda: OrderedDict([("cls", cls())]),
]
factory = draw(st.sampled_from(factories))
attrs = [*draw(list_of_attrs), attr.ib(default=attr.Factory(factory))]
return make_class("HypClass", dict(zip(gen_attr_names(), attrs)))
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(allow_nan=False).map(lambda f: attr.ib(default=f))
dict_attrs = st.dictionaries(keys=st.text(), values=st.integers()).map(
lambda d: attr.ib(default=d)
)
simple_attrs_without_metadata = (
bare_attrs | int_attrs | str_attrs | float_attrs | dict_attrs
)
@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()
metadata = draw(
st.dictionaries(keys=keys, values=vals, min_size=1, max_size=3)
)
return attr.ib(
default=c_attr._default,
validator=c_attr._validator,
repr=c_attr.repr,
eq=c_attr.eq,
order=c_attr.order,
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=3)
@st.composite
def simple_classes(
draw,
slots=None,
frozen=None,
weakref_slot=None,
private_attrs=None,
cached_property=None,
):
"""
A strategy that generates classes with default non-attr attributes.
For example, this strategy might generate a class such as:
@attr.s(slots=True, frozen=True, weakref_slot=True)
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})
By default, all combinations of slots, frozen, and weakref_slot classes
will be generated. If `slots=True` is passed in, only slotted classes will
be generated, and if `slots=False` is passed in, no slotted classes will be
generated. The same applies to `frozen` and `weakref_slot`.
By default, some attributes will be private (those 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())
slots_flag = draw(st.booleans())
weakref_flag = draw(st.booleans())
if private_attrs is None:
attr_names = maybe_underscore_prefix(gen_attr_names())
elif private_attrs is True:
attr_names = ("_" + n for n in gen_attr_names())
elif private_attrs is False:
attr_names = gen_attr_names()
cls_dict = dict(zip(attr_names, attrs))
pre_init_flag = draw(st.booleans())
post_init_flag = draw(st.booleans())
init_flag = draw(st.booleans())
cached_property_flag = draw(st.booleans())
if pre_init_flag:
def pre_init(self):
pass
cls_dict["__attrs_pre_init__"] = pre_init
if post_init_flag:
def post_init(self):
pass
cls_dict["__attrs_post_init__"] = post_init
if not init_flag:
def init(self, *args, **kwargs):
self.__attrs_init__(*args, **kwargs)
cls_dict["__init__"] = init
bases = (object,)
if cached_property or (cached_property is None and cached_property_flag):
class BaseWithCachedProperty:
@functools.cached_property
def _cached_property(self) -> int:
return 1
bases = (BaseWithCachedProperty,)
return make_class(
"HypClass",
cls_dict,
bases=bases,
slots=slots_flag if slots is None else slots,
frozen=frozen_flag if frozen is None else frozen,
weakref_slot=weakref_flag if weakref_slot is None else weakref_slot,
init=init_flag,
)
# 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(
simple_classes(), _create_hyp_nested_strategy, max_leaves=3
)