From 58cd4ce2d18f283704ba10fd2f434d788f1ae9b7 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Tue, 11 Feb 2020 12:27:10 -0600 Subject: [PATCH] Add numbers ABC interfaces. --- docs/api/common.rst | 5 ++ src/zope/interface/common/collections.py | 1 - src/zope/interface/common/numbers.py | 83 +++++++++++++++++++ src/zope/interface/common/tests/__init__.py | 58 ++++++++++++- .../common/tests/test_collections.py | 43 ++-------- .../interface/common/tests/test_numbers.py | 53 ++++++++++++ 6 files changed, 204 insertions(+), 39 deletions(-) create mode 100644 src/zope/interface/common/numbers.py create mode 100644 src/zope/interface/common/tests/test_numbers.py diff --git a/docs/api/common.rst b/docs/api/common.rst index a83731b1..2763340e 100644 --- a/docs/api/common.rst +++ b/docs/api/common.rst @@ -31,3 +31,8 @@ zope.interface.common.collections ================================= .. automodule:: zope.interface.common.collections + +zope.interface.common.numbers +============================= + +.. automodule:: zope.interface.common.numbers diff --git a/src/zope/interface/common/collections.py b/src/zope/interface/common/collections.py index 6f5969d5..6e215186 100644 --- a/src/zope/interface/common/collections.py +++ b/src/zope/interface/common/collections.py @@ -139,7 +139,6 @@ class ISized(ABCInterface): # ICallable is not defined because there's no standard signature. - class ICollection(ISized, IIterable, IContainer): diff --git a/src/zope/interface/common/numbers.py b/src/zope/interface/common/numbers.py new file mode 100644 index 00000000..e62ca304 --- /dev/null +++ b/src/zope/interface/common/numbers.py @@ -0,0 +1,83 @@ +############################################################################## +# Copyright (c) 2020 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +############################################################################## +""" +Interface definitions paralleling the abstract base classes defined in +:mod:`numbers`. + +After this module is imported, the standard library types will declare +that they implement the appropriate interface. + +.. versionadded:: 5.0.0 +""" +from __future__ import absolute_import + +import numbers as abc + +from zope.interface.common import ABCInterface +from zope.interface.common import optional + +from zope.interface._compat import PYTHON2 as PY2 + +# pylint:disable=inherit-non-class, +# pylint:disable=no-self-argument,no-method-argument +# pylint:disable=unexpected-special-method-signature +# pylint:disable=no-value-for-parameter + + +class INumber(ABCInterface): + abc = abc.Number + + +class IComplex(INumber): + abc = abc.Complex + + @optional + def __complex__(): + """ + Rarely implemented, even in builtin types. + """ + if PY2: + @optional + def __eq__(other): + """ + The interpreter may supply one through complicated rules. + """ + + __ne__ = __eq__ + +class IReal(IComplex): + abc = abc.Real + + @optional + def __complex__(): + """ + Rarely implemented, even in builtin types. + """ + + __floor__ = __ceil__ = __complex__ + + if PY2: + @optional + def __le__(other): + """ + The interpreter may supply one through complicated rules. + """ + + __lt__ = __le__ + + +class IRational(IReal): + abc = abc.Rational + + +class IIntegral(IRational): + abc = abc.Integral diff --git a/src/zope/interface/common/tests/__init__.py b/src/zope/interface/common/tests/__init__.py index b711d360..3fe38821 100644 --- a/src/zope/interface/common/tests/__init__.py +++ b/src/zope/interface/common/tests/__init__.py @@ -1,2 +1,58 @@ +############################################################################## +# Copyright (c) 2020 Zope Foundation and Contributors. +# All Rights Reserved. # -# This file is necessary to make this directory a package. +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +############################################################################## + + +from zope.interface.common import ABCInterface +from zope.interface.common import ABCInterfaceClass + + +def iter_abc_interfaces(predicate=lambda iface: True): + # Iterate ``(iface, classes)``, where ``iface`` is a descendent of + # the ABCInterfaceClass passing the *predicate* and ``classes`` is + # an iterable of classes registered to conform to that interface. + # + # Note that some builtin classes are registered for two distinct + # parts of the ABC/interface tree. For example, bytearray is both ByteString + # and MutableSequence. + seen = set() + stack = list(ABCInterface.dependents) # subclasses, but also implementedBy objects + while stack: + iface = stack.pop(0) + if iface in seen or not isinstance(iface, ABCInterfaceClass): + continue + seen.add(iface) + stack.extend(list(iface.dependents)) + if not predicate(iface): + continue + + registered = list(iface.getRegisteredConformers()) + if registered: + yield iface, registered + + +def add_abc_interface_tests(cls, module): + def predicate(iface): + return iface.__module__ == module + + for iface, registered_classes in iter_abc_interfaces(predicate): + for stdlib_class in registered_classes: + + def test(self, stdlib_class=stdlib_class, iface=iface): + if stdlib_class in self.UNVERIFIABLE or stdlib_class.__name__ in self.UNVERIFIABLE: + self.skipTest("Unable to verify %s" % stdlib_class) + + self.assertTrue(self.verify(iface, stdlib_class)) + + name = 'test_auto_' + stdlib_class.__name__ + '_' + iface.__name__ + test.__name__ = name + assert not hasattr(cls, name) + setattr(cls, name, test) diff --git a/src/zope/interface/common/tests/test_collections.py b/src/zope/interface/common/tests/test_collections.py index 8fd1d0ce..6b142ee4 100644 --- a/src/zope/interface/common/tests/test_collections.py +++ b/src/zope/interface/common/tests/test_collections.py @@ -25,8 +25,7 @@ from zope.interface.verify import verifyClass from zope.interface.verify import verifyObject -from zope.interface.common import ABCInterface -from zope.interface.common import ABCInterfaceClass + # Note that importing z.i.c.collections does work on import. from zope.interface.common import collections @@ -34,23 +33,7 @@ from zope.interface._compat import PYPY from zope.interface._compat import PYTHON2 as PY2 -def walk_abc_interfaces(): - # Note that some builtin classes are registered for two distinct - # parts of the ABC/interface tree. For example, bytearray is both ByteString - # and MutableSequence. - seen = set() - stack = list(ABCInterface.dependents) # subclasses, but also implementedBy objects - while stack: - iface = stack.pop(0) - if iface in seen or not isinstance(iface, ABCInterfaceClass): - continue - seen.add(iface) - stack.extend(list(iface.dependents)) - - registered = list(iface.getRegisteredConformers()) - if registered: - yield iface, registered - +from . import add_abc_interface_tests class TestVerifyClass(unittest.TestCase): @@ -81,7 +64,7 @@ def test_list(self): # about third-party code here, just standard library types. We start with a # blacklist of things to exclude, but if that gets out of hand we can figure # out a better whitelisting. - _UNVERIFIABLE = { + UNVERIFIABLE = { # This is declared to be an ISequence, but is missing lots of methods, # including some that aren't part of a language protocol, such as # ``index`` and ``count``. @@ -97,7 +80,7 @@ def test_list(self): } if PYPY: - _UNVERIFIABLE.update({ + UNVERIFIABLE.update({ # collections.deque.pop() doesn't support the index= argument to # MutableSequence.pop(). We can't verify this on CPython because we can't # get the signature, but on PyPy we /can/ get the signature, and of course @@ -109,7 +92,7 @@ def test_list(self): if PY2: # pylint:disable=undefined-variable,no-member # There are a lot more types that are fundamentally unverifiable on Python 2. - _UNVERIFIABLE.update({ + UNVERIFIABLE.update({ # Missing several key methods like __getitem__ basestring, # Missing __iter__ and __contains__, hard to construct. @@ -123,22 +106,8 @@ def test_list(self): str, }) - @classmethod - def gen_tests(cls): - for iface, registered_classes in walk_abc_interfaces(): - for stdlib_class in registered_classes: - if stdlib_class in cls._UNVERIFIABLE or stdlib_class.__name__ in cls._UNVERIFIABLE: - continue - - def test(self, stdlib_class=stdlib_class, iface=iface): - self.assertTrue(self.verify(iface, stdlib_class)) - - name = 'test_auto_' + stdlib_class.__name__ + '_' + iface.__name__ - test.__name__ = name - assert not hasattr(cls, name) - setattr(cls, name, test) +add_abc_interface_tests(TestVerifyClass, collections.ISet.__module__) -TestVerifyClass.gen_tests() class TestVerifyObject(TestVerifyClass): diff --git a/src/zope/interface/common/tests/test_numbers.py b/src/zope/interface/common/tests/test_numbers.py new file mode 100644 index 00000000..7400838c --- /dev/null +++ b/src/zope/interface/common/tests/test_numbers.py @@ -0,0 +1,53 @@ +############################################################################## +# Copyright (c) 2020 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +############################################################################## + + +import unittest +import numbers as abc + +from zope.interface.verify import verifyClass +from zope.interface.verify import verifyObject + +# Note that importing z.i.c.numbers does work on import. +from zope.interface.common import numbers + +from . import add_abc_interface_tests + + +class TestVerifyClass(unittest.TestCase): + verifier = staticmethod(verifyClass) + UNVERIFIABLE = () + + def _adjust_object_before_verify(self, iface, x): + return x + + def verify(self, iface, klass, **kwargs): + return self.verifier(iface, + self._adjust_object_before_verify(iface, klass), + **kwargs) + + def test_int(self): + self.assertIsInstance(int(), abc.Integral) + self.assertTrue(self.verify(numbers.IIntegral, int)) + + def test_float(self): + self.assertIsInstance(float(), abc.Real) + self.assertTrue(self.verify(numbers.IReal, float)) + +add_abc_interface_tests(TestVerifyClass, numbers.INumber.__module__) + + +class TestVerifyObject(TestVerifyClass): + verifier = staticmethod(verifyObject) + + def _adjust_object_before_verify(self, iface, x): + return x()