Skip to content

Implement astuple method #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -7,3 +7,4 @@ htmlcov
dist
.cache
.hypothesis
.venv*
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@ Changes:

- Converts now work with frozen classes.
`#76 <https://github.com/hynek/attrs/issues/76>`_
- Implements ``astuple`` method.
`#77 <https://github.com/hynek/attrs/issues/77>`_
- Instantiation of ``attrs`` classes with converters is now significantly faster.
`#80 <https://github.com/hynek/attrs/pull/80>`_
- Pickling now works with ``__slots__`` classes.
2 changes: 2 additions & 0 deletions src/attr/__init__.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

from ._funcs import (
asdict,
astuple,
assoc,
has,
)
@@ -46,6 +47,7 @@
"Factory",
"NOTHING",
"asdict",
"astuple",
"assoc",
"attr",
"attrib",
67 changes: 67 additions & 0 deletions src/attr/_funcs.py
Original file line number Diff line number Diff line change
@@ -67,6 +67,73 @@ def asdict(inst, recurse=True, filter=None, dict_factory=dict,
return rv


def astuple(inst, recurse=True, filter=None, tuple_factory=tuple,
retain_collection_types=False):
"""
Return the ``attrs`` attribute values of *inst* as a tuple.

Optionally recurse into other ``attrs``-decorated classes.

:param inst: Instance of an ``attrs``-decorated class.
:param bool recurse: Recurse into classes that are also
``attrs``-decorated.
:param callable filter: A callable whose return code determines whether an
attribute or element is included (``True``) or dropped (``False``). Is
called with the :class:`attr.Attribute` as the first argument and the
value as the second argument.
:param callable tuple_factory: A callable to produce tuples from. For
example, to produce list instead of tuples.
:param bool retain_collection_types: Do not convert to ``list``
or ``dict`` when encountering an attribute which is type
``tuple``, ``dict`` or ``set``.
Only meaningful if ``recurse`` is ``True``.

:rtype: return type of *tuple_factory*
"""
attrs = fields(inst.__class__)
rv = []
retain = retain_collection_types # Very long. :/
for a in attrs:
v = getattr(inst, a.name)
if filter is not None and not filter(a, v):
continue
if recurse is True:
if has(v.__class__):
rv.append(astuple(v, recurse=True, filter=filter,
tuple_factory=tuple_factory,
retain_collection_types=retain))
elif isinstance(v, (tuple, list, set)):
cf = v.__class__ if retain is True else list
rv.append(cf([
astuple(j, recurse=True, filter=filter,
tuple_factory=tuple_factory,
retain_collection_types=retain)
if has(j.__class__) else j
for j in v
]))
elif isinstance(v, dict):
df = v.__class__ if retain is True else dict
rv.append(df(
(
astuple(
kk,
tuple_factory=tuple_factory,
retain_collection_types=retain
) if has(kk.__class__) else kk,
astuple(
vv,
tuple_factory=tuple_factory,
retain_collection_types=retain
) if has(vv.__class__) else vv
)
for kk, vv in iteritems(v)))
else:
rv.append(v)
else:
rv.append(v)
return rv if tuple_factory is list else tuple_factory(rv)


def has(cls):
"""
Check whether *cls* is a class with ``attrs`` attributes.
148 changes: 147 additions & 1 deletion tests/test_funcs.py
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@

from attr._funcs import (
asdict,
astuple,
assoc,
has,
)
@@ -30,7 +31,7 @@

class TestAsDict(object):
"""
Tests for `asdict`.
Tests for `astuple`.
"""
@given(st.sampled_from(MAPPING_TYPES))
def test_shallow(self, C, dict_factory):
@@ -156,6 +157,151 @@ def test_asdict_preserve_order(self, cls):
assert [a.name for a in fields(cls)] == list(dict_instance.keys())


class TestAsTuple(object):
"""
Tests for `astuple`.
"""

@given(st.sampled_from(SEQUENCE_TYPES))
def test_shallow(self, C, tuple_factory):
"""
Shallow astuple returns correct dict.
"""
assert (tuple_factory([1, 2]) ==
astuple(C(x=1, y=2), False, tuple_factory=tuple_factory))

@given(st.sampled_from(SEQUENCE_TYPES))
def test_recurse(self, C, tuple_factory):
"""
Deep astuple returns correct tuple.
"""
assert (tuple_factory([tuple_factory([1, 2]),
tuple_factory([3, 4])])
== astuple(C(
C(1, 2),
C(3, 4),
),
tuple_factory=tuple_factory))

@given(nested_classes, st.sampled_from(SEQUENCE_TYPES))
def test_recurse_property(self, cls, tuple_class):
"""
Property tests for recursive astuple.
"""
obj = cls()
obj_tuple = astuple(obj, tuple_factory=tuple_class)

def assert_proper_tuple_class(obj, obj_tuple):
assert isinstance(obj_tuple, tuple_class)
for index, field in enumerate(fields(obj.__class__)):
field_val = getattr(obj, field.name)
if has(field_val.__class__):
# This field holds a class, recurse the assertions.
assert_proper_tuple_class(field_val, obj_tuple[index])

assert_proper_tuple_class(obj, obj_tuple)

@given(nested_classes, st.sampled_from(SEQUENCE_TYPES))
def test_recurse_retain(self, cls, tuple_class):
"""
Property tests for asserting collection types are retained.
"""
obj = cls()
obj_tuple = astuple(obj, tuple_factory=tuple_class,
retain_collection_types=True)

def assert_proper_col_class(obj, obj_tuple):
# Iterate over all attributes, and if they are lists or mappings
# in the original, assert they are the same class in the dumped.
for index, field in enumerate(fields(obj.__class__)):
field_val = getattr(obj, field.name)
if has(field_val.__class__):
# This field holds a class, recurse the assertions.
assert_proper_col_class(field_val, obj_tuple[index])
elif isinstance(field_val, (list, tuple)):
# This field holds a sequence of something.
assert type(field_val) is type(obj_tuple[index]) # noqa: E721
for obj_e, obj_tuple_e in zip(field_val, obj_tuple[index]):
if has(obj_e.__class__):
assert_proper_col_class(obj_e, obj_tuple_e)
elif isinstance(field_val, dict):
orig = field_val
tupled = obj_tuple[index]
assert type(orig) is type(tupled) # noqa: E721
for obj_e, obj_tuple_e in zip(orig.items(),
tupled.items()):
if has(obj_e[0].__class__): # Dict key
assert_proper_col_class(obj_e[0], obj_tuple_e[0])
if has(obj_e[1].__class__): # Dict value
assert_proper_col_class(obj_e[1], obj_tuple_e[1])

assert_proper_col_class(obj, obj_tuple)

@given(st.sampled_from(SEQUENCE_TYPES))
def test_filter(self, C, tuple_factory):
"""
Attributes that are supposed to be skipped are skipped.
"""
assert tuple_factory([tuple_factory([1, ]), ]) == astuple(C(
C(1, 2),
C(3, 4),
), filter=lambda a, v: a.name != "y", tuple_factory=tuple_factory)

@given(container=st.sampled_from(SEQUENCE_TYPES))
def test_lists_tuples(self, container, C):
"""
If recurse is True, also recurse into lists.
"""
assert ((1, [(2, 3), (4, 5), "a"])
== astuple(C(1, container([C(2, 3), C(4, 5), "a"])))
)

@given(st.sampled_from(SEQUENCE_TYPES))
def test_dicts(self, C, tuple_factory):
"""
If recurse is True, also recurse into dicts.
"""
res = astuple(C(1, {"a": C(4, 5)}), tuple_factory=tuple_factory)
assert tuple_factory([1, {"a": tuple_factory([4, 5])}]) == res
assert isinstance(res, tuple_factory)

@given(container=st.sampled_from(SEQUENCE_TYPES))
def test_lists_tuples_retain_type(self, container, C):
"""
If recurse and retain_collection_types are True, also recurse
into lists and do not convert them into list.
"""
assert (
(1, container([(2, 3), (4, 5), "a"]))
== astuple(C(1, container([C(2, 3), C(4, 5), "a"])),
retain_collection_types=True))

@given(container=st.sampled_from(MAPPING_TYPES))
def test_dicts_retain_type(self, container, C):
"""
If recurse and retain_collection_types are True, also recurse
into lists and do not convert them into list.
"""
assert (
(1, container({"a": (4, 5)}))
== astuple(C(1, container({"a": C(4, 5)})),
retain_collection_types=True))

@given(simple_classes(), st.sampled_from(SEQUENCE_TYPES))
def test_roundtrip(self, cls, tuple_class):
"""
Test dumping to tuple and back for Hypothesis-generated classes.
"""
instance = cls()
tuple_instance = astuple(instance, tuple_factory=tuple_class)

assert isinstance(tuple_instance, tuple_class)

roundtrip_instance = cls(*tuple_instance)

assert instance == roundtrip_instance


class TestHas(object):
"""
Tests for `has`.
22 changes: 19 additions & 3 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@
import keyword
import string

from collections import OrderedDict

from hypothesis import strategies as st

import attr
@@ -85,8 +87,8 @@ def _create_hyp_nested_strategy(simple_class_strategy):

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, or a dict mapping the string
"cls" to a simpler class.
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.
"""
# Use a tuple strategy to combine simple attributes and an attr class.
def just_class(tup):
@@ -100,19 +102,33 @@ def list_of_class(tup):
combined_attrs.append(attr.ib(default=default))
return _create_hyp_class(combined_attrs)

def tuple_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)

def ordereddict_of_class(tup):
default = attr.Factory(lambda: OrderedDict([("cls", tup[1]())]))
combined_attrs = list(tup[0])
combined_attrs.append(attr.ib(default=default))
return _create_hyp_class(combined_attrs)

# A strategy producing tuples of the form ([list of attributes], <given
# class strategy>).
attrs_and_classes = st.tuples(list_of_attrs, simple_class_strategy)

return st.one_of(attrs_and_classes.map(just_class),
attrs_and_classes.map(list_of_class),
attrs_and_classes.map(dict_of_class))
attrs_and_classes.map(tuple_of_class),
attrs_and_classes.map(dict_of_class),
attrs_and_classes.map(ordereddict_of_class))

bare_attrs = st.just(attr.ib(default=None))
int_attrs = st.integers().map(lambda i: attr.ib(default=i))