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

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.
This commit is contained in:
Hynek Schlawack 2018-08-29 18:52:47 +02:00 committed by GitHub
parent 12682192ba
commit 73ae718ec5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 72 additions and 72 deletions

View File

@ -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>`_.
Its the spiritual successor of `characteristic <https://characteristic.readthedocs.io/>`_ and aspires to fix some of it clunkiness and unfortunate decisions.
Both were inspired by Twisteds `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>`_, mkay?
Both were inspired by Twisteds `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>`_, mkay?

View File

@ -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>`_)

View File

@ -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.

View File

@ -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.

View File

@ -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 = "<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 _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)"

View File

@ -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()`.
"""

View File

@ -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.
"""

View File

@ -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):