Add interfaces for builtins and the io ABCs.

This commit is contained in:
Jason Madden 2020-02-13 09:03:02 -06:00
parent 5cda166377
commit a061c2d726
No known key found for this signature in database
GPG Key ID: 349F84431A08B99E
10 changed files with 449 additions and 64 deletions

View File

@ -36,3 +36,13 @@ zope.interface.common.numbers
=============================
.. automodule:: zope.interface.common.numbers
zope.interface.common.builtins
==============================
.. automodule:: zope.interface.common.builtins
zope.interface.common.io
========================
.. automodule:: zope.interface.common.io

View File

@ -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)

View File

@ -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 = ()

View File

@ -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):

View File

@ -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

View File

@ -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))()

View File

@ -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__)
}

View File

@ -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))()

View File

@ -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,
})

View File

@ -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