From 1025519cb68ad8fdae379daa6de8d9ec427456cd Mon Sep 17 00:00:00 2001 From: Jan-Jaap Driessen Date: Fri, 25 Sep 2020 11:29:29 +0200 Subject: [PATCH 1/5] When an invariant is defined in an interface, it's found by `validateInvariants` in all interfaces inheriting from that interface. Make sure to call each invariant only once when validating invariants. --- .gitignore | 1 + CHANGES.rst | 3 +++ src/zope/interface/interface.py | 9 +++++++-- src/zope/interface/tests/test_interface.py | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) 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 a22876df..dbcaf412 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,9 @@ that argument. See `issue 208 `_. +- When an invariant is defined in an interface, it's found by + `validateInvariants` in all interfaces inheriting from that interface. + Make sure to call each invariant only once when validating invariants. 5.1.0 (2020-04-08) ================== diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py index f819441d..9b6b752b 100644 --- a/src/zope/interface/interface.py +++ b/src/zope/interface/interface.py @@ -875,9 +875,14 @@ def direct(self, name): def queryDescriptionFor(self, name, default=None): return self.get(name, default) - def validateInvariants(self, obj, errors=None): + def validateInvariants(self, obj, errors=None, seen=None): """validate object to defined invariants.""" + if seen is None: + seen = set() for call in self.queryTaggedValue('invariants', []): + if call in seen: + continue + seen.add(call) try: call(obj) except Invalid as e: @@ -886,7 +891,7 @@ def validateInvariants(self, obj, errors=None): errors.append(e) for base in self.__bases__: try: - base.validateInvariants(obj, errors) + base.validateInvariants(obj, errors, seen=seen) except Invalid: if errors is None: raise 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') From 27e832a04c8b3b092ccb2568634e46f8dd8629e1 Mon Sep 17 00:00:00 2001 From: Jan-Jaap Driessen Date: Mon, 28 Sep 2020 14:34:38 +0200 Subject: [PATCH 2/5] Update CHANGES.rst Co-authored-by: Jason Madden --- CHANGES.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index dbcaf412..e2d16f95 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,9 +12,9 @@ that argument. See `issue 208 `_. -- When an invariant is defined in an interface, it's found by - `validateInvariants` in all interfaces inheriting from that interface. - Make sure to call each invariant only once when validating invariants. +- 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.0 (2020-04-08) ================== From 64fb805b3b2a6536c4874b4f0c3fbbbf4bea9e23 Mon Sep 17 00:00:00 2001 From: Jan-Jaap Driessen Date: Mon, 28 Sep 2020 14:41:03 +0200 Subject: [PATCH 3/5] Use an internal method for validating invariants, so we don't taint the public interface/signature. --- src/zope/interface/interface.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py index 9b6b752b..918383b0 100644 --- a/src/zope/interface/interface.py +++ b/src/zope/interface/interface.py @@ -875,10 +875,7 @@ def direct(self, name): def queryDescriptionFor(self, name, default=None): return self.get(name, default) - def validateInvariants(self, obj, errors=None, seen=None): - """validate object to defined invariants.""" - if seen is None: - seen = set() + def __validateInvariants(self, obj, errors, seen): for call in self.queryTaggedValue('invariants', []): if call in seen: continue @@ -891,13 +888,17 @@ def validateInvariants(self, obj, errors=None, seen=None): errors.append(e) for base in self.__bases__: try: - base.validateInvariants(obj, errors, seen=seen) + base.__validateInvariants(obj, errors, seen) except Invalid: if errors is None: raise if errors: raise Invalid(errors) + def validateInvariants(self, obj, errors=None): + """validate object to defined invariants.""" + self.__validateInvariants(obj, errors, set()) + def queryTaggedValue(self, tag, default=None): """ Queries for the value associated with *tag*, returning it from the nearest From 856ee6086af24792a0ff1f63bc6eb750de0892a3 Mon Sep 17 00:00:00 2001 From: Jan-Jaap Driessen Date: Thu, 1 Oct 2020 12:59:18 +0200 Subject: [PATCH 4/5] Fix rst syntax/line folding. --- CHANGES.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e2d16f95..84c83898 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,9 +12,11 @@ that argument. See `issue 208 `_. -- 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 `_. +- 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.0 (2020-04-08) ================== From 3fc878eb00deb9e2988ef651c74e09e39dd9d49a Mon Sep 17 00:00:00 2001 From: Jan-Jaap Driessen Date: Thu, 1 Oct 2020 13:02:38 +0200 Subject: [PATCH 5/5] Use queryDirectTaggedValue to find invariants removes the need to keep track of invariants we have already seen. --- src/zope/interface/interface.py | 34 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py index 918383b0..d100aae2 100644 --- a/src/zope/interface/interface.py +++ b/src/zope/interface/interface.py @@ -875,29 +875,21 @@ def direct(self, name): def queryDescriptionFor(self, name, default=None): return self.get(name, default) - def __validateInvariants(self, obj, errors, seen): - for call in self.queryTaggedValue('invariants', []): - if call in seen: - continue - seen.add(call) - 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, seen) - except Invalid: - if errors is None: - raise - if errors: - raise Invalid(errors) - def validateInvariants(self, obj, errors=None): """validate object to defined invariants.""" - self.__validateInvariants(obj, errors, set()) + + 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) def queryTaggedValue(self, tag, default=None): """