parent
ca2a1e10f0
commit
1b419e304b
|
@ -6,4 +6,4 @@ docs/_build/
|
|||
htmlcov
|
||||
dist
|
||||
.cache
|
||||
.hypothesis
|
||||
.hypothesis
|
|
@ -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.
|
||||
|
||||
|
|
13
docs/api.rst
13
docs/api.rst
|
@ -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
|
||||
|
|
|
@ -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
|
||||
--------
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue