Add attr.astuple

Fixes #77 and fixes #78
This commit is contained in:
inso 2016-09-11 08:54:39 +02:00 committed by Hynek Schlawack
parent ca2a1e10f0
commit 1b419e304b
9 changed files with 291 additions and 16 deletions

2
.gitignore vendored
View File

@ -6,4 +6,4 @@ docs/_build/
htmlcov
dist
.cache
.hypothesis
.hypothesis

View File

@ -11,15 +11,17 @@ The third digit is only for regressions.
Changes:
^^^^^^^^
- Add ``attr.astuple()`` that -- similarly to ``attr.asdict()`` -- returns the instance as a tuple.
`#77 <https://github.com/hynek/attrs/issues/77>`_
- Converts now work with frozen classes.
`#76 <https://github.com/hynek/attrs/issues/76>`_
- Instantiation of ``attrs`` classes with converters is now significantly faster.
`#80 <https://github.com/hynek/attrs/pull/80>`_
- Pickling now works with ``__slots__`` classes.
`#81 <https://github.com/hynek/attrs/issues/81>`_
- ``attr.assoc`` now works with ``__slots__`` classes.
- ``attr.assoc()`` now works with ``__slots__`` classes.
`#84 <https://github.com/hynek/attrs/issues/84>`_
- The tuple returned by ``attr.fields`` now also allows to access the ``Attribute`` instances by name.
- The tuple returned by ``attr.fields()`` now also allows to access the ``Attribute`` instances by name.
Yes, we've subclassed ``tuple`` so you don't have to!
Therefore ``attr.fields(C).x`` is equivalent to the deprecated ``C.x`` and works with ``__slots__`` classes.
`#88 <https://github.com/hynek/attrs/issues/88>`_
@ -43,7 +45,7 @@ Deprecations:
^^^^^^^^^^^^^
- Accessing ``Attribute`` instances on class objects is now deprecated and will stop working in 2017.
If you need introspection please use the ``__attrs_attrs__`` attribute or the ``attr.fields`` function that carry them too.
If you need introspection please use the ``__attrs_attrs__`` attribute or the ``attr.fields()`` function that carry them too.
In the future, the attributes that are defined on the class body and are usually overwritten in your ``__init__`` method are simply removed after ``@attr.s`` has been applied.
This will remove the confusing error message if you write your own ``__init__`` and forget to initialize some attribute.
@ -56,15 +58,15 @@ Deprecations:
Changes:
^^^^^^^^
- ``attr.asdict``\ 's ``dict_factory`` arguments is now propagated on recursion.
- ``attr.asdict()``\ 's ``dict_factory`` arguments is now propagated on recursion.
`#45 <https://github.com/hynek/attrs/issues/45>`_
- ``attr.asdict``, ``attr.has`` and ``attr.fields`` are significantly faster.
- ``attr.asdict()``, ``attr.has()`` and ``attr.fields()`` are significantly faster.
`#48 <https://github.com/hynek/attrs/issues/48>`_
`#51 <https://github.com/hynek/attrs/issues/51>`_
- Add ``attr.attrs`` and ``attr.attrib`` as a more consistent aliases for ``attr.s`` and ``attr.ib``.
- Add ``frozen`` option to ``attr.s`` that will make instances best-effort immutable.
`#60 <https://github.com/hynek/attrs/issues/60>`_
- ``attr.asdict`` now takes ``retain_collection_types`` as an argument.
- ``attr.asdict()`` now takes ``retain_collection_types`` as an argument.
If ``True``, it does not convert attributes of type ``tuple`` or ``set`` to ``list``.
`#69 <https://github.com/hynek/attrs/issues/69>`_
@ -95,7 +97,7 @@ Changes:
- Allow the case of initializing attributes that are set to ``init=False``.
This allows for clean initializer parameter lists while being able to initialize attributes to default values.
`#32 <https://github.com/hynek/attrs/issues/32>`_
- ``attr.asdict`` can now produce arbitrary mappings instead of Python ``dict``\ s when provided with a ``dict_factory`` argument.
- ``attr.asdict()`` can now produce arbitrary mappings instead of Python ``dict``\ s when provided with a ``dict_factory`` argument.
`#40 <https://github.com/hynek/attrs/issues/40>`_
- Multiple performance improvements.

View File

@ -161,6 +161,19 @@ Helpers
{'y': {'y': 3, 'x': 2}, 'x': 1}
.. autofunction:: attr.astuple
For example:
.. doctest::
>>> @attr.s
... class C(object):
... x = attr.ib()
... y = attr.ib()
>>> attr.astuple(C(1,2))
(1, 2)
``attrs`` includes some handy helpers for filtering:
.. autofunction:: attr.filters.include

View File

@ -162,8 +162,8 @@ On Python 3 it overrides the implicit detection.
.. _asdict:
Converting to Dictionaries
--------------------------
Converting to Collections Types
-------------------------------
When you have a class with data, it often is very convenient to transform that class into a :class:`dict` (for example if you want to serialize it to JSON):
@ -210,6 +210,29 @@ For the common case where you want to :func:`include <attr.filters.include>` or
... filter=attr.filters.include(int, attr.fields(C).x))
{'z': 3, 'x': 'foo'}
Other times, all you want is a tuple and ``attrs`` won't let you down:
.. doctest::
>>> import sqlite3
>>> import attr
>>> @attr.s
... class Foo:
... a = attr.ib()
... b = attr.ib()
>>> foo = Foo(2, 3)
>>> with sqlite3.connect(":memory:") as conn:
... c = conn.cursor()
... c.execute("CREATE TABLE foo (x INTEGER PRIMARY KEY ASC, y)") #doctest: +ELLIPSIS
... c.execute("INSERT INTO foo VALUES (?, ?)", attr.astuple(foo)) #doctest: +ELLIPSIS
... foo2 = Foo(*c.execute("SELECT x, y FROM foo").fetchone())
<sqlite3.Cursor object at ...>
<sqlite3.Cursor object at ...>
>>> foo == foo2
True
Defaults
--------

View File

@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function
from ._funcs import (
asdict,
assoc,
astuple,
has,
)
from ._make import (
@ -46,6 +47,7 @@ __all__ = [
"Factory",
"NOTHING",
"asdict",
"astuple",
"assoc",
"attr",
"attrib",

View File

@ -25,7 +25,7 @@ def asdict(inst, recurse=True, filter=None, dict_factory=dict,
example, to produce ordered dictionaries instead of normal Python
dictionaries, pass in ``collections.OrderedDict``.
:param bool retain_collection_types: Do not convert to ``list`` when
encountering an attribute which is type ``tuple`` or ``set``. Only
encountering an attribute whose type is ``tuple`` or ``set``. Only
meaningful if ``recurse`` is ``True``.
:rtype: return type of *dict_factory*
@ -67,6 +67,78 @@ 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 lists instead of tuples.
:param bool retain_collection_types: Do not convert to ``list``
or ``dict`` when encountering an attribute which type is
``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is
``True``.
:rtype: return type of *tuple_factory*
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
.. versionadded:: 16.2.0
"""
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.

View File

@ -6,7 +6,9 @@ class FrozenInstanceError(AttributeError):
A frozen/immutable instance has been attempted to be modified.
It mirrors the behavior of ``namedtuples`` by using the same error message
and subclassing :exc:`AttributeError``.
and subclassing :exc:`AttributeError`.
.. versionadded:: 16.1.0
"""
msg = "can't set attribute"
args = [msg]

View File

@ -15,6 +15,7 @@ from .utils import simple_classes, nested_classes
from attr._funcs import (
asdict,
assoc,
astuple,
has,
)
from attr._make import (
@ -30,7 +31,7 @@ SEQUENCE_TYPES = (list, tuple)
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,150 @@ class TestAsDict(object):
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`.

View File

@ -7,6 +7,8 @@ from __future__ import absolute_import, division, print_function
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 _create_hyp_nested_strategy(simple_class_strategy):
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))