Skip to content

Commit

Permalink
Consistently use "base class" and "subclass" (python-attrs#436)
Browse files Browse the repository at this point in the history
Thanks to ABCs, "base class" is more Python than "superclass" and the latter is
also slightly confusing by alluding to "super" and/or being judgy.
  • Loading branch information
hynek authored Aug 29, 2018
1 parent 1268219 commit 73ae718
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 72 deletions.
2 changes: 1 addition & 1 deletion AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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>`_)
Expand Down
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
Expand Up @@ -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.

Expand Down
82 changes: 41 additions & 41 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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__(
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -655,7 +655,7 @@ def add_init(self):
self._frozen,
self._slots,
self._cache_hash,
self._super_attr_map,
self._base_attr_map,
)
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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__.
Expand All @@ -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__.
Expand Down Expand Up @@ -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.
Expand All @@ -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")
Expand Down Expand Up @@ -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

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

Expand Down
Loading

0 comments on commit 73ae718

Please sign in to comment.