Skip to content

Commit

Permalink
Merge pull request #44 from zopefoundation/comp-implements
Browse files Browse the repository at this point in the history
Make declarations.Implements sortable.
  • Loading branch information
jamadden committed Aug 4, 2016
2 parents e792730 + bbf1a3d commit b516fca
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 14 deletions.
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Changes
4.2.1 (unreleased)
------------------

- TBD
- Add the ability to sort the objects returned by ``implementedBy``.
This is compatible with the way interface classes sort so they can
be used together in ordered containers like BTrees.
(https://github.com/zopefoundation/zope.interface/issues/42)

4.2.0 (2016-06-10)
------------------
Expand Down
88 changes: 80 additions & 8 deletions src/zope/interface/declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ class implements (that instances of the class provides).
from types import FunctionType
from types import MethodType
from types import ModuleType
import warnings
import weakref

from zope.interface.advice import addClassAdvisor
Expand Down Expand Up @@ -131,12 +130,86 @@ class Implements(Declaration):

__name__ = '?'

@classmethod
def named(cls, name, *interfaces):
# Implementation method: Produce an Implements interface with
# a fully fleshed out __name__ before calling the constructor, which
# sets bases to the given interfaces and which may pass this object to
# other objects (e.g., to adjust dependents). If they're sorting or comparing
# by name, this needs to be set.
inst = cls.__new__(cls)
inst.__name__ = name
inst.__init__(*interfaces)
return inst

def __repr__(self):
return '<implementedBy %s>' % (self.__name__)

def __reduce__(self):
return implementedBy, (self.inherit, )

def __cmp(self, other):
# Yes, I did mean to name this __cmp, rather than __cmp__.
# It is a private method used by __lt__ and __gt__.
# This is based on, and compatible with, InterfaceClass.
# (The two must be mutually comparable to be able to work in e.g., BTrees.)
# Instances of this class generally don't have a __module__ other than
# `zope.interface.declarations`, whereas they *do* have a __name__ that is the
# fully qualified name of the object they are representing.

# Note, though, that equality and hashing are still identity based. This
# accounts for things like nested objects that have the same name (typically
# only in tests) and is consistent with pickling. As far as comparisons to InterfaceClass
# goes, we'll never have equal name and module to those, so we're still consistent there.
# Instances of this class are essentially intended to be unique and are
# heavily cached (note how our __reduce__ handles this) so having identity
# based hash and eq should also work.
if other is None:
return -1

n1 = (self.__name__, self.__module__)
n2 = (getattr(other, '__name__', ''), getattr(other, '__module__', ''))

# This spelling works under Python3, which doesn't have cmp().
return (n1 > n2) - (n1 < n2)

def __hash__(self):
return Declaration.__hash__(self)

def __eq__(self, other):
return self is other

def __ne__(self, other):
return self is not other

def __lt__(self, other):
c = self.__cmp(other)
return c < 0

def __le__(self, other):
c = self.__cmp(other)
return c <= 0

def __gt__(self, other):
c = self.__cmp(other)
return c > 0

def __ge__(self, other):
c = self.__cmp(other)
return c >= 0

def _implements_name(ob):
# Return the __name__ attribute to be used by its __implemented__
# property.
# This must be stable for the "same" object across processes
# because it is used for sorting. It needn't be unique, though, in cases
# like nested classes named Foo created by different functions, because
# equality and hashing is still based on identity.
# It might be nice to use __qualname__ on Python 3, but that would produce
# different values between Py2 and Py3.
return (getattr(ob, '__module__', '?') or '?') + \
'.' + (getattr(ob, '__name__', '?') or '?')

def implementedByFallback(cls):
"""Return the interfaces implemented for a class' instances
Expand Down Expand Up @@ -183,10 +256,11 @@ def implementedByFallback(cls):
return spec

# TODO: need old style __implements__ compatibility?
spec_name = _implements_name(cls)
if spec is not None:
# old-style __implemented__ = foo declaration
spec = (spec, ) # tuplefy, as it might be just an int
spec = Implements(*_normalizeargs(spec))
spec = Implements.named(spec_name, *_normalizeargs(spec))
spec.inherit = None # old-style implies no inherit
del cls.__implemented__ # get rid of the old-style declaration
else:
Expand All @@ -197,12 +271,9 @@ def implementedByFallback(cls):
raise TypeError("ImplementedBy called for non-factory", cls)
bases = ()

spec = Implements(*[implementedBy(c) for c in bases])
spec = Implements.named(spec_name, *[implementedBy(c) for c in bases])
spec.inherit = cls

spec.__name__ = (getattr(cls, '__module__', '?') or '?') + \
'.' + (getattr(cls, '__name__', '?') or '?')

try:
cls.__implemented__ = spec
if not hasattr(cls, '__providedBy__'):
Expand Down Expand Up @@ -314,7 +385,8 @@ def __call__(self, ob):
classImplements(ob, *self.interfaces)
return ob

spec = Implements(*self.interfaces)
spec_name = _implements_name(ob)
spec = Implements.named(spec_name, *self.interfaces)
try:
ob.__implemented__ = spec
except AttributeError:
Expand Down Expand Up @@ -641,7 +713,7 @@ def classProvides(*interfaces):
"""
# This entire approach is invalid under Py3K. Don't even try to fix
# the coverage for this block there. :(

if PYTHON3: #pragma NO COVER
raise TypeError(_ADVICE_ERROR % 'provider')

Expand Down
2 changes: 1 addition & 1 deletion src/zope/interface/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def __adapt__(self, obj):
if adapter is not None:
return adapter


InterfaceBase = InterfaceBasePy
try:
from _zope_interface_coptimizations import InterfaceBase
Expand Down
25 changes: 22 additions & 3 deletions src/zope/interface/tests/test_declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ def test___add___related_interface(self):
self.assertEqual(list(after), [IFoo, IBar, IBaz])


class ImplementsTests(unittest.TestCase):
class TestImplements(unittest.TestCase):

def _getTargetClass(self):
from zope.interface.declarations import Implements
Expand All @@ -264,6 +264,25 @@ def test___reduce__(self):
impl = self._makeOne()
self.assertEqual(impl.__reduce__(), (implementedBy, (None,)))

def test_sort(self):
from zope.interface.declarations import implementedBy
class A(object):
pass
class B(object):
pass
from zope.interface.interface import InterfaceClass
IFoo = InterfaceClass('IFoo')

self.assertEqual(implementedBy(A), implementedBy(A))
self.assertEqual(hash(implementedBy(A)), hash(implementedBy(A)))
self.assertTrue(implementedBy(A) < None)
self.assertTrue(None > implementedBy(A))
self.assertTrue(implementedBy(A) < implementedBy(B))
self.assertTrue(implementedBy(A) > IFoo)
self.assertTrue(implementedBy(A) <= implementedBy(B))
self.assertTrue(implementedBy(A) >= IFoo)
self.assertTrue(implementedBy(A) != IFoo)


class Test_implementedByFallback(unittest.TestCase):

Expand Down Expand Up @@ -597,7 +616,7 @@ class Foo(object):
returned = decorator(foo)
self.assertTrue(returned is foo)
spec = foo.__implemented__
self.assertEqual(spec.__name__, '?')
self.assertEqual(spec.__name__, 'zope.interface.tests.test_declarations.?')
self.assertTrue(spec.inherit is None)
self.assertTrue(foo.__implemented__ is spec)

Expand Down Expand Up @@ -1567,7 +1586,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(DeclarationTests),
unittest.makeSuite(ImplementsTests),
unittest.makeSuite(TestImplements),
unittest.makeSuite(Test_implementedByFallback),
unittest.makeSuite(Test_implementedBy),
unittest.makeSuite(Test_classImplementsOnly),
Expand Down
1 change: 0 additions & 1 deletion src/zope/interface/tests/test_odd_declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ class B(Odd): __implemented__ = I2
# a different mechanism.

# from zope.interface import classProvides

class A(Odd):
pass
classImplements(A, I1)
Expand Down

0 comments on commit b516fca

Please sign in to comment.