From d10e5c41d614f8ca7b1b7a7c7a98f9dbe2d2b6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Fri, 3 Jun 2016 00:40:15 +0200 Subject: [PATCH] asdict - propagate dict_factory properly (#45) * asdict - propagate dict_factory properly. Hypothesis tests. * Disable Hypothesis health checks on Travis/PyPy. * Tox - pass the HYPOTHESIS_PROFILE environment variable. * Shut up pytest again. --- .travis.yml | 2 +- conftest.py | 6 ++++++ src/attr/_funcs.py | 14 ++++++++----- tests/__init__.py | 50 +++++++++++++++++++++++++++++++++++++++++++-- tests/test_funcs.py | 43 +++++++++++++++++++++++++++++++------- tox.ini | 1 + 6 files changed, 101 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 088f7dd7d..0f81e4cf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: - python: "3.5" env: TOXENV=py35 - python: "pypy" - env: TOXENV=pypy + env: TOXENV=pypy HYPOTHESIS_PROFILE=travis_pypy # Meta - python: "3.5" diff --git a/conftest.py b/conftest.py index 2fd79e9f6..1a4d6284d 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,9 @@ from __future__ import absolute_import, division, print_function +import os import pytest +from hypothesis import settings @pytest.fixture(scope="session") @@ -17,3 +19,7 @@ class C(object): y = attr() return C + +# PyPy on Travis appears to be too slow. +settings.register_profile("travis_pypy", settings(perform_health_check=False)) +settings.load_profile(os.getenv(u'HYPOTHESIS_PROFILE', 'default')) diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index 07cc7bf96..07f4495df 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -38,17 +38,21 @@ def asdict(inst, recurse=True, filter=None, dict_factory=dict): continue if recurse is True: if has(v.__class__): - rv[a.name] = asdict(v, recurse=True, filter=filter) + rv[a.name] = asdict(v, recurse=True, filter=filter, + dict_factory=dict_factory) elif isinstance(v, (tuple, list, set)): rv[a.name] = [ - asdict(i, recurse=True, filter=filter) + asdict(i, recurse=True, filter=filter, + dict_factory=dict_factory) if has(i.__class__) else i for i in v ] elif isinstance(v, dict): - rv[a.name] = dict((asdict(kk) if has(kk.__class__) else kk, - asdict(vv) if has(vv.__class__) else vv) - for kk, vv in iteritems(v)) + df = dict_factory + rv[a.name] = df(( + asdict(kk, dict_factory=df) if has(kk.__class__) else kk, + asdict(vv, dict_factory=df) if has(vv.__class__) else vv) + for kk, vv in iteritems(v)) else: rv[a.name] = v else: diff --git a/tests/__init__.py b/tests/__init__.py index 49b0648ec..407665560 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, division, print_function +import keyword import string from hypothesis import strategies as st @@ -51,13 +52,18 @@ def _gen_attr_names(): """ Generate names for attributes, 'a'...'z', then 'aa'...'zz'. - 702 different attribute names should be enough in practice. + ~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 @@ -67,11 +73,51 @@ def _create_hyp_class(attrs): """ return make_class('HypClass', dict(zip(_gen_attr_names(), attrs))) + +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 the simpler class as an + attribute. + """ + # 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 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) + + return st.one_of(st.tuples(st.lists(simple_attrs), simple_class_strategy) + .map(just_class), + st.tuples(st.lists(simple_attrs), simple_class_strategy) + .map(list_of_class)) + bare_attrs = st.just(attr.ib(default=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)) +dict_attrs = (st.dictionaries(keys=st.text(), values=st.integers()) + .map(lambda d: attr.ib(default=d))) -simple_attrs = st.one_of(bare_attrs, int_attrs, str_attrs, float_attrs) +simple_attrs = st.one_of(bare_attrs, int_attrs, str_attrs, float_attrs, + dict_attrs) simple_classes = st.lists(simple_attrs).map(_create_hyp_class) + +# Ok, so 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) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 660c67482..7dc47dd19 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -4,13 +4,13 @@ from __future__ import absolute_import, division, print_function -from collections import OrderedDict +from collections import OrderedDict, Sequence, Mapping import pytest from hypothesis import given, strategies as st -from . import simple_classes +from . import simple_classes, nested_classes from attr._funcs import ( asdict, @@ -42,7 +42,7 @@ def test_shallow(self, C, dict_factory): } == asdict(C(x=1, y=2), False, dict_factory=dict_factory) @given(st.sampled_from(MAPPING_TYPES)) - def test_recurse(self, C, dict_factory): + def test_recurse(self, C, dict_class): """ Deep asdict returns correct dict. """ @@ -52,7 +52,36 @@ def test_recurse(self, C, dict_factory): } == asdict(C( C(1, 2), C(3, 4), - ), dict_factory=dict_factory) + ), dict_factory=dict_class) + + @given(nested_classes, st.sampled_from(MAPPING_TYPES)) + def test_recurse_property(self, cls, dict_class): + """ + Property tests for recursive asdict. + """ + obj = cls() + obj_dict = asdict(obj, dict_factory=dict_class) + + def assert_proper_dict_class(obj, obj_dict): + assert isinstance(obj_dict, dict_class) + for field in fields(obj.__class__): + field_val = getattr(obj, field.name) + if has(field_val.__class__): + # This field holds a class, recurse the assertions. + assert_proper_dict_class(field_val, obj_dict[field.name]) + elif isinstance(field_val, Sequence): + dict_val = obj_dict[field.name] + for item, item_dict in zip(field_val, dict_val): + if has(item.__class__): + assert_proper_dict_class(item, item_dict) + elif isinstance(field_val, Mapping): + # This field holds a dictionary. + assert isinstance(obj_dict[field.name], dict_class) + for key, val in field_val.items(): + if has(val.__class__): + assert_proper_dict_class(val, obj_dict[key]) + + assert_proper_dict_class(obj, obj_dict) @given(st.sampled_from(MAPPING_TYPES)) def test_filter(self, C, dict_factory): @@ -89,14 +118,14 @@ def test_dicts(self, C, dict_factory): assert isinstance(res, dict_factory) @given(simple_classes, st.sampled_from(MAPPING_TYPES)) - def test_roundtrip(self, cls, dict_factory): + def test_roundtrip(self, cls, dict_class): """ Test dumping to dicts and back for Hypothesis-generated classes. """ instance = cls() - dict_instance = asdict(instance, dict_factory=dict_factory) + dict_instance = asdict(instance, dict_factory=dict_class) - assert isinstance(dict_instance, dict_factory) + assert isinstance(dict_instance, dict_class) roundtrip_instance = cls(**dict_instance) diff --git a/tox.ini b/tox.ini index 0d9689593..1b89a2d81 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = coverage-clean,py27,py34,py35,pypy,flake8,manifest,docs,readme,coverag [testenv] deps = -rdev-requirements.txt commands = coverage run --parallel -m pytest {posargs} +passenv = HYPOTHESIS_PROFILE [testenv:flake8]