Repository: python-attrs/attrs

Problem statement:
Inherited attributes of subclasses no longer compare equal to the equivalent attributes of base class
A change between 19.3.0 → 20.1.0 which doesn't seem to be deliberate, but may be related to #635 is that inherited attributes no longer compare equal to the attributes of the base class. Here is a MWE:

```python
import attr

@attr.s()
class BaseClass(object):
    x = attr.ib()

@attr.s()
class SubClass(BaseClass):
    pass

assert attr.fields(BaseClass)[0].name == attr.fields(SubClass)[0].name   # Succeeds in all versions
assert attr.fields(BaseClass)[0] == attr.fields(SubClass)[0]  # Fails in 20.1.0
```

Interestingly, if you shadow the attribute, it works as expected:

```python
import attr

@attr.s()
class BaseClass(object):
    x = attr.ib()

@attr.s()
class SubClass(BaseClass):
    x = attr.ib()

# Works in all versions
assert attr.fields(BaseClass)[0].name == attr.fields(SubClass)[0].name
assert attr.fields(BaseClass)[0] == attr.fields(SubClass)[0]
```

I think the issue is that `inherited` is included in the calculation of whether two `Attribute`s are equal or not.

I discovered this when upgrading some code that does something like this:

```python
import attr

@attr.s()
class BaseClass:
    x = attr.ib()

@attr.s()
class SubClass(BaseClass):
    y = attr.ib()

def f(x) -> None:
    print(x)

def my_function(obj : BaseClass) -> None:
    kwargs = attr.asdict(obj, filter=attr.filters.include(*attr.fields(BaseClass)))
    f(**kwargs)

if __name__ == "__main__":
    my_function(SubClass(1, 2))
```

The idea there is that they want to pull out the subset of the structure that corresponds to the base class.

In this case, I can imagine working around it by pulling out the `name` attribute for each of the `Attribute`s (since in this case I _think_ they would want the subclass to pass along *any* attribute with the name of an attribute on the base class, not just ones that are configured the same way). I can imagine there might be other problems, though, if `attr.filters.include(*attr.fields(BaseClass))` is a common pattern.


Hints:
Yeah for all practical purposes, I think we've got it wrong ATM.  And I think hashing should also not take it into account. Technically it's wrong but in practice I think it makes it useless.

Patch:
diff --git a/src/attr/_make.py b/src/attr/_make.py
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -2185,6 +2185,8 @@ class Attribute(object):
 
     .. versionadded:: 20.1.0 *inherited*
     .. versionadded:: 20.1.0 *on_setattr*
+    .. versionchanged:: 20.2.0 *inherited* is not taken into account for
+        equality checks and hashing anymore.
 
     For the full version history of the fields, see `attr.ib`.
     """
@@ -2354,8 +2356,11 @@ def _setattrs(self, name_values_pairs):
 ]
 
 Attribute = _add_hash(
-    _add_eq(_add_repr(Attribute, attrs=_a), attrs=_a),
-    attrs=[a for a in _a if a.hash],
+    _add_eq(
+        _add_repr(Attribute, attrs=_a),
+        attrs=[a for a in _a if a.name != "inherited"],
+    ),
+    attrs=[a for a in _a if a.hash and a.name != "inherited"],
 )
 
 


Test patch:
diff --git a/tests/test_make.py b/tests/test_make.py
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -691,6 +691,26 @@ def test_sugar_callable(self):
             class C(object):
                 x = attr.ib(factory=Factory(list))
 
+    def test_inherited_does_not_affect_hashing_and_equality(self):
+        """
+        Whether or not an Attribute has been inherited doesn't affect how it's
+        hashed and compared.
+        """
+
+        @attr.s
+        class BaseClass(object):
+            x = attr.ib()
+
+        @attr.s
+        class SubClass(BaseClass):
+            pass
+
+        ba = attr.fields(BaseClass)[0]
+        sa = attr.fields(SubClass)[0]
+
+        assert ba == sa
+        assert hash(ba) == hash(sa)
+
 
 @pytest.mark.skipif(PY2, reason="keyword-only arguments are PY3-only.")
 class TestKeywordOnlyAttributes(object):