From fc2062ea0cd4bf38759815498eb153240ef2853f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 16 Jan 2018 19:09:23 +0100 Subject: [PATCH] Do not delete attributes from class body if these is passed (#323) Fixes #322 --- changelog.d/322.change.rst | 1 + changelog.d/323.change.rst | 1 + src/attr/_make.py | 42 +++++++++++++++++++++----------------- tests/test_make.py | 11 ++++++++++ 4 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 changelog.d/322.change.rst create mode 100644 changelog.d/323.change.rst diff --git a/changelog.d/322.change.rst b/changelog.d/322.change.rst new file mode 100644 index 00000000..874a0cec --- /dev/null +++ b/changelog.d/322.change.rst @@ -0,0 +1 @@ +If ``attr.s`` is passed a *these* argument, it will not attempt to remove attributes with the same name from the class body anymore. diff --git a/changelog.d/323.change.rst b/changelog.d/323.change.rst new file mode 100644 index 00000000..874a0cec --- /dev/null +++ b/changelog.d/323.change.rst @@ -0,0 +1 @@ +If ``attr.s`` is passed a *these* argument, it will not attempt to remove attributes with the same name from the class body anymore. diff --git a/src/attr/_make.py b/src/attr/_make.py index 74042e79..7868a718 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -128,13 +128,13 @@ def attrib(default=NOTHING, validator=None, .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* - .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. - .. versionchanged:: 17.1.0 - *hash* is ``None`` and therefore mirrors *cmp* by default. - .. versionadded:: 17.3.0 *type* - .. deprecated:: 17.4.0 *convert* - .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated - *convert* to achieve consistency with other noun-based arguments. + .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. + .. versionchanged:: 17.1.0 + *hash* is ``None`` and therefore mirrors *cmp* by default. + .. versionadded:: 17.3.0 *type* + .. deprecated:: 17.4.0 *convert* + .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated + *convert* to achieve consistency with other noun-based arguments. """ if hash is not None and hash is not True and hash is not False: raise TypeError( @@ -364,7 +364,7 @@ class _ClassBuilder(object): """ __slots__ = ( "_cls", "_cls_dict", "_attrs", "_super_names", "_attr_names", "_slots", - "_frozen", "_has_post_init", + "_frozen", "_has_post_init", "_delete_attribs", ) def __init__(self, cls, these, slots, frozen, auto_attribs): @@ -378,6 +378,7 @@ class _ClassBuilder(object): self._slots = slots self._frozen = frozen or _has_frozen_superclass(cls) self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) + self._delete_attribs = not bool(these) self._cls_dict["__attrs_attrs__"] = self._attrs @@ -407,10 +408,11 @@ class _ClassBuilder(object): super_names = self._super_names # Clean class of attribute definitions (`attr.ib()`s). - for name in self._attr_names: - if name not in super_names and \ - getattr(cls, name, None) is not None: - delattr(cls, name) + if self._delete_attribs: + for name in self._attr_names: + if name not in super_names and \ + getattr(cls, name, None) is not None: + delattr(cls, name) # Attach our dunder methods. for name, value in self._cls_dict.items(): @@ -575,7 +577,7 @@ def attrs(maybe_cls=None, these=None, repr_ns=None, Django models) or don't want to. If *these* is not ``None``, ``attrs`` will *not* search the class body - for attributes. + for attributes and will *not* remove any attributes from it. :type these: :class:`dict` of :class:`str` to :func:`attr.ib` @@ -656,13 +658,15 @@ def attrs(maybe_cls=None, these=None, repr_ns=None, .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/ - .. versionadded:: 16.0.0 *slots* - .. versionadded:: 16.1.0 *frozen* - .. versionadded:: 16.3.0 *str*, and support for ``__attrs_post_init__``. - .. versionchanged:: - 17.1.0 *hash* supports ``None`` as value which is also the default - now. + .. versionadded:: 16.0.0 *slots* + .. versionadded:: 16.1.0 *frozen* + .. versionadded:: 16.3.0 *str* + .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. + .. versionchanged:: 17.1.0 + *hash* supports ``None`` as value which is also the default now. .. versionadded:: 17.3.0 *auto_attribs* + .. versionchanged:: 18.1.0 + If *these* is passed, no attributes are deleted from the class body. """ def wrap(cls): if getattr(cls, "__class__", None) is None: diff --git a/tests/test_make.py b/tests/test_make.py index 9746a0c8..4107bb97 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -269,6 +269,17 @@ class TestTransformAttrs(object): simple_attr("x"), ) == attrs + def test_these_leave_body(self): + """ + If these is passed, no attributes are removed from the body. + """ + @attr.s(init=False, these={"x": attr.ib()}) + class C(object): + x = 5 + + assert 5 == C().x + assert "C(x=5)" == repr(C()) + def test_multiple_inheritance(self): """ Order of attributes doesn't get mixed up by multiple inheritance.