Skip to content

Consistently use "base class" and "subclass" #436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 29, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AUTHORS.rst
Original file line number Diff line number Diff line change
@@ -8,4 +8,4 @@ The development is kindly supported by `Variomedia AG <https://www.variomedia.de
A full list of contributors can be found in `GitHub's overview <https://github.com/python-attrs/attrs/graphs/contributors>`_.

It’s the spiritual successor of `characteristic <https://characteristic.readthedocs.io/>`_ and aspires to fix some of it clunkiness and unfortunate decisions.
Both were inspired by Twisted’s `FancyEqMixin <https://twistedmatrix.com/documents/current/api/twisted.python.util.FancyEqMixin.html>`_ but both are implemented using class decorators because `sub-classing is bad for you <https://www.youtube.com/watch?v=3MNVP9-hglc>`_, m’kay?
Both were inspired by Twisted’s `FancyEqMixin <https://twistedmatrix.com/documents/current/api/twisted.python.util.FancyEqMixin.html>`_ but both are implemented using class decorators because `subclassing is bad for you <https://www.youtube.com/watch?v=3MNVP9-hglc>`_, m’kay?
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -192,7 +192,7 @@ Changes
(`#198 <https://github.com/python-attrs/attrs/issues/198>`_)
- ``attr.Factory`` is hashable again.
(`#204 <https://github.com/python-attrs/attrs/issues/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 <https://github.com/python-attrs/attrs/issues/221>`_, `#229 <https://github.com/python-attrs/attrs/issues/229>`_)
2 changes: 1 addition & 1 deletion changelog.d/431.change.rst
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion docs/how-does-it-work.rst
Original file line number Diff line number Diff line change
@@ -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.

82 changes: 41 additions & 41 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
@@ -244,16 +244,16 @@ class MyClassAttributes(tuple):


# 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,15 +441,15 @@ class _ClassBuilder(object):
"_cls",
"_cls_dict",
"_attrs",
"_super_names",
"_base_names",
"_attr_names",
"_slots",
"_frozen",
"_weakref_slot",
"_cache_hash",
"_has_post_init",
"_delete_attribs",
"_super_attr_map",
"_base_attr_map",
)

def __init__(
@@ -463,18 +463,18 @@ def __init__(
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 @@ def _patch_original_class(self):
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 @@ def _create_slots_class(self):
"""
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 @@ def _create_slots_class(self):
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 @@ def _create_slots_class(self):

# 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 @@ def add_init(self):
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 @@ def wrap(cls):

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 @@ def _has_frozen_superclass(cls):

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 = "<attrs generated init {0}>".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 fmt_setter_with_converter(attr_name, value_var):
)

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 fmt_setter(attr_name, value_var):

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)"
6 changes: 3 additions & 3 deletions tests/test_annotations.py
Original file line number Diff line number Diff line change
@@ -165,7 +165,7 @@ class C:
@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 C:
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()`.
"""

30 changes: 15 additions & 15 deletions tests/test_dark_magic.py
Original file line number Diff line number Diff line change
@@ -47,28 +47,28 @@ class C2Slots(object):


@attr.s
class Super(object):
class Base(object):
x = attr.ib()

def meth(self):
return self.x


@attr.s(slots=True)
class SuperSlots(object):
class BaseSlots(object):
x = attr.ib()

def meth(self):
return self.x


@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 @@ def test_programmatic(self, slots, frozen):
@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 @@ def test_subclassing_with_extra_attrs(self, cls):
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 @@ def test_frozen_instance(self, frozen_class):
C1Slots,
C2,
C2Slots,
Super,
SuperSlots,
Base,
BaseSlots,
Sub,
SubSlots,
Frozen,
@@ -283,8 +283,8 @@ def test_pickle_attributes(self, cls, protocol):
C1Slots,
C2,
C2Slots,
Super,
SuperSlots,
Base,
BaseSlots,
Sub,
SubSlots,
Frozen,
@@ -342,11 +342,11 @@ def compute(self):
@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 C(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.
"""

18 changes: 9 additions & 9 deletions tests/test_make.py
Original file line number Diff line number Diff line change
@@ -264,9 +264,9 @@ def test_transforms_to_attribute(self):
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 C(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 C(B):
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 C(B):

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 Base(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):