diff --git a/changelog.d/280.change.rst b/changelog.d/280.change.rst new file mode 100644 index 000000000..edd21e9c1 --- /dev/null +++ b/changelog.d/280.change.rst @@ -0,0 +1,2 @@ +``attr.ib``\ ’s ``metadata`` argument now defaults to a unique empty ``dict`` instance instead of sharing a common empty ``dict`` for all. +The singleton empty ``dict`` is still enforced. diff --git a/src/attr/_make.py b/src/attr/_make.py index e0b2ea859..e93239e26 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -55,7 +55,7 @@ def __hash__(self): def attrib(default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, - convert=None, metadata={}, type=None): + convert=None, metadata=None, type=None): """ Create a new attribute on a class. @@ -134,6 +134,8 @@ def attrib(default=NOTHING, validator=None, raise TypeError( "Invalid value for hash. Must be True, False, or None." ) + if metadata is None: + metadata = {} return _CountingAttr( default=default, validator=validator, diff --git a/tests/test_make.py b/tests/test_make.py index 70cfaf3ec..ad06a5384 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function import inspect +import itertools import sys from operator import attrgetter @@ -26,7 +27,7 @@ from .utils import ( gen_attr_names, list_of_attrs, simple_attr, simple_attrs, - simple_attrs_without_metadata, simple_classes + simple_attrs_with_metadata, simple_attrs_without_metadata, simple_classes ) @@ -823,6 +824,34 @@ def test_empty_metadata_singleton(self, list_of_attrs): for a in fields(C)[1:]: assert a.metadata is fields(C)[0].metadata + @given(lists(simple_attrs_without_metadata, min_size=2, max_size=5)) + def test_empty_countingattr_metadata_independent(self, list_of_attrs): + """ + All empty metadata attributes are independent before ``@attr.s``. + """ + for x, y in itertools.combinations(list_of_attrs, 2): + assert x.metadata is not y.metadata + + @given(lists(simple_attrs_with_metadata(), min_size=2, max_size=5)) + def test_not_none_metadata(self, list_of_attrs): + """ + Non-empty metadata attributes exist as fields after ``@attr.s``. + """ + C = make_class("C", dict(zip(gen_attr_names(), list_of_attrs))) + + assert len(fields(C)) > 0 + + for cls_a, raw_a in zip(fields(C), list_of_attrs): + assert cls_a.metadata != {} + assert cls_a.metadata == raw_a.metadata + + def test_not_none_metadata_force_coverage(self): + """ + Force coverage of metadata is not None case even though other tests + should do so anyways. + """ + attr.ib(metadata={}) + class TestClassBuilder(object): """ diff --git a/tests/utils.py b/tests/utils.py index 36b624981..ea80cd486 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -145,7 +145,7 @@ def ordereddict_of_class(tup): attrs_and_classes.map(ordereddict_of_class)) -bare_attrs = st.just(attr.ib(default=None)) +bare_attrs = st.builds(attr.ib, default=st.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)) @@ -164,7 +164,8 @@ def simple_attrs_with_metadata(draw): c_attr = draw(simple_attrs) keys = st.booleans() | st.binary() | st.integers() | st.text() vals = st.booleans() | st.binary() | st.integers() | st.text() - metadata = draw(st.dictionaries(keys=keys, values=vals)) + metadata = draw(st.dictionaries( + keys=keys, values=vals, min_size=1, max_size=5)) return attr.ib(c_attr._default, c_attr._validator, c_attr.repr, c_attr.cmp, c_attr.hash, c_attr.init, c_attr.convert,