diff --git a/AUTHORS.rst b/AUTHORS.rst index 73eae215..f14ef6c6 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -8,4 +8,4 @@ The development is kindly supported by `Variomedia AG `_. It’s the spiritual successor of `characteristic `_ and aspires to fix some of it clunkiness and unfortunate decisions. -Both were inspired by Twisted’s `FancyEqMixin `_ but both are implemented using class decorators because `sub-classing is bad for you `_, m’kay? +Both were inspired by Twisted’s `FancyEqMixin `_ but both are implemented using class decorators because `subclassing is bad for you `_, m’kay? diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 08252de8..a7fad311 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -192,7 +192,7 @@ Changes (`#198 `_) - ``attr.Factory`` is hashable again. (`#204 `_) -- Subclasses now can overwrite attribute definitions of their superclass. +- Subclasses now can overwrite attribute definitions of their base classes. That means that you can -- for example -- change the default value for an attribute by redefining it. (`#221 `_, `#229 `_) diff --git a/changelog.d/431.change.rst b/changelog.d/431.change.rst index c96bb7b4..0b1f7020 100644 --- a/changelog.d/431.change.rst +++ b/changelog.d/431.change.rst @@ -1 +1 @@ -It is now possible to override a superclass' class variable using only class annotations. +It is now possible to override a base class' class variable using only class annotations. diff --git a/docs/how-does-it-work.rst b/docs/how-does-it-work.rst index 4937e24e..f8dc04de 100644 --- a/docs/how-does-it-work.rst +++ b/docs/how-does-it-work.rst @@ -13,7 +13,7 @@ But its **declarative** approach combined with **no runtime overhead** lets it s Once you apply the ``@attr.s`` decorator to a class, ``attrs`` searches the class object for instances of ``attr.ib``\ s. Internally they're a representation of the data passed into ``attr.ib`` along with a counter to preserve the order of the attributes. -In order to ensure that sub-classing works as you'd expect it to work, ``attrs`` also walks the class hierarchy and collects the attributes of all super-classes. +In order to ensure that subclassing works as you'd expect it to work, ``attrs`` also walks the class hierarchy and collects the attributes of all base classes. Please note that ``attrs`` does *not* call ``super()`` *ever*. It will write dunder methods to work on *all* of those attributes which also has performance benefits due to fewer function calls. diff --git a/src/attr/_make.py b/src/attr/_make.py index 5a4222f8..efc0c200 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -244,16 +244,16 @@ def _make_attr_tuple_class(cls_name, attr_names): # Tuple class for extracted attributes from a class definition. -# `super_attrs` is a subset of `attrs`. +# `base_attrs` is a subset of `attrs`. _Attributes = _make_attr_tuple_class( "_Attributes", [ # all attributes to build dunder methods for "attrs", # attributes that have been inherited - "super_attrs", + "base_attrs", # map inherited attributes to their originating classes - "super_attrs_map", + "base_attrs_map", ], ) @@ -278,8 +278,8 @@ def _get_annotations(cls): return {} # Verify that the annotations aren't merely inherited. - for super_cls in cls.__mro__[1:]: - if anns is getattr(super_cls, "__annotations__", None): + for base_cls in cls.__mro__[1:]: + if anns is getattr(base_cls, "__annotations__", None): return {} return anns @@ -354,32 +354,32 @@ def _transform_attrs(cls, these, auto_attribs, kw_only): for attr_name, ca in ca_list ] - super_attrs = [] - super_attr_map = {} # A dictionary of superattrs to their classes. + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. taken_attr_names = {a.name: a for a in own_attrs} # Traverse the MRO and collect attributes. - for super_cls in cls.__mro__[1:-1]: - sub_attrs = getattr(super_cls, "__attrs_attrs__", None) + for base_cls in cls.__mro__[1:-1]: + sub_attrs = getattr(base_cls, "__attrs_attrs__", None) if sub_attrs is not None: for a in sub_attrs: prev_a = taken_attr_names.get(a.name) # Only add an attribute if it hasn't been defined before. This # allows for overwriting attribute definitions by subclassing. if prev_a is None: - super_attrs.append(a) + base_attrs.append(a) taken_attr_names[a.name] = a - super_attr_map[a.name] = super_cls + base_attr_map[a.name] = base_cls - attr_names = [a.name for a in super_attrs + own_attrs] + attr_names = [a.name for a in base_attrs + own_attrs] AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) if kw_only: own_attrs = [a._assoc(kw_only=True) for a in own_attrs] - super_attrs = [a._assoc(kw_only=True) for a in super_attrs] + base_attrs = [a._assoc(kw_only=True) for a in base_attrs] - attrs = AttrsClass(super_attrs + own_attrs) + attrs = AttrsClass(base_attrs + own_attrs) had_default = False was_kw_only = False @@ -415,7 +415,7 @@ def _transform_attrs(cls, these, auto_attribs, kw_only): if was_kw_only is False and a.init is True and a.kw_only is True: was_kw_only = True - return _Attributes((attrs, super_attrs, super_attr_map)) + return _Attributes((attrs, base_attrs, base_attr_map)) def _frozen_setattrs(self, name, value): @@ -441,7 +441,7 @@ class _ClassBuilder(object): "_cls", "_cls_dict", "_attrs", - "_super_names", + "_base_names", "_attr_names", "_slots", "_frozen", @@ -449,7 +449,7 @@ class _ClassBuilder(object): "_cache_hash", "_has_post_init", "_delete_attribs", - "_super_attr_map", + "_base_attr_map", ) def __init__( @@ -463,18 +463,18 @@ class _ClassBuilder(object): kw_only, cache_hash, ): - attrs, super_attrs, super_map = _transform_attrs( + attrs, base_attrs, base_map = _transform_attrs( cls, these, auto_attribs, kw_only ) self._cls = cls self._cls_dict = dict(cls.__dict__) if slots else {} self._attrs = attrs - self._super_names = set(a.name for a in super_attrs) - self._super_attr_map = super_map + self._base_names = set(a.name for a in base_attrs) + self._base_attr_map = base_map self._attr_names = tuple(a.name for a in attrs) self._slots = slots - self._frozen = frozen or _has_frozen_superclass(cls) + self._frozen = frozen or _has_frozen_base_class(cls) self._weakref_slot = weakref_slot self._cache_hash = cache_hash self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) @@ -505,19 +505,19 @@ class _ClassBuilder(object): Apply accumulated methods and return the class. """ cls = self._cls - super_names = self._super_names + base_names = self._base_names # Clean class of attribute definitions (`attr.ib()`s). if self._delete_attribs: for name in self._attr_names: if ( - name not in super_names + name not in base_names and getattr(cls, name, None) is not None ): try: delattr(cls, name) except AttributeError: - # This can happen if a superclass defines a class + # This can happen if a base class defines a class # variable and we want to set an attribute with the # same name by using only a type annotation. pass @@ -532,7 +532,7 @@ class _ClassBuilder(object): """ Build and return a new class with a `__slots__` attribute. """ - super_names = self._super_names + base_names = self._base_names cd = { k: v for k, v in iteritems(self._cls_dict) @@ -542,8 +542,8 @@ class _ClassBuilder(object): weakref_inherited = False # Traverse the MRO to check for an existing __weakref__. - for super_cls in self._cls.__mro__[1:-1]: - if "__weakref__" in getattr(super_cls, "__dict__", ()): + for base_cls in self._cls.__mro__[1:-1]: + if "__weakref__" in getattr(base_cls, "__dict__", ()): weakref_inherited = True break @@ -558,7 +558,7 @@ class _ClassBuilder(object): # We only add the names of attributes that aren't inherited. # Settings __slots__ to inherited attributes wastes memory. - slot_names = [name for name in names if name not in super_names] + slot_names = [name for name in names if name not in base_names] if self._cache_hash: slot_names.append(_hash_cache_field) cd["__slots__"] = tuple(slot_names) @@ -655,7 +655,7 @@ class _ClassBuilder(object): self._frozen, self._slots, self._cache_hash, - self._super_attr_map, + self._base_attr_map, ) ) @@ -746,7 +746,7 @@ def attrs( 2. If *cmp* is True and *frozen* is False, ``__hash__`` will be set to None, marking it unhashable (which it is). 3. If *cmp* is False, ``__hash__`` will be left untouched meaning the - ``__hash__`` method of the superclass will be used (if superclass is + ``__hash__`` method of the base class will be used (if base class is ``object``, this means it will fall back to id-based hashing.). Although not recommended, you can decide for yourself and force @@ -909,7 +909,7 @@ Internal alias so we can use it in functions that take an argument called if PY2: - def _has_frozen_superclass(cls): + def _has_frozen_base_class(cls): """ Check whether *cls* has a frozen ancestor by looking at its __setattr__. @@ -923,7 +923,7 @@ if PY2: else: - def _has_frozen_superclass(cls): + def _has_frozen_base_class(cls): """ Check whether *cls* has a frozen ancestor by looking at its __setattr__. @@ -1209,7 +1209,7 @@ def _add_repr(cls, ns=None, attrs=None): return cls -def _make_init(attrs, post_init, frozen, slots, cache_hash, super_attr_map): +def _make_init(attrs, post_init, frozen, slots, cache_hash, base_attr_map): attrs = [a for a in attrs if a.init or a.default is not NOTHING] # We cache the generated init methods for the same kinds of attributes. @@ -1218,7 +1218,7 @@ def _make_init(attrs, post_init, frozen, slots, cache_hash, super_attr_map): unique_filename = "".format(sha1.hexdigest()) script, globs, annotations = _attrs_to_init_script( - attrs, frozen, slots, post_init, cache_hash, super_attr_map + attrs, frozen, slots, post_init, cache_hash, base_attr_map ) locs = {} bytecode = compile(script, unique_filename, "exec") @@ -1254,7 +1254,7 @@ def _add_init(cls, frozen): frozen, _is_slot_cls(cls), cache_hash=False, - super_attr_map={}, + base_attr_map={}, ) return cls @@ -1336,15 +1336,15 @@ def _is_slot_cls(cls): return "__slots__" in cls.__dict__ -def _is_slot_attr(a_name, super_attr_map): +def _is_slot_attr(a_name, base_attr_map): """ Check if the attribute name comes from a slot class. """ - return a_name in super_attr_map and _is_slot_cls(super_attr_map[a_name]) + return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name]) def _attrs_to_init_script( - attrs, frozen, slots, post_init, cache_hash, super_attr_map + attrs, frozen, slots, post_init, cache_hash, base_attr_map ): """ Return a script of an initializer for *attrs* and a dict of globals. @@ -1356,7 +1356,7 @@ def _attrs_to_init_script( """ lines = [] any_slot_ancestors = any( - _is_slot_attr(a.name, super_attr_map) for a in attrs + _is_slot_attr(a.name, base_attr_map) for a in attrs ) if frozen is True: if slots is True: @@ -1395,7 +1395,7 @@ def _attrs_to_init_script( ) def fmt_setter(attr_name, value_var): - if _is_slot_attr(attr_name, super_attr_map): + if _is_slot_attr(attr_name, base_attr_map): res = "_setattr('%(attr_name)s', %(value_var)s)" % { "attr_name": attr_name, "value_var": value_var, @@ -1409,7 +1409,7 @@ def _attrs_to_init_script( def fmt_setter_with_converter(attr_name, value_var): conv_name = _init_converter_pat.format(attr_name) - if _is_slot_attr(attr_name, super_attr_map): + if _is_slot_attr(attr_name, base_attr_map): tmpl = "_setattr('%(attr_name)s', %(c)s(%(value_var)s))" else: tmpl = "_inst_dict['%(attr_name)s'] = %(c)s(%(value_var)s)" diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 4be1ea6d..cbf5bd02 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -165,7 +165,7 @@ class TestAnnotations: @pytest.mark.parametrize("slots", [True, False]) def test_auto_attribs_subclassing(self, slots): """ - Attributes from superclasses are inherited, it doesn't matter if the + Attributes from base classes are inherited, it doesn't matter if the subclass has annotations or not. Ref #291 @@ -251,9 +251,9 @@ class TestAnnotations: assert c.x == 0 assert c.y == 1 - def test_super_class_variable(self): + def test_base_class_variable(self): """ - Superclass class variables can be overridden with an attribute + Base class' class variables can be overridden with an attribute without resorting to using an explicit `attr.ib()`. """ diff --git a/tests/test_dark_magic.py b/tests/test_dark_magic.py index e0336279..e40fe93d 100644 --- a/tests/test_dark_magic.py +++ b/tests/test_dark_magic.py @@ -47,7 +47,7 @@ class C2Slots(object): @attr.s -class Super(object): +class Base(object): x = attr.ib() def meth(self): @@ -55,7 +55,7 @@ class Super(object): @attr.s(slots=True) -class SuperSlots(object): +class BaseSlots(object): x = attr.ib() def meth(self): @@ -63,12 +63,12 @@ class SuperSlots(object): @attr.s -class Sub(Super): +class Sub(Base): y = attr.ib() @attr.s(slots=True) -class SubSlots(SuperSlots): +class SubSlots(BaseSlots): y = attr.ib() @@ -203,7 +203,7 @@ class TestDarkMagic(object): @pytest.mark.parametrize("cls", [Sub, SubSlots]) def test_subclassing_with_extra_attrs(self, cls): """ - Sub-classing (where the subclass has extra attrs) does what you'd hope + Subclassing (where the subclass has extra attrs) does what you'd hope for. """ obj = object() @@ -215,10 +215,10 @@ class TestDarkMagic(object): else: assert "SubSlots(x={obj}, y=2)".format(obj=obj) == repr(i) - @pytest.mark.parametrize("base", [Super, SuperSlots]) + @pytest.mark.parametrize("base", [Base, BaseSlots]) def test_subclass_without_extra_attrs(self, base): """ - Sub-classing (where the subclass does not have extra attrs) still + Subclassing (where the subclass does not have extra attrs) still behaves the same as a subclass with extra attrs. """ @@ -259,8 +259,8 @@ class TestDarkMagic(object): C1Slots, C2, C2Slots, - Super, - SuperSlots, + Base, + BaseSlots, Sub, SubSlots, Frozen, @@ -283,8 +283,8 @@ class TestDarkMagic(object): C1Slots, C2, C2Slots, - Super, - SuperSlots, + Base, + BaseSlots, Sub, SubSlots, Frozen, @@ -342,11 +342,11 @@ class TestDarkMagic(object): @pytest.mark.parametrize("weakref_slot", [True, False]) def test_attrib_overwrite(self, slots, frozen, weakref_slot): """ - Subclasses can overwrite attributes of their superclass. + Subclasses can overwrite attributes of their base class. """ @attr.s(slots=slots, frozen=frozen, weakref_slot=weakref_slot) - class SubOverwrite(Super): + class SubOverwrite(Base): x = attr.ib(default=attr.Factory(list)) assert SubOverwrite([]) == SubOverwrite() @@ -419,9 +419,9 @@ class TestDarkMagic(object): assert hash(C()) != hash(C()) - def test_overwrite_super(self): + def test_overwrite_base(self): """ - Superclasses can overwrite each other and the attributes are added + Base classes can overwrite each other and the attributes are added in the order they are defined. """ diff --git a/tests/test_make.py b/tests/test_make.py index 884aee08..c967268b 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -264,9 +264,9 @@ class TestTransformAttrs(object): All `_CountingAttr`s are transformed into `Attribute`s. """ C = make_tc() - attrs, super_attrs, _ = _transform_attrs(C, None, False, False) + attrs, base_attrs, _ = _transform_attrs(C, None, False, False) - assert [] == super_attrs + assert [] == base_attrs assert 3 == len(attrs) assert all(isinstance(a, Attribute) for a in attrs) @@ -292,11 +292,11 @@ class TestTransformAttrs(object): def test_kw_only(self): """ - Converts all attributes, including superclass attributes, if `kw_only` + Converts all attributes, including base class' attributes, if `kw_only` is provided. Therefore, `kw_only` allows attributes with defaults to preceed mandatory attributes. - Updates in the subclass *don't* affect the superclass attributes. + Updates in the subclass *don't* affect the base class attributes. """ @attr.s @@ -310,10 +310,10 @@ class TestTransformAttrs(object): x = attr.ib(default=None) y = attr.ib() - attrs, super_attrs, _ = _transform_attrs(C, None, False, True) + attrs, base_attrs, _ = _transform_attrs(C, None, False, True) assert len(attrs) == 3 - assert len(super_attrs) == 1 + assert len(base_attrs) == 1 for a in attrs: assert a.kw_only is True @@ -323,7 +323,7 @@ class TestTransformAttrs(object): def test_these(self): """ - If these is passed, use it and ignore body and superclasses. + If these is passed, use it and ignore body and base classes. """ class Base(object): @@ -332,11 +332,11 @@ class TestTransformAttrs(object): class C(Base): y = attr.ib() - attrs, super_attrs, _ = _transform_attrs( + attrs, base_attrs, _ = _transform_attrs( C, {"x": attr.ib()}, False, False ) - assert [] == super_attrs + assert [] == base_attrs assert (simple_attr("x"),) == attrs def test_these_leave_body(self):