From 0c8211d9222b49b8fa1aafd8f464459e70d8b5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Mon, 5 Dec 2016 09:50:50 +0100 Subject: [PATCH] Feature/better simple classes (#119) --- tests/test_funcs.py | 22 +++++++++++++++------- tests/utils.py | 29 +++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 47380804..772e60da 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -132,10 +132,13 @@ class TestAsDict(object): } == res assert isinstance(res, dict_factory) - @given(simple_classes(), st.sampled_from(MAPPING_TYPES)) + @given(simple_classes(private_attrs=False), st.sampled_from(MAPPING_TYPES)) def test_roundtrip(self, cls, dict_class): """ Test dumping to dicts and back for Hypothesis-generated classes. + + Private attributes don't round-trip (the attribute name is different + than the initializer argument). """ instance = cls() dict_instance = asdict(instance, dict_factory=dict_class) @@ -359,22 +362,27 @@ class TestAssoc(object): assert i1 is not i2 assert i1 == i2 - @given(simple_classes(), st.integers()) - def test_change(self, C, val): + @given(simple_classes(), st.data()) + def test_change(self, C, data): """ Changes work. """ # Take the first attribute, and change it. assume(fields(C)) # Skip classes with no attributes. + field_names = [a.name for a in fields(C)] original = C() - attribute = fields(C)[0] - changed = assoc(original, **{attribute.name: val}) - assert getattr(changed, attribute.name) == val + chosen_names = data.draw(st.sets(st.sampled_from(field_names))) + change_dict = {name: data.draw(st.integers()) + for name in chosen_names} + changed = assoc(original, **change_dict) + for k, v in change_dict.items(): + assert getattr(changed, k) == v @given(simple_classes()) def test_unknown(self, C): """ - Wanting to change an unknown attribute raises a ValueError. + Wanting to change an unknown attribute raises an + AttrsAttributeNotFoundError. """ # No generated class will have a four letter attribute. with pytest.raises(AttrsAttributeNotFoundError) as e: diff --git a/tests/utils.py b/tests/utils.py index c3f6cd35..3b6a5a84 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -74,6 +74,16 @@ def gen_attr_names(): 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 + + def _create_hyp_class(attrs): """ A helper function for Hypothesis to generate attrs classes. @@ -164,7 +174,7 @@ list_of_attrs = st.lists(simple_attrs, average_size=3, max_size=9) @st.composite -def simple_classes(draw, slots=None, frozen=None): +def simple_classes(draw, slots=None, frozen=None, private_attrs=None): """ A strategy that generates classes with default non-attr attributes. @@ -173,21 +183,32 @@ def simple_classes(draw, slots=None, frozen=None): @attr.s(slots=True, frozen=True) class HypClass: a = attr.ib(default=1) - b = attr.ib(default=None) + _b = attr.ib(default=None) c = attr.ib(default='text') - d = attr.ib(default=1.0) + _d = attr.ib(default=1.0) c = attr.ib(default={'t': 1}) By default, all combinations of slots and frozen classes will be generated. If `slots=True` is passed in, only slots classes will be generated, and if `slots=False` is passed in, no slot classes will be generated. The same applies to `frozen`. + + 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 - cls_dict = dict(zip(gen_attr_names(), attrs)) + 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)) post_init_flag = draw(st.booleans()) if post_init_flag: def post_init(self):