Skip to content

Commit

Permalink
asdict - propagate dict_factory properly (#45)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
Tinche authored and hynek committed Jun 2, 2016
1 parent e83f0e5 commit d10e5c4
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions 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")
Expand All @@ -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'))
14 changes: 9 additions & 5 deletions src/attr/_funcs.py
Expand Up @@ -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:
Expand Down
50 changes: 48 additions & 2 deletions 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
Expand Down Expand Up @@ -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


Expand All @@ -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)
43 changes: 36 additions & 7 deletions tests/test_funcs.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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):
Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions tox.ini
Expand Up @@ -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]
Expand Down

0 comments on commit d10e5c4

Please sign in to comment.