diff --git a/.gitignore b/.gitignore index cbf117ff..9aae7811 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.pyc *.pyo *.so +*.swp __pycache__ .coverage .coverage.* diff --git a/CHANGES.rst b/CHANGES.rst index 400dd846..a988b410 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,11 @@ 5.1.2 (unreleased) ================== -- Nothing changed yet. - +- Make sure to call each invariant only once when validating invariants. + Previously, invariants could be called multiple times because when an + invariant is defined in an interface, it's found by in all interfaces + inheriting from that interface. See `pull request 215 + `_. 5.1.1 (2020-09-30) ================== @@ -45,7 +48,6 @@ Note that such potential errors are not new, they are just once again a possibility. - 5.1.0 (2020-04-08) ================== diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py index f819441d..d100aae2 100644 --- a/src/zope/interface/interface.py +++ b/src/zope/interface/interface.py @@ -877,19 +877,17 @@ def queryDescriptionFor(self, name, default=None): def validateInvariants(self, obj, errors=None): """validate object to defined invariants.""" - for call in self.queryTaggedValue('invariants', []): - try: - call(obj) - except Invalid as e: - if errors is None: - raise - errors.append(e) - for base in self.__bases__: - try: - base.validateInvariants(obj, errors) - except Invalid: - if errors is None: - raise + + for iface in self.__iro__: + for invariant in iface.queryDirectTaggedValue('invariants', ()): + try: + invariant(obj) + except Invalid as error: + if errors is not None: + errors.append(error) + else: + raise + if errors: raise Invalid(errors) diff --git a/src/zope/interface/tests/test_interface.py b/src/zope/interface/tests/test_interface.py index 036e8582..9dc2aff6 100644 --- a/src/zope/interface/tests/test_interface.py +++ b/src/zope/interface/tests/test_interface.py @@ -1014,6 +1014,20 @@ def _fail(*args, **kw): self.assertEqual(len(_errors), 1) self.assertTrue(isinstance(_errors[0], Invalid)) + def test_validateInvariants_inherited_not_called_multiple_times(self): + _passable_called_with = [] + + def _passable(*args, **kw): + _passable_called_with.append((args, kw)) + return True + + obj = object() + base = self._makeOne('IBase') + base.setTaggedValue('invariants', [_passable]) + derived = self._makeOne('IDerived', (base,)) + derived.validateInvariants(obj) + self.assertEqual(1, len(_passable_called_with)) + def test___reduce__(self): iface = self._makeOne('PickleMe') self.assertEqual(iface.__reduce__(), 'PickleMe')