diff options
| author | Jason Madden <jamadden@gmail.com> | 2020-02-13 09:03:02 -0600 |
|---|---|---|
| committer | Jason Madden <jamadden@gmail.com> | 2020-02-17 07:01:03 -0600 |
| commit | a061c2d726a287fb012d1262fcf4bfe14ab134a8 (patch) | |
| tree | 8f84962d1714b9c8d69f9ede7011c283b80a735f /src | |
| parent | 5cda166377889ad1603832f28f75b02ef335f28e (diff) | |
| download | zope-interface-a061c2d726a287fb012d1262fcf4bfe14ab134a8.tar.gz | |
Add interfaces for builtins and the io ABCs.
Diffstat (limited to 'src')
| -rw-r--r-- | src/zope/interface/common/__init__.py | 83 | ||||
| -rw-r--r-- | src/zope/interface/common/builtins.py | 117 | ||||
| -rw-r--r-- | src/zope/interface/common/collections.py | 24 | ||||
| -rw-r--r-- | src/zope/interface/common/io.py | 52 | ||||
| -rw-r--r-- | src/zope/interface/common/tests/__init__.py | 28 | ||||
| -rw-r--r-- | src/zope/interface/common/tests/test_builtins.py | 65 | ||||
| -rw-r--r-- | src/zope/interface/common/tests/test_collections.py | 57 | ||||
| -rw-r--r-- | src/zope/interface/common/tests/test_io.py | 51 | ||||
| -rw-r--r-- | src/zope/interface/common/tests/test_numbers.py | 26 |
9 files changed, 439 insertions, 64 deletions
diff --git a/src/zope/interface/common/__init__.py b/src/zope/interface/common/__init__.py index d4c41ee..3269120 100644 --- a/src/zope/interface/common/__init__.py +++ b/src/zope/interface/common/__init__.py @@ -10,6 +10,7 @@ # FOR A PARTICULAR PURPOSE. ############################################################################## +import itertools from types import FunctionType from zope.interface import classImplements @@ -41,9 +42,24 @@ class ABCInterfaceClass(InterfaceClass): Internal use only. + The body of the interface definition *must* define + a property ``abc`` that is the ABC to base the interface on. + + If ``abc`` is *not* in the interface definition, a regular + interface will be defined instead (but ``extra_classes`` is still + respected). + + Use the ``@optional`` decorator on method definitions if + the ABC defines methods that are not actually required in all cases + because the Python language has multiple ways to implement a protocol. + For example, the ``iter()`` protocol can be implemented with + ``__iter__`` or the pair ``__len__`` and ``__getitem__``. + When created, any existing classes that are registered to conform to the ABC are declared to implement this interface. This is *not* - automatically updated as the ABC registry changes. + automatically updated as the ABC registry changes. If the body of the + interface definition defines ``extra_classes``, it should be a + tuple giving additional classes to declare implement the interface. Note that this is not fully symmetric. For example, it is usually the case that a subclass relationship carries the interface @@ -103,27 +119,51 @@ class ABCInterfaceClass(InterfaceClass): def __init__(self, name, bases, attrs): # go ahead and give us a name to ease debugging. self.__name__ = name + extra_classes = attrs.pop('extra_classes', ()) + + if 'abc' not in attrs: + # Something like ``IList(ISequence)``: We're extending + # abc interfaces but not an ABC interface ourself. + self.__class__ = InterfaceClass + InterfaceClass.__init__(self, name, bases, attrs) + for cls in extra_classes: + classImplements(cls, self) + return based_on = attrs.pop('abc') - if based_on is None: - # An ABC from the future, not available to us. - methods = { - '__doc__': 'This ABC is not available.' - } - else: - assert name[1:] == based_on.__name__, (name, based_on) - methods = { - # Passing the name is important in case of aliases, - # e.g., ``__ror__ = __or__``. - k: self.__method_from_function(v, k) - for k, v in vars(based_on).items() - if isinstance(v, FunctionType) and not self.__is_private_name(k) - and not self.__is_reverse_protocol_name(k) - } - methods['__doc__'] = "See `%s.%s`" % ( - based_on.__module__, - based_on.__name__, - ) + self.__abc = based_on + self.__extra_classes = tuple(extra_classes) + + assert name[1:] == based_on.__name__, (name, based_on) + methods = { + # Passing the name is important in case of aliases, + # e.g., ``__ror__ = __or__``. + k: self.__method_from_function(v, k) + for k, v in vars(based_on).items() + if isinstance(v, FunctionType) and not self.__is_private_name(k) + and not self.__is_reverse_protocol_name(k) + } + + def ref(c): + mod = c.__module__ + name = c.__name__ + if mod == str.__module__: + return "`%s`" % name + if mod == '_io': + mod = 'io' + return "`%s.%s`" % (mod, name) + implementations_doc = "\n - ".join( + ref(c) + for c in sorted(self.getRegisteredConformers(), key=ref) + ) + if implementations_doc: + implementations_doc = "\n\nKnown implementations are:\n\n - " + implementations_doc + + methods['__doc__'] = """Interface for the ABC `%s.%s`.%s""" % ( + based_on.__module__, + based_on.__name__, + implementations_doc + ) # Anything specified in the body takes precedence. # This lets us remove things that are rarely, if ever, # actually implemented. For example, ``tuple`` is registered @@ -132,7 +172,6 @@ class ABCInterfaceClass(InterfaceClass): # because it has ``__len__`` and ``__getitem__``. methods.update(attrs) InterfaceClass.__init__(self, name, bases, methods) - self.__abc = based_on self.__register_classes() @staticmethod @@ -187,7 +226,7 @@ class ABCInterfaceClass(InterfaceClass): registered = [x() for x in registry] registered = [x for x in registered if x is not None] - return registered + return set(itertools.chain(registered, self.__extra_classes)) ABCInterface = ABCInterfaceClass.__new__(ABCInterfaceClass, None, None, None) diff --git a/src/zope/interface/common/builtins.py b/src/zope/interface/common/builtins.py new file mode 100644 index 0000000..7b09036 --- /dev/null +++ b/src/zope/interface/common/builtins.py @@ -0,0 +1,117 @@ +############################################################################## +# 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 for builtin types. + +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 + +from zope.interface import classImplements + +from zope.interface.common import collections +from zope.interface.common import numbers +from zope.interface.common import io + +__all__ = [ + 'IList', + 'ITuple', + 'ITextString', + 'IByteString', + 'INativeString', + 'IBool', + 'IDict', + 'IFile', +] + +class IList(collections.IMutableSequence): + """ + Interface for :class:`list` + """ + extra_classes = (list,) + + +class ITuple(collections.ISequence): + """ + Interface for :class:`tuple` + """ + extra_classes = (tuple,) + + +class ITextString(collections.ISequence): + """ + Interface for text (unicode) strings. + + On Python 2, this is :class:`unicode`. On Python 3, + this is :class:`str` + """ + extra_classes = (type(u'unicode'),) + + +class IByteString(collections.IByteString): + """ + Interface for immutable byte strings. + + On all Python versions this is :class:`bytes`. + + Unlike :class:`zope.interface.common.collections.IByteString` + (the parent of this interface) this does *not* include + :class:`bytearray`. + """ + extra_classes = (bytes,) + + +class INativeString(IByteString if str is bytes else ITextString): + """ + Interface for native strings. + + On all Python versions, this is :class:`str`. On Python 2, + this extends :class:`IByteString`, while on Python 3 it extends + :class:`ITextString`. + """ +# We're not extending ABCInterface so extra_classes won't work +classImplements(str, INativeString) + + +class IBool(numbers.IIntegral): + """ + Interface for :class:`bool` + """ + extra_classes = (bool,) + + +class IDict(collections.IMutableMapping): + """ + Interface for :class:`dict` + """ + extra_classes = (dict,) + + +class IFile(io.IIOBase): + """ + Interface for :class:`file`. + + It is recommended to use the interfaces from :mod:`zope.interface.common.io` + instead of this interface. + + On Python 3, there is no single implementation of this interface; + depending on the arguments, the :func:`open` builtin can return + many different classes that implement different interfaces from + :mod:`zope.interface.common.io`. + """ + try: + extra_classes = (file,) + except NameError: + extra_classes = () diff --git a/src/zope/interface/common/collections.py b/src/zope/interface/common/collections.py index 6e21518..61cedc4 100644 --- a/src/zope/interface/common/collections.py +++ b/src/zope/interface/common/collections.py @@ -34,11 +34,31 @@ from __future__ import absolute_import import sys from abc import ABCMeta +# The collections imports are here, and not in +# zope.interface._compat to avoid importing collections +# unless requested. It's a big import. try: from collections import abc except ImportError: import collections as abc +try: + # On Python 3, all of these extend the appropriate collection ABC, + # but on Python 2, UserDict does not (though it is registered as a + # MutableMapping). (Importantly, UserDict on Python 2 is *not* + # registered, because it's not iterable.) Extending the ABC is not + # taken into account for interface declarations, though, so we + # need to be explicit about it. + from collections import UserList + from collections import UserDict + from collections import UserString +except ImportError: + # Python 2 + from UserList import UserList + from UserDict import IterableUserDict as UserDict + from UserString import UserString + + from zope.interface._compat import PYTHON2 as PY2 from zope.interface._compat import PYTHON3 as PY3 from zope.interface.common import ABCInterface @@ -149,6 +169,7 @@ class ICollection(ISized, class ISequence(IReversible, ICollection): abc = abc.Sequence + extra_classes = (UserString,) @optional def __reversed__(): @@ -161,6 +182,7 @@ class ISequence(IReversible, class IMutableSequence(ISequence): abc = abc.MutableSequence + extra_classes = (UserList,) class IByteString(ISequence): @@ -192,8 +214,10 @@ class IMapping(ICollection): __ne__ = __eq__ + class IMutableMapping(IMapping): abc = abc.MutableMapping + extra_classes = (UserDict,) class IMappingView(ISized): diff --git a/src/zope/interface/common/io.py b/src/zope/interface/common/io.py new file mode 100644 index 0000000..d55f7d6 --- /dev/null +++ b/src/zope/interface/common/io.py @@ -0,0 +1,52 @@ +############################################################################## +# 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:`io`. + +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 io as abc +try: + import cStringIO + import StringIO +except ImportError: + # Python 3 + extra_buffered_io_base = () +else: + extra_buffered_io_base = (StringIO.StringIO, cStringIO.InputType, cStringIO.OutputType) + +from zope.interface.common import ABCInterface + +# pylint:disable=inherit-non-class, +# pylint:disable=no-member + +class IIOBase(ABCInterface): + abc = abc.IOBase + + +class IRawIOBase(IIOBase): + abc = abc.RawIOBase + + +class IBufferedIOBase(IIOBase): + abc = abc.BufferedIOBase + extra_classes = extra_buffered_io_base + + +class ITextIOBase(IIOBase): + abc = abc.TextIOBase diff --git a/src/zope/interface/common/tests/__init__.py b/src/zope/interface/common/tests/__init__.py index 3fe3882..6298d01 100644 --- a/src/zope/interface/common/tests/__init__.py +++ b/src/zope/interface/common/tests/__init__.py @@ -10,6 +10,10 @@ # FOR A PARTICULAR PURPOSE. ############################################################################## +import unittest + +from zope.interface.verify import verifyClass +from zope.interface.verify import verifyObject from zope.interface.common import ABCInterface from zope.interface.common import ABCInterfaceClass @@ -56,3 +60,27 @@ def add_abc_interface_tests(cls, module): test.__name__ = name assert not hasattr(cls, name) setattr(cls, name, test) + + + +class VerifyClassMixin(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) + + +class VerifyObjectMixin(VerifyClassMixin): + verifier = staticmethod(verifyObject) + CONSTRUCTORS = { + } + + def _adjust_object_before_verify(self, iface, x): + return self.CONSTRUCTORS.get(iface, + self.CONSTRUCTORS.get(x, x))() diff --git a/src/zope/interface/common/tests/test_builtins.py b/src/zope/interface/common/tests/test_builtins.py new file mode 100644 index 0000000..7c6cef6 --- /dev/null +++ b/src/zope/interface/common/tests/test_builtins.py @@ -0,0 +1,65 @@ +############################################################################## +# 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. +############################################################################## +from __future__ import absolute_import + +import unittest + +from zope.interface._compat import PYTHON2 as PY2 +from zope.interface.common import builtins + +from . import VerifyClassMixin +from . import VerifyObjectMixin + + +class TestVerifyClass(VerifyClassMixin, + unittest.TestCase): + UNVERIFIABLE = ( + + ) + FILE_IMPL = () + if PY2: + UNVERIFIABLE += ( + # On both CPython and PyPy, there's no + # exposed __iter__ method for strings or unicode. + unicode, + str, + ) + FILE_IMPL = ((file, builtins.IFile),) + @classmethod + def create_tests(cls): + for klass, iface in ( + (list, builtins.IList), + (tuple, builtins.ITuple), + (type(u'abc'), builtins.ITextString), + (bytes, builtins.IByteString), + (str, builtins.INativeString), + (bool, builtins.IBool), + (dict, builtins.IDict), + ) + cls.FILE_IMPL: + def test(self, klass=klass, iface=iface): + if klass in self.UNVERIFIABLE: + self.skipTest("Cannot verify %s" % klass) + + self.assertTrue(self.verify(iface, klass)) + + name = 'test_auto_' + klass.__name__ + '_' + iface.__name__ + test.__name__ = name + setattr(cls, name, test) + +TestVerifyClass.create_tests() + + +class TestVerifyObject(VerifyObjectMixin, + TestVerifyClass): + CONSTRUCTORS = { + builtins.IFile: lambda: open(__file__) + } diff --git a/src/zope/interface/common/tests/test_collections.py b/src/zope/interface/common/tests/test_collections.py index 6b142ee..779f3c7 100644 --- a/src/zope/interface/common/tests/test_collections.py +++ b/src/zope/interface/common/tests/test_collections.py @@ -18,11 +18,13 @@ except ImportError: import collections as abc from collections import deque + try: from types import MappingProxyType except ImportError: MappingProxyType = object() +from zope.interface import Invalid from zope.interface.verify import verifyClass from zope.interface.verify import verifyObject @@ -34,19 +36,10 @@ from zope.interface._compat import PYPY from zope.interface._compat import PYTHON2 as PY2 from . import add_abc_interface_tests +from . import VerifyClassMixin +from . import VerifyObjectMixin -class TestVerifyClass(unittest.TestCase): - - verifier = staticmethod(verifyClass) - - 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) - +class TestVerifyClass(VerifyClassMixin, unittest.TestCase): # Here we test some known builtin classes that are defined to implement # various collection interfaces as a quick sanity test. @@ -58,6 +51,29 @@ class TestVerifyClass(unittest.TestCase): self.assertIsInstance(list(), abc.MutableSequence) self.assertTrue(self.verify(collections.IMutableSequence, list)) + # Here we test some derived classes. + def test_UserList(self): + self.assertTrue(self.verify(collections.IMutableSequence, + collections.UserList)) + + def test_UserDict(self): + self.assertTrue(self.verify(collections.IMutableMapping, + collections.UserDict)) + + def test_UserString(self): + self.assertTrue(self.verify(collections.ISequence, + collections.UserString)) + + def test_non_iterable_UserDict(self): + try: + from UserDict import UserDict as NonIterableUserDict # pylint:disable=import-error + except ImportError: + # Python 3 + self.skipTest("No UserDict.NonIterableUserDict on Python 3") + + with self.assertRaises(Invalid): + self.verify(collections.IMutableMapping, NonIterableUserDict) + # Now we go through the registry, which should have several things, # mostly builtins, but if we've imported other libraries already, # it could contain things from outside of there too. We aren't concerned @@ -109,25 +125,20 @@ class TestVerifyClass(unittest.TestCase): add_abc_interface_tests(TestVerifyClass, collections.ISet.__module__) - -class TestVerifyObject(TestVerifyClass): - verifier = staticmethod(verifyObject) - - _CONSTRUCTORS = { +class TestVerifyObject(VerifyObjectMixin, + TestVerifyClass): + CONSTRUCTORS = { collections.IValuesView: {}.values, collections.IItemsView: {}.items, collections.IKeysView: {}.keys, memoryview: lambda: memoryview(b'abc'), range: lambda: range(10), - MappingProxyType: lambda: MappingProxyType({}) + MappingProxyType: lambda: MappingProxyType({}), + collections.UserString: lambda: collections.UserString('abc'), } if PY2: # pylint:disable=undefined-variable,no-member - _CONSTRUCTORS.update({ + CONSTRUCTORS.update({ collections.IValuesView: {}.viewvalues, }) - - def _adjust_object_before_verify(self, iface, x): - return self._CONSTRUCTORS.get(iface, - self._CONSTRUCTORS.get(x, x))() diff --git a/src/zope/interface/common/tests/test_io.py b/src/zope/interface/common/tests/test_io.py new file mode 100644 index 0000000..c3cea86 --- /dev/null +++ b/src/zope/interface/common/tests/test_io.py @@ -0,0 +1,51 @@ +############################################################################## +# 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 io as abc + +# Note that importing z.i.c.io does work on import. +from zope.interface.common import io + +from . import add_abc_interface_tests +from . import VerifyClassMixin +from . import VerifyObjectMixin + + +class TestVerifyClass(VerifyClassMixin, + unittest.TestCase): + pass + +add_abc_interface_tests(TestVerifyClass, io.IIOBase.__module__) + + +class TestVerifyObject(VerifyObjectMixin, + TestVerifyClass): + CONSTRUCTORS = { + abc.BufferedWriter: lambda: abc.BufferedWriter(abc.StringIO()), + abc.BufferedReader: lambda: abc.BufferedReader(abc.StringIO()), + abc.TextIOWrapper: lambda: abc.TextIOWrapper(abc.BytesIO()), + abc.BufferedRandom: lambda: abc.BufferedRandom(abc.BytesIO()), + abc.BufferedRWPair: lambda: abc.BufferedRWPair(abc.BytesIO(), abc.BytesIO()), + abc.FileIO: lambda: abc.FileIO(__file__), + } + + try: + import cStringIO + except ImportError: + pass + else: + CONSTRUCTORS.update({ + cStringIO.InputType: lambda cStringIO=cStringIO: cStringIO.StringIO('abc'), + cStringIO.OutputType: cStringIO.StringIO, + }) diff --git a/src/zope/interface/common/tests/test_numbers.py b/src/zope/interface/common/tests/test_numbers.py index 7400838..abf9695 100644 --- a/src/zope/interface/common/tests/test_numbers.py +++ b/src/zope/interface/common/tests/test_numbers.py @@ -14,26 +14,16 @@ 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 +from . import VerifyClassMixin +from . import VerifyObjectMixin -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) +class TestVerifyClass(VerifyClassMixin, + unittest.TestCase): def test_int(self): self.assertIsInstance(int(), abc.Integral) @@ -46,8 +36,6 @@ class TestVerifyClass(unittest.TestCase): add_abc_interface_tests(TestVerifyClass, numbers.INumber.__module__) -class TestVerifyObject(TestVerifyClass): - verifier = staticmethod(verifyObject) - - def _adjust_object_before_verify(self, iface, x): - return x() +class TestVerifyObject(VerifyObjectMixin, + TestVerifyClass): + pass |
