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
Expand Up @@ -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
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/attr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from ._funcs import (
asdict,
astuple,
assoc,
has,
)
Expand Down Expand Up @@ -46,6 +47,7 @@
"Factory",
"NOTHING",
"asdict",
"astuple",
"assoc",
"attr",
"attrib",
Expand Down
67 changes: 67 additions & 0 deletions src/attr/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
148 changes: 147 additions & 1 deletion tests/test_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from attr._funcs import (
asdict,
astuple,
assoc,
has,
)
Expand All @@ -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):
Expand Down Expand Up @@ -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`.
Expand Down
22 changes: 19 additions & 3 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import keyword
import string

from collections import OrderedDict

from hypothesis import strategies as st

import attr
Expand Down Expand Up @@ -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):
Expand All @@ -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))
Expand Down