Skip to content

Commit

Permalink
Optimize the extremely common case of a __bases__ of length one.
Browse files Browse the repository at this point in the history
In the benchmark, 4/5 of the interfaces and related objects have a base of length one.
  • Loading branch information
jamadden committed Mar 10, 2020
1 parent ec136d8 commit f05c811
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 21 deletions.
14 changes: 14 additions & 0 deletions src/zope/interface/declarations.py
Expand Up @@ -663,6 +663,13 @@ def __init__(self, cls, *interfaces):
self._cls = cls
Declaration.__init__(self, *(interfaces + (implementedBy(cls), )))

def __repr__(self):
return "<%s.%s for %s>" % (
self.__class__.__module__,
self.__class__.__name__,
self._cls,
)

def __reduce__(self):
return Provides, self.__args

Expand Down Expand Up @@ -793,6 +800,13 @@ def __init__(self, cls, metacls, *interfaces):
self.__args = (cls, metacls, ) + interfaces
Declaration.__init__(self, *(interfaces + (implementedBy(metacls), )))

def __repr__(self):
return "<%s.%s for %s>" % (
self.__class__.__module__,
self.__class__.__name__,
self._cls,
)

def __reduce__(self):
return self.__class__, self.__args

Expand Down
9 changes: 8 additions & 1 deletion src/zope/interface/interface.py
Expand Up @@ -283,7 +283,14 @@ def changed(self, originally_changed):
implied = self._implied
implied.clear()

ancestors = ro(self)
if len(self.__bases__) == 1:
# Fast path: One base makes it trivial to calculate
# the MRO.
sro = self.__bases__[0].__sro__
ancestors = [self]
ancestors.extend(sro)
else:
ancestors = ro(self)

try:
if Interface not in ancestors:
Expand Down
2 changes: 1 addition & 1 deletion src/zope/interface/registry.py
Expand Up @@ -550,7 +550,7 @@ def _getAdapterRequired(factory, required):
r = implementedBy(r)
else:
raise TypeError("Required specification must be a "
"specification or class."
"specification or class, not %r" % type(r)
)
result.append(r)
return tuple(result)
Expand Down
64 changes: 45 additions & 19 deletions src/zope/interface/ro.py
Expand Up @@ -125,17 +125,29 @@ def _find_next_base(C, base_tree_remaining, ignore_first):
# ``[IIOError, IOSError, IEnvironmentError, IStandardError, IException, Interface]``
# but the standard Python algorithm would forbid creating that order entirely.
else:
# We truly have a non-resolvable IRO.
# We truly have a non-resolvable, inconsistent IRO.
base = None
# Unlike Python's MRO, we resolve the issue. If possible, we look ahead by
# one to see if we can resolve it at that point; that handles
# a large majority of cases of simple divergence (the OSError example above).
# If that doesn't work, we arbitrarily pick the next possible
# base to be the next actual base, thus putting the user in more direct control.
# When we do this, we also need to remove it from any other place that it
# may still appear both transiently and then permanently (that part is handled by ``_merge``).
# TODO: Track these, allow ways for people to find what objects they create
# have inconsistent IRO.
# Unlike Python's MRO, we resolve the issue. If possible, we
# look ahead by one to see if we can resolve it at that point;
# that handles a large majority of cases of simple divergence
# (the OSError example above). If that doesn't work, we
# arbitrarily pick the next possible base to be the next
# actual base, thus putting the user in more direct control.
# When we do this, we also need to remove it from any other
# place that it may still appear both transiently and then
# permanently (that part is handled by ``_merge``).

# TODO: Have a way (environment variable) for a user to opt-in
# to having ``zope.interface`` track and/or warn about
# locations of inconsistent IROs. A user would know to turn
# this on because we would issue a simple warning just for the
# first inconsistent case we saw (so no worrying about
# stacklevels or extra messaging). The problem there,
# naturally, is that some of the common interfaces shipped in
# ``zope.interface`` itself exhibit inconsistent IROs right
# now, so until that's changed, everyone would always get a
# warning. (The problem classes are list, dict, tuple, str,
# bytes, and OSError).
if ignore_first and len(base_tree_remaining[0]) > 1:
first_base = base_tree_remaining[0][0]
ignoring_first = _nonempty_bases_ignoring(base_tree_remaining, first_base)
Expand All @@ -145,10 +157,8 @@ def _find_next_base(C, base_tree_remaining, ignore_first):
base = base_tree_remaining[0][0]
return base

def _merge(C, base_tree, memo):
if C in memo:
return memo[C]

def _merge(C, base_tree):
# Returns a merged *list*.
result = []
base_tree_remaining = base_tree
base = None
Expand All @@ -160,20 +170,36 @@ def _merge(C, base_tree, memo):
base_tree_remaining = _nonempty_bases_ignoring(base_tree_remaining, base)

if not base_tree_remaining:
result = tuple(result)
memo[C] = result
return result

base = _find_next_base(C, base_tree_remaining, True)

result.append(base)

def ro(C, _memo=None):
"Compute the precedence list (mro) according to C3"
"""
Compute the precedence list (mro) according to C3.
As an implementation note, this always calculates the full MRO by
examining all the bases recursively. If there are special cases
that can reuse pre-calculated partial MROs, such as a
``__bases__`` of length one, the caller is responsible for
optimizing that. (This is because this function doesn't know how
to get the complete MRO of a base; it only knows how to get their
``__bases__``.)
:return: A fresh `list` object.
"""
memo = _memo if _memo is not None else {}
try:
return list(memo[C])
except KeyError:
pass

result = _merge(
C,
[[C]] + [ro(base, memo) for base in C.__bases__] + [list(C.__bases__)],
memo,
)
return list(result)

memo[C] = tuple(result)
return result
13 changes: 13 additions & 0 deletions src/zope/interface/tests/test_declarations.py
Expand Up @@ -1126,6 +1126,13 @@ def _test():
return foo.__provides__
self.assertRaises(AttributeError, _test)

def test__repr__(self):
inst = self._makeOne(type(self))
self.assertEqual(
repr(inst),
"<zope.interface.Provides for %r>" % type(self)
)


class Test_Provides(unittest.TestCase):

Expand Down Expand Up @@ -1395,6 +1402,12 @@ class Foo(object):
self.assertEqual(cp.__reduce__(),
(self._getTargetClass(), (Foo, type(Foo), IBar)))

def test__repr__(self):
inst = self._makeOne(type(self), type)
self.assertEqual(
repr(inst),
"<zope.interface.declarations.ClassProvides for %r>" % type(self)
)

class Test_directlyProvidedBy(unittest.TestCase):

Expand Down

0 comments on commit f05c811

Please sign in to comment.