summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2020-02-13 09:03:02 -0600
committerJason Madden <jamadden@gmail.com>2020-02-17 07:01:03 -0600
commita061c2d726a287fb012d1262fcf4bfe14ab134a8 (patch)
tree8f84962d1714b9c8d69f9ede7011c283b80a735f /src
parent5cda166377889ad1603832f28f75b02ef335f28e (diff)
downloadzope-interface-a061c2d726a287fb012d1262fcf4bfe14ab134a8.tar.gz
Add interfaces for builtins and the io ABCs.
Diffstat (limited to 'src')
-rw-r--r--src/zope/interface/common/__init__.py83
-rw-r--r--src/zope/interface/common/builtins.py117
-rw-r--r--src/zope/interface/common/collections.py24
-rw-r--r--src/zope/interface/common/io.py52
-rw-r--r--src/zope/interface/common/tests/__init__.py28
-rw-r--r--src/zope/interface/common/tests/test_builtins.py65
-rw-r--r--src/zope/interface/common/tests/test_collections.py57
-rw-r--r--src/zope/interface/common/tests/test_io.py51
-rw-r--r--src/zope/interface/common/tests/test_numbers.py26
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