Repository: tobgu/pyrsistent

Issue:
Not all inherited `__invariant__` definitions are discovered by PClassMeta / store_invariants
Consider this class hierarchy:

```
from pyrsistent import PClass

class W(object):
    def __invariant__(self):
        return [(False, "W")]

class Y(W, PClass):
    pass
```

Attempting to instantiate this `Y` results in a failure as expected (at least by me):

```
pyrsistent._checked_types.InvariantException: 
    Global invariant failed, invariant_errors=[('W',)], missing_fields=[]
```

Now compare to this class hierarchy:

```
from pyrsistent import PClass

class W(object):
    def __invariant__(self):
        return [(False, "W")]

class X(W):
    pass

class Y(X, PClass):
    pass
```

Attempting to instantiate *this* `Y` succeeds.

Discussion:

Patch:
diff --git a/pyrsistent/_checked_types.py b/pyrsistent/_checked_types.py
index 492a862..8c768aa 100644
--- a/pyrsistent/_checked_types.py
+++ b/pyrsistent/_checked_types.py
@@ -83,15 +83,36 @@ def wrap_invariant(invariant):
     return f
 
 
+def _all_dicts(bases, seen=None):
+    """
+    Yield each class in ``bases`` and each of their base classes.
+    """
+    if seen is None:
+        seen = set()
+    for cls in bases:
+        if cls in seen:
+            continue
+        seen.add(cls)
+        yield cls.__dict__
+        for b in _all_dicts(cls.__bases__, seen):
+            yield b
+
+
 def store_invariants(dct, bases, destination_name, source_name):
     # Invariants are inherited
-    invariants = [dct[source_name]] if source_name in dct else []
-    invariants += [b.__dict__[source_name] for b in bases if source_name in b.__dict__]
+    invariants = []
+    for ns in [dct] + list(_all_dicts(bases)):
+        try:
+            invariant = ns[source_name]
+        except KeyError:
+            continue
+        invariants.append(invariant)
+
     if not all(callable(invariant) for invariant in invariants):
         raise TypeError('Invariants must be callable')
-
     dct[destination_name] = tuple(wrap_invariant(inv) for inv in invariants)
 
+
 class _CheckedTypeMeta(type):
     def __new__(mcs, name, bases, dct):
         _store_types(dct, bases, '_checked_types', '__type__')


Test patch:
diff --git a/tests/class_test.py b/tests/class_test.py
index d029e38..8320e97 100644
--- a/tests/class_test.py
+++ b/tests/class_test.py
@@ -283,6 +283,49 @@ def test_multiple_global_invariants():
         assert e.invariant_errors == (('x', 'y'),)
 
 
+def test_inherited_global_invariants():
+    class Distant(object):
+        def __invariant__(self):
+            return [(self.distant, "distant")]
+
+    class Nearby(Distant):
+        def __invariant__(self):
+            return [(self.nearby, "nearby")]
+
+    class MultipleInvariantGlobal(Nearby, PClass):
+        distant = field()
+        nearby = field()
+
+    try:
+        MultipleInvariantGlobal(distant=False, nearby=False)
+        assert False
+    except InvariantException as e:
+        assert e.invariant_errors == (("nearby",), ("distant",),)
+
+
+def test_diamond_inherited_global_invariants():
+    counter = []
+    class Base(object):
+        def __invariant__(self):
+            counter.append(None)
+            return [(False, "base")]
+
+    class Left(Base):
+        pass
+
+    class Right(Base):
+        pass
+
+    class SingleInvariantGlobal(Left, Right, PClass):
+        pass
+
+    try:
+        SingleInvariantGlobal()
+        assert False
+    except InvariantException as e:
+        assert e.invariant_errors == (("base",),)
+        assert counter == [None]
+
 def test_supports_weakref():
     import weakref
     weakref.ref(Point(x=1, y=2))
@@ -357,4 +400,4 @@ def test_enum_key_type():
     class MyClass2(PClass):
         f = pmap_field(key_type=(Foo,), value_type=int)
 
-    MyClass2()
\ No newline at end of file
+    MyClass2()