Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sure to call each invariant only once when validating invariants. #215

Merged
merged 6 commits into from Oct 1, 2020
Merged

Conversation

janjaapdriessen
Copy link
Member

When an invariant is defined in an interface, it's found by validateInvariants in all interfaces inheriting from that interface.

`validateInvariants` in all interfaces inheriting from that interface.
Make sure to call each invariant only once when validating invariants.
Copy link
Member

@jamadden jamadden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!

CHANGES.rst Outdated Show resolved Hide resolved
src/zope/interface/interface.py Outdated Show resolved Hide resolved
janjaapdriessen and others added 2 commits September 28, 2020 14:34
Co-authored-by: Jason Madden <jason+github@nextthought.com>
@jamadden
Copy link
Member

Thanks for considering that! I see your new implementation and I agree it looks nice and clean!

While reviewing it, I had another thought though: What if the body was changed from self.queryTaggedValue() to self.queryDirectTaggedValue()? That would be only change to the entirety of the original function.

queryTaggedValue returns the first value seen when walking up the __iro__. That could result in the same value being seen multiple times if the tag is defined somewhere up the __iro__ from the current object: the problem this PR is preventing.

queryDirectTaggedValue, OTOH, looks only and exactly for a value defined in this exact instance. That would eliminate the entire possibility of finding duplicates through the inheritance hierarchy.

As this function proceeds to walk up __bases__ it should ultimately always find everything included in __iro__….

But here's the problem with that: it might actually find them multiple times:

>>> from zope.interface import Interface
>>> class I1(Interface):
...     pass
...
>>> class I2(I1):
...     pass
...
>>> class I11(I1): 
…     pass
…
>>> class I3(I11, I2):
...     pass
...
>>> I3.__iro__
(<InterfaceClass __main__.I3>,
 <InterfaceClass __main__.I11>,
 <InterfaceClass __main__.I2>,
 <InterfaceClass __main__.I1>,
 <InterfaceClass zope.interface.Interface>)
>>> def visit(s, pfx=''):
...     print(pfx, s)
...     for b in s.__bases__:
...         visit(b, pfx + ' ')
...
>>> visit(I3)
 <InterfaceClass __main__.I3>
  <InterfaceClass __main__.I11>
   <InterfaceClass __main__.I1>
    <InterfaceClass zope.interface.Interface>
  <InterfaceClass __main__.I2>
   <InterfaceClass __main__.I1>
    <InterfaceClass zope.interface.Interface>

Actually, thinking more about this, I find this method very confusing as it currently exists. It uses __iro__ to find invariants, but it walks up __bases__. Since this is InterfaceClass, __bases__ should only consist of other interfaces (not specifications or anything like that), like __iro__. One consequence of this oddity is that the method is O(n^2) due to the hidden loop in queryTaggedValue; also, recursion isn't especially fast in Python.

What if the method was made non-recursive using __iro__ and queryDirectTaggedValue directly?

def validateInvariants(self, obj, errors=None):
    # pseudo-code
    for iface in self.__iro__: # Iterate starting with self
        for invariant in iface.queryDirectTaggedValue('invariants', ()):
            try:
                invariant(obj)
            except Invalid:
                 if errors is not None: errors.append(e)
                 else: raise
    if errors: raise Invalid(errors)

That seems a lot simpler and more direct. It eliminates the hashing of the PR. It still has the same issue with people that have tried to override validateInvariants as the PR, but in reality that's probably a non-issue.

queryDirectTaggedValue is relatively new, so that might explain why it wasn't written this way to start with. It was added specifically because using __bases__ can result in inconsistencies.

Thoughts? What have I missed?

CHANGES.rst Outdated Show resolved Hide resolved
@janjaapdriessen
Copy link
Member Author

The implementation using queryDirectTaggedValue looks smooth, all tests pass, and the issue in my codebase is fixed. I am happy with this fix, thanks for you help @jamadden!

Copy link
Member

@jamadden jamadden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for being willing to iterate!

@janjaapdriessen janjaapdriessen merged commit 5cb7ffd into zopefoundation:master Oct 1, 2020
@janjaapdriessen
Copy link
Member Author

janjaapdriessen commented Oct 1, 2020

Would you mind making a pypi release so I can tie up loose ends at my end? Thanks!

@jamadden
Copy link
Member

jamadden commented Oct 1, 2020

Done! And thank you! I haven't benchmarked but for deep interface trees I expect this also results in a small performance boost too.

netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this pull request Oct 18, 2020
5.1.2 (2020-10-01)
==================

- 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
  <https://github.com/zopefoundation/zope.interface/pull/215/>`_.

5.1.1 (2020-09-30)
==================

- Fix the method definitions of ``IAdapterRegistry.subscribe``,
  ``subscriptions`` and ``subscribers``. Previously, they all were
  defined to accept a ``name`` keyword argument, but subscribers have
  no names and the implementation of that interface did not accept
  that argument. See `issue 208
  <https://github.com/zopefoundation/zope.interface/issues/208>`_.

- Fix a potential reference leak in the C optimizations. Previously,
  applications that dynamically created unique ``Specification``
  objects (e.g., used ``@implementer`` on dynamic classes) could
  notice a growth of small objects over time leading to increased
  garbage collection times. See `issue 216
  <https://github.com/zopefoundation/zope.interface/issues/216>`_.

  .. caution::

     This leak could prevent interfaces used as the bases of
     other interfaces from being garbage collected. Those interfaces
     will now be collected.

     One way in which this would manifest was that ``weakref.ref``
     objects (and things built upon them, like
     ``Weak[Key|Value]Dictionary``) would continue to have access to
     the original object even if there were no other visible
     references to Python and the original object *should* have been
     collected. This could be especially problematic for the
     ``WeakKeyDictionary`` when combined with dynamic or local
     (created in the scope of a function) interfaces, since interfaces
     are hashed based just on their name and module name. See the
     linked issue for an example of a resulting ``KeyError``.

     Note that such potential errors are not new, they are just once
     again a possibility.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants