summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2020-03-19 07:40:56 -0500
committerJason Madden <jamadden@gmail.com>2020-03-19 07:40:56 -0500
commit6c98b3381a5fbe5023c72bb560aabffb1af63df3 (patch)
tree86eafb004bcf13e270d129e7c632329305919b66
parent4c4e1c985ffa710482cfeebaf1cb28b470bab47e (diff)
parent04152ae7d2687bc7f100f4f504a342e84fc5c167 (diff)
downloadzope-interface-6c98b3381a5fbe5023c72bb560aabffb1af63df3.tar.gz
Merge branch 'faster-eq-hash-comparison'
-rw-r--r--.gitignore1
-rw-r--r--CHANGES.rst11
-rw-r--r--MANIFEST.in1
-rw-r--r--benchmarks/.gitignore1
-rw-r--r--benchmarks/micro.py234
-rw-r--r--src/zope/interface/_compat.py1
-rw-r--r--src/zope/interface/_zope_interface_coptimizations.c243
-rw-r--r--src/zope/interface/declarations.py153
-rw-r--r--src/zope/interface/interface.py432
-rw-r--r--src/zope/interface/ro.py65
-rw-r--r--src/zope/interface/tests/__init__.py22
-rw-r--r--src/zope/interface/tests/test_declarations.py32
-rw-r--r--src/zope/interface/tests/test_interface.py218
-rw-r--r--src/zope/interface/tests/test_registry.py5
-rw-r--r--src/zope/interface/tests/test_ro.py21
-rw-r--r--src/zope/interface/tests/test_sorting.py17
-rw-r--r--tox.ini4
17 files changed, 1169 insertions, 292 deletions
diff --git a/.gitignore b/.gitignore
index 35946f4..cbf117f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
*.egg-info
*.pyc
+*.pyo
*.so
__pycache__
.coverage
diff --git a/CHANGES.rst b/CHANGES.rst
index 3d9bb5d..9ba0d8b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -210,6 +210,17 @@
hierarchy, ``Interface`` could be assigned too high a priority.
See `issue 8 <https://github.com/zopefoundation/zope.interface/issues/8>`_.
+- Implement sorting, equality, and hashing in C for ``Interface``
+ objects. In micro benchmarks, this makes those operations 40% to 80%
+ faster. This translates to a 20% speed up in querying adapters.
+
+ Note that this changes certain implementation details. In
+ particular, ``InterfaceClass`` now has a non-default metaclass, and
+ it is enforced that ``__module__`` in instances of
+ ``InterfaceClass`` is read-only.
+
+ See `PR 183 <https://github.com/zopefoundation/zope.interface/pull/183>`_.
+
4.7.2 (2020-03-10)
==================
diff --git a/MANIFEST.in b/MANIFEST.in
index 1dcbd31..83a0eb8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -18,3 +18,4 @@ global-exclude coverage.xml
global-exclude appveyor.yml
prune docs/_build
+prune benchmarks
diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore
new file mode 100644
index 0000000..a6c57f5
--- /dev/null
+++ b/benchmarks/.gitignore
@@ -0,0 +1 @@
+*.json
diff --git a/benchmarks/micro.py b/benchmarks/micro.py
new file mode 100644
index 0000000..a9527db
--- /dev/null
+++ b/benchmarks/micro.py
@@ -0,0 +1,234 @@
+import pyperf
+
+from zope.interface import Interface
+from zope.interface import classImplements
+from zope.interface import implementedBy
+from zope.interface.interface import InterfaceClass
+from zope.interface.registry import Components
+
+# Long, mostly similar names are a worst case for equality
+# comparisons.
+ifaces = [
+ InterfaceClass('I' + ('0' * 20) + str(i), (Interface,), {})
+ for i in range(100)
+]
+
+class IWideInheritance(*ifaces):
+ """
+ Inherits from 100 unrelated interfaces.
+ """
+
+class WideInheritance(object):
+ pass
+classImplements(WideInheritance, IWideInheritance)
+
+def make_deep_inheritance():
+ children = []
+ base = Interface
+ for iface in ifaces:
+ child = InterfaceClass('IDerived' + base.__name__, (iface, base,), {})
+ base = child
+ children.append(child)
+ return children
+
+deep_ifaces = make_deep_inheritance()
+
+class DeepestInheritance(object):
+ pass
+classImplements(DeepestInheritance, deep_ifaces[-1])
+
+def make_implementer(iface):
+ c = type('Implementer' + iface.__name__, (object,), {})
+ classImplements(c, iface)
+ return c
+
+implementers = [
+ make_implementer(iface)
+ for iface in ifaces
+]
+
+providers = [
+ implementer()
+ for implementer in implementers
+]
+
+INNER = 100
+
+def bench_in(loops, o):
+ t0 = pyperf.perf_counter()
+ for _ in range(loops):
+ for _ in range(INNER):
+ o.__contains__(Interface)
+
+ return pyperf.perf_counter() - t0
+
+def bench_sort(loops, objs):
+ import random
+ rand = random.Random(8675309)
+
+ shuffled = list(objs)
+ rand.shuffle(shuffled)
+
+ t0 = pyperf.perf_counter()
+ for _ in range(loops):
+ for _ in range(INNER):
+ sorted(shuffled)
+
+ return pyperf.perf_counter() - t0
+
+def bench_query_adapter(loops, components, objs=providers):
+ # One time through to prime the caches
+ for iface in ifaces:
+ for provider in providers:
+ components.queryAdapter(provider, iface)
+
+ t0 = pyperf.perf_counter()
+ for _ in range(loops):
+ for iface in ifaces:
+ for provider in objs:
+ components.queryAdapter(provider, iface)
+ return pyperf.perf_counter() - t0
+
+
+def bench_getattr(loops, name, get=getattr):
+ t0 = pyperf.perf_counter()
+ for _ in range(loops):
+ for _ in range(INNER):
+ get(Interface, name) # 1
+ get(Interface, name) # 2
+ get(Interface, name) # 3
+ get(Interface, name) # 4
+ get(Interface, name) # 5
+ get(Interface, name) # 6
+ get(Interface, name) # 7
+ get(Interface, name) # 8
+ get(Interface, name) # 9
+ get(Interface, name) # 10
+ return pyperf.perf_counter() - t0
+
+runner = pyperf.Runner()
+
+runner.bench_time_func(
+ 'read __module__', # stored in C, accessed through __getattribute__
+ bench_getattr,
+ '__module__',
+ inner_loops=INNER * 10
+)
+
+runner.bench_time_func(
+ 'read __name__', # stored in C, accessed through PyMemberDef
+ bench_getattr,
+ '__name__',
+ inner_loops=INNER * 10
+)
+
+runner.bench_time_func(
+ 'read __doc__', # stored in Python instance dictionary directly
+ bench_getattr,
+ '__doc__',
+ inner_loops=INNER * 10
+)
+
+runner.bench_time_func(
+ 'read providedBy', # from the class, wrapped into a method object.
+ bench_getattr,
+ 'providedBy',
+ inner_loops=INNER * 10
+)
+
+runner.bench_time_func(
+ 'query adapter (no registrations)',
+ bench_query_adapter,
+ Components(),
+ inner_loops=1
+)
+
+def populate_components():
+ def factory(o):
+ return 42
+
+ pop_components = Components()
+ for iface in ifaces:
+ for other_iface in ifaces:
+ pop_components.registerAdapter(factory, (iface,), other_iface, event=False)
+
+ return pop_components
+
+runner.bench_time_func(
+ 'query adapter (all trivial registrations)',
+ bench_query_adapter,
+ populate_components(),
+ inner_loops=1
+)
+
+runner.bench_time_func(
+ 'query adapter (all trivial registrations, wide inheritance)',
+ bench_query_adapter,
+ populate_components(),
+ [WideInheritance()],
+ inner_loops=1
+)
+
+runner.bench_time_func(
+ 'query adapter (all trivial registrations, deep inheritance)',
+ bench_query_adapter,
+ populate_components(),
+ [DeepestInheritance()],
+ inner_loops=1
+)
+
+runner.bench_time_func(
+ 'sort interfaces',
+ bench_sort,
+ ifaces,
+ inner_loops=INNER,
+)
+
+runner.bench_time_func(
+ 'sort implementedBy',
+ bench_sort,
+ [implementedBy(p) for p in implementers],
+ inner_loops=INNER,
+)
+
+runner.bench_time_func(
+ 'sort mixed',
+ bench_sort,
+ [implementedBy(p) for p in implementers] + ifaces,
+ inner_loops=INNER,
+)
+
+runner.bench_time_func(
+ 'contains (empty dict)',
+ bench_in,
+ {},
+ inner_loops=INNER
+)
+
+runner.bench_time_func(
+ 'contains (populated dict: interfaces)',
+ bench_in,
+ {k: k for k in ifaces},
+ inner_loops=INNER
+)
+
+runner.bench_time_func(
+ 'contains (populated list: interfaces)',
+ bench_in,
+ ifaces,
+ inner_loops=INNER
+)
+
+runner.bench_time_func(
+ 'contains (populated dict: implementedBy)',
+ bench_in,
+ {implementedBy(p): 1 for p in implementers},
+ inner_loops=INNER
+)
+
+runner.bench_time_func(
+ 'contains (populated list: implementedBy)',
+ bench_in,
+ [implementedBy(p) for p in implementers],
+ inner_loops=INNER
+)
diff --git a/src/zope/interface/_compat.py b/src/zope/interface/_compat.py
index a57951a..3587463 100644
--- a/src/zope/interface/_compat.py
+++ b/src/zope/interface/_compat.py
@@ -166,4 +166,5 @@ def _use_c_impl(py_impl, name=None, globs=None):
# name (for testing and documentation)
globs[name + 'Py'] = py_impl
globs[name + 'Fallback'] = py_impl
+
return c_impl
diff --git a/src/zope/interface/_zope_interface_coptimizations.c b/src/zope/interface/_zope_interface_coptimizations.c
index 180cdef..4e09614 100644
--- a/src/zope/interface/_zope_interface_coptimizations.c
+++ b/src/zope/interface/_zope_interface_coptimizations.c
@@ -33,6 +33,9 @@
#if PY_MAJOR_VERSION >= 3
#define PY3K
+#define PyNative_FromString PyUnicode_FromString
+#else
+#define PyNative_FromString PyString_FromString
#endif
static PyObject *str__dict__, *str__implemented__, *strextends;
@@ -44,6 +47,8 @@ static PyObject *str_uncached_lookup, *str_uncached_lookupAll;
static PyObject *str_uncached_subscriptions;
static PyObject *str_registry, *strro, *str_generation, *strchanged;
static PyObject *str__self__;
+static PyObject *str__module__;
+static PyObject *str__name__;
static PyTypeObject *Implements;
@@ -93,7 +98,7 @@ import_declarations(void)
}
-static PyTypeObject SpecType; /* Forward */
+static PyTypeObject SpecificationBaseType; /* Forward */
static PyObject *
implementedByFallback(PyObject *cls)
@@ -173,7 +178,7 @@ getObjectSpecification(PyObject *ignored, PyObject *ob)
PyObject *cls, *result;
result = PyObject_GetAttr(ob, str__provides__);
- if (result != NULL && PyObject_TypeCheck(result, &SpecType))
+ if (result != NULL && PyObject_TypeCheck(result, &SpecificationBaseType))
return result;
@@ -221,7 +226,7 @@ providedBy(PyObject *ignored, PyObject *ob)
because we may have a proxy, so we'll just try to get the
only attribute.
*/
- if (PyObject_TypeCheck(result, &SpecType)
+ if (PyObject_TypeCheck(result, &SpecificationBaseType)
||
PyObject_HasAttr(result, strextends)
)
@@ -325,6 +330,9 @@ Spec_clear(Spec* self)
static void
Spec_dealloc(Spec* self)
{
+ /* PyType_GenericAlloc that you get when you don't
+ specify a tp_alloc always tracks the object. */
+ PyObject_GC_UnTrack((PyObject *)self);
if (self->weakreflist != NULL) {
PyObject_ClearWeakRefs(OBJECT(self));
}
@@ -374,7 +382,7 @@ Spec_providedBy(PyObject *self, PyObject *ob)
if (decl == NULL)
return NULL;
- if (PyObject_TypeCheck(decl, &SpecType))
+ if (PyObject_TypeCheck(decl, &SpecificationBaseType))
item = Spec_extends((Spec*)decl, self);
else
/* decl is probably a security proxy. We have to go the long way
@@ -401,7 +409,7 @@ Spec_implementedBy(PyObject *self, PyObject *cls)
if (decl == NULL)
return NULL;
- if (PyObject_TypeCheck(decl, &SpecType))
+ if (PyObject_TypeCheck(decl, &SpecificationBaseType))
item = Spec_extends((Spec*)decl, self);
else
item = PyObject_CallFunctionObjArgs(decl, self, NULL);
@@ -434,7 +442,7 @@ static PyMemberDef Spec_members[] = {
};
-static PyTypeObject SpecType = {
+static PyTypeObject SpecificationBaseType = {
PyVarObject_HEAD_INIT(NULL, 0)
/* tp_name */ "_interface_coptimizations."
"SpecificationBase",
@@ -557,7 +565,7 @@ CPB_traverse(CPB* self, visitproc visit, void* arg)
{
Py_VISIT(self->_cls);
Py_VISIT(self->_implements);
- return 0;
+ return Spec_traverse((Spec*)self, visit, arg);
}
static int
@@ -565,14 +573,16 @@ CPB_clear(CPB* self)
{
Py_CLEAR(self->_cls);
Py_CLEAR(self->_implements);
+ Spec_clear((Spec*)self);
return 0;
}
static void
CPB_dealloc(CPB* self)
{
+ PyObject_GC_UnTrack((PyObject *)self);
CPB_clear(self);
- Py_TYPE(self)->tp_free(OBJECT(self));
+ Spec_dealloc((Spec*)self);
}
static PyMemberDef CPB_members[] = {
@@ -613,7 +623,7 @@ static PyTypeObject CPBType = {
/* tp_methods */ 0,
/* tp_members */ CPB_members,
/* tp_getset */ 0,
- /* tp_base */ &SpecType,
+ /* tp_base */ &SpecificationBaseType,
/* tp_dict */ 0, /* internal use */
/* tp_descr_get */ (descrgetfunc)CPB_descr_get,
/* tp_descr_set */ 0,
@@ -650,7 +660,7 @@ __adapt__(PyObject *self, PyObject *obj)
if (decl == NULL)
return NULL;
- if (PyObject_TypeCheck(decl, &SpecType))
+ if (PyObject_TypeCheck(decl, &SpecificationBaseType))
{
PyObject *implied;
@@ -709,6 +719,17 @@ __adapt__(PyObject *self, PyObject *obj)
return Py_None;
}
+#ifndef PY3K
+typedef long Py_hash_t;
+#endif
+
+typedef struct {
+ Spec spec;
+ PyObject* __name__;
+ PyObject* __module__;
+ Py_hash_t _v_cached_hash;
+} IB;
+
static struct PyMethodDef ib_methods[] = {
{"__adapt__", (PyCFunction)__adapt__, METH_O,
"Adapt an object to the reciever"},
@@ -776,13 +797,185 @@ ib_call(PyObject *self, PyObject *args, PyObject *kwargs)
return NULL;
}
+
+static int
+IB_traverse(IB* self, visitproc visit, void* arg)
+{
+ Py_VISIT(self->__name__);
+ Py_VISIT(self->__module__);
+ return Spec_traverse((Spec*)self, visit, arg);
+}
+
+static int
+IB_clear(IB* self)
+{
+ Py_CLEAR(self->__name__);
+ Py_CLEAR(self->__module__);
+ return Spec_clear((Spec*)self);
+}
+
+static void
+IB_dealloc(IB* self)
+{
+ PyObject_GC_UnTrack((PyObject *)self);
+ IB_clear(self);
+ Spec_dealloc((Spec*)self);
+}
+
+static PyMemberDef IB_members[] = {
+ {"__name__", T_OBJECT_EX, offsetof(IB, __name__), 0, ""},
+ // The redundancy between __module__ and __ibmodule__ is because
+ // __module__ is often shadowed by subclasses.
+ {"__module__", T_OBJECT_EX, offsetof(IB, __module__), READONLY, ""},
+ {"__ibmodule__", T_OBJECT_EX, offsetof(IB, __module__), 0, ""},
+ {NULL}
+};
+
+static Py_hash_t
+IB_hash(IB* self)
+{
+ PyObject* tuple;
+ if (!self->__module__) {
+ PyErr_SetString(PyExc_AttributeError, "__module__");
+ return -1;
+ }
+ if (!self->__name__) {
+ PyErr_SetString(PyExc_AttributeError, "__name__");
+ return -1;
+ }
+
+ if (self->_v_cached_hash) {
+ return self->_v_cached_hash;
+ }
+
+ tuple = PyTuple_Pack(2, self->__name__, self->__module__);
+ if (!tuple) {
+ return -1;
+ }
+ self->_v_cached_hash = PyObject_Hash(tuple);
+ Py_CLEAR(tuple);
+ return self->_v_cached_hash;
+}
+
+static PyTypeObject InterfaceBaseType;
+
+static PyObject*
+IB_richcompare(IB* self, PyObject* other, int op)
+{
+ PyObject* othername;
+ PyObject* othermod;
+ PyObject* oresult;
+ IB* otherib;
+ int result;
+
+ otherib = NULL;
+ oresult = othername = othermod = NULL;
+
+ if (OBJECT(self) == other) {
+ switch(op) {
+ case Py_EQ:
+ case Py_LE:
+ case Py_GE:
+ Py_RETURN_TRUE;
+ break;
+ case Py_NE:
+ Py_RETURN_FALSE;
+ }
+ }
+
+ if (other == Py_None) {
+ switch(op) {
+ case Py_LT:
+ case Py_LE:
+ case Py_NE:
+ Py_RETURN_TRUE;
+ default:
+ Py_RETURN_FALSE;
+ }
+ }
+
+ if (PyObject_TypeCheck(other, &InterfaceBaseType)) {
+ // This branch borrows references. No need to clean
+ // up if otherib is not null.
+ otherib = (IB*)other;
+ othername = otherib->__name__;
+ othermod = otherib->__module__;
+ }
+ else {
+ othername = PyObject_GetAttrString(other, "__name__");
+ if (othername) {
+ othermod = PyObject_GetAttrString(other, "__module__");
+ }
+ if (!othername || !othermod) {
+ if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_AttributeError)) {
+ PyErr_Clear();
+ oresult = Py_NotImplemented;
+ }
+ goto cleanup;
+ }
+ }
+#if 0
+// This is the simple, straightforward version of what Python does.
+ PyObject* pt1 = PyTuple_Pack(2, self->__name__, self->__module__);
+ PyObject* pt2 = PyTuple_Pack(2, othername, othermod);
+ oresult = PyObject_RichCompare(pt1, pt2, op);
+#endif
+
+ // tuple comparison is decided by the first non-equal element.
+ result = PyObject_RichCompareBool(self->__name__, othername, Py_EQ);
+ if (result == 0) {
+ result = PyObject_RichCompareBool(self->__name__, othername, op);
+ }
+ else if (result == 1) {
+ result = PyObject_RichCompareBool(self->__module__, othermod, op);
+ }
+ // If either comparison failed, we have an error set.
+ // Leave oresult NULL so we raise it.
+ if (result == -1) {
+ goto cleanup;
+ }
+
+ oresult = result ? Py_True : Py_False;
+
+
+cleanup:
+ Py_XINCREF(oresult);
+
+ if (!otherib) {
+ Py_XDECREF(othername);
+ Py_XDECREF(othermod);
+ }
+ return oresult;
+
+}
+
+static int
+IB_init(IB* self, PyObject* args, PyObject* kwargs)
+{
+ static char *kwlist[] = {"__name__", "__module__", NULL};
+ PyObject* module = NULL;
+ PyObject* name = NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OO:InterfaceBase.__init__", kwlist,
+ &name, &module)) {
+ return -1;
+ }
+ IB_clear(self);
+ self->__module__ = module ? module : Py_None;
+ Py_INCREF(self->__module__);
+ self->__name__ = name ? name : Py_None;
+ Py_INCREF(self->__name__);
+ return 0;
+}
+
+
static PyTypeObject InterfaceBaseType = {
PyVarObject_HEAD_INIT(NULL, 0)
/* tp_name */ "_zope_interface_coptimizations."
"InterfaceBase",
- /* tp_basicsize */ 0,
+ /* tp_basicsize */ sizeof(IB),
/* tp_itemsize */ 0,
- /* tp_dealloc */ (destructor)0,
+ /* tp_dealloc */ (destructor)IB_dealloc,
/* tp_print */ (printfunc)0,
/* tp_getattr */ (getattrfunc)0,
/* tp_setattr */ (setattrfunc)0,
@@ -791,22 +984,30 @@ static PyTypeObject InterfaceBaseType = {
/* tp_as_number */ 0,
/* tp_as_sequence */ 0,
/* tp_as_mapping */ 0,
- /* tp_hash */ (hashfunc)0,
+ /* tp_hash */ (hashfunc)IB_hash,
/* tp_call */ (ternaryfunc)ib_call,
/* tp_str */ (reprfunc)0,
/* tp_getattro */ (getattrofunc)0,
/* tp_setattro */ (setattrofunc)0,
/* tp_as_buffer */ 0,
/* tp_flags */ Py_TPFLAGS_DEFAULT
- | Py_TPFLAGS_BASETYPE ,
+ | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
/* tp_doc */ "Interface base type providing __call__ and __adapt__",
- /* tp_traverse */ (traverseproc)0,
- /* tp_clear */ (inquiry)0,
- /* tp_richcompare */ (richcmpfunc)0,
+ /* tp_traverse */ (traverseproc)IB_traverse,
+ /* tp_clear */ (inquiry)IB_clear,
+ /* tp_richcompare */ (richcmpfunc)IB_richcompare,
/* tp_weaklistoffset */ (long)0,
/* tp_iter */ (getiterfunc)0,
/* tp_iternext */ (iternextfunc)0,
/* tp_methods */ ib_methods,
+ /* tp_members */ IB_members,
+ /* tp_getset */ 0,
+ /* tp_base */ &SpecificationBaseType,
+ /* tp_dict */ 0,
+ /* tp_descr_get */ 0,
+ /* tp_descr_set */ 0,
+ /* tp_dictoffset */ 0,
+ /* tp_init */ (initproc)IB_init,
};
/* =================== End: __call__ and __adapt__ ==================== */
@@ -1760,14 +1961,16 @@ init(void)
DEFINE_STRING(ro);
DEFINE_STRING(changed);
DEFINE_STRING(__self__);
+ DEFINE_STRING(__name__);
+ DEFINE_STRING(__module__);
#undef DEFINE_STRING
adapter_hooks = PyList_New(0);
if (adapter_hooks == NULL)
return NULL;
/* Initialize types: */
- SpecType.tp_new = PyBaseObject_Type.tp_new;
- if (PyType_Ready(&SpecType) < 0)
+ SpecificationBaseType.tp_new = PyBaseObject_Type.tp_new;
+ if (PyType_Ready(&SpecificationBaseType) < 0)
return NULL;
OSDType.tp_new = PyBaseObject_Type.tp_new;
if (PyType_Ready(&OSDType) < 0)
@@ -1799,7 +2002,7 @@ init(void)
return NULL;
/* Add types: */
- if (PyModule_AddObject(m, "SpecificationBase", OBJECT(&SpecType)) < 0)
+ if (PyModule_AddObject(m, "SpecificationBase", OBJECT(&SpecificationBaseType)) < 0)
return NULL;
if (PyModule_AddObject(m, "ObjectSpecificationDescriptor",
(PyObject *)&OSDType) < 0)
diff --git a/src/zope/interface/declarations.py b/src/zope/interface/declarations.py
index 1e9a2ea..d85dbf4 100644
--- a/src/zope/interface/declarations.py
+++ b/src/zope/interface/declarations.py
@@ -36,6 +36,7 @@ from zope.interface.advice import addClassAdvisor
from zope.interface.interface import InterfaceClass
from zope.interface.interface import SpecificationBase
from zope.interface.interface import Specification
+from zope.interface.interface import NameAndModuleComparisonMixin
from zope.interface._compat import CLASS_TYPES as DescriptorAwareMetaClasses
from zope.interface._compat import PYTHON3
from zope.interface._compat import _use_c_impl
@@ -45,6 +46,8 @@ __all__ = [
# re-exported from zope.interface directly.
]
+# pylint:disable=too-many-lines
+
# Registry of class-implementation specifications
BuiltinImplementationSpecifications = {}
@@ -101,12 +104,14 @@ class Declaration(Specification):
def __sub__(self, other):
"""Remove interfaces from a specification
"""
- return Declaration(
- *[i for i in self.interfaces()
- if not [j for j in other.interfaces()
- if i.extends(j, 0)]
+ return Declaration(*[
+ i for i in self.interfaces()
+ if not [
+ j
+ for j in other.interfaces()
+ if i.extends(j, 0) # non-strict extends
]
- )
+ ])
def __add__(self, other):
"""Add two specifications or a specification and an interface
@@ -197,7 +202,29 @@ class _ImmutableDeclaration(Declaration):
#
# These specify interfaces implemented by instances of classes
-class Implements(Declaration):
+class Implements(NameAndModuleComparisonMixin,
+ Declaration):
+ # Inherit from NameAndModuleComparisonMixin to be
+ # mutually comparable with InterfaceClass objects.
+ # (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.
+
+ # We want equality and hashing to be based on identity. However, we can't actually
+ # implement __eq__/__ne__ to do this because sometimes we get wrapped in a proxy.
+ # We need to let the proxy types implement these methods so they can handle unwrapping
+ # and then rely on: (1) the interpreter automatically changing `implements == proxy` into
+ # `proxy == implements` (which will call proxy.__eq__ to do the unwrapping) and then
+ # (2) the default equality and hashing semantics being identity based.
# class whose specification should be used as additional base
inherit = None
@@ -206,7 +233,9 @@ class Implements(Declaration):
declared = ()
# Weak cache of {class: <implements>} for super objects.
- # Created on demand.
+ # Created on demand. These are rare, as of 5.0 anyway. Using a class
+ # level default doesn't take space in instances. Using _v_attrs would be
+ # another place to store this without taking space unless needed.
_super_cache = None
__name__ = '?'
@@ -224,7 +253,10 @@ class Implements(Declaration):
return inst
def changed(self, originally_changed):
- self._super_cache = None
+ try:
+ del self._super_cache
+ except AttributeError:
+ pass
return super(Implements, self).changed(originally_changed)
def __repr__(self):
@@ -233,53 +265,6 @@ class Implements(Declaration):
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)
-
- # We want equality and hashing to be based on identity. However, we can't actually
- # implement __eq__/__ne__ to do this because sometimes we get wrapped in a proxy.
- # We need to let the proxy types implement these methods so they can handle unwrapping
- # and then rely on: (1) the interpreter automatically changing `implements == proxy` into
- # `proxy == implements` (which will call proxy.__eq__ to do the unwrapping) and then
- # (2) the default equality and hashing semantics being identity based.
-
- 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__
@@ -307,7 +292,7 @@ def _implementedBy_super(sup):
# that excludes the classes being skipped over but
# includes everything else.
implemented_by_self = implementedBy(sup.__self_class__)
- cache = implemented_by_self._super_cache
+ cache = implemented_by_self._super_cache # pylint:disable=protected-access
if cache is None:
cache = implemented_by_self._super_cache = weakref.WeakKeyDictionary()
@@ -342,7 +327,7 @@ def _implementedBy_super(sup):
@_use_c_impl
-def implementedBy(cls):
+def implementedBy(cls): # pylint:disable=too-many-return-statements,too-many-branches
"""Return the interfaces implemented for a class' instances
The value returned is an `~zope.interface.interfaces.IDeclaration`.
@@ -417,8 +402,7 @@ def implementedBy(cls):
cls.__providedBy__ = objectSpecificationDescriptor
if (isinstance(cls, DescriptorAwareMetaClasses)
- and
- '__provides__' not in cls.__dict__):
+ and '__provides__' not in cls.__dict__):
# Make sure we get a __provides__ descriptor
cls.__provides__ = ClassProvides(
cls,
@@ -523,9 +507,9 @@ def _classImplements_ordered(spec, before=(), after=()):
def _implements_advice(cls):
- interfaces, classImplements = cls.__dict__['__implements_advice_data__']
+ interfaces, do_classImplements = cls.__dict__['__implements_advice_data__']
del cls.__implements_advice_data__
- classImplements(cls, *interfaces)
+ do_classImplements(cls, *interfaces)
return cls
@@ -557,6 +541,7 @@ class implementer(object):
after the class has been created.
"""
+ __slots__ = ('interfaces',)
def __init__(self, *interfaces):
self.interfaces = interfaces
@@ -607,7 +592,7 @@ class implementer_only(object):
if isinstance(ob, (FunctionType, MethodType)):
# XXX Does this decorator make sense for anything but classes?
# I don't think so. There can be no inheritance of interfaces
- # on a method pr function....
+ # on a method or function....
raise ValueError('The implementer_only decorator is not '
'supported for methods or functions.')
else:
@@ -615,11 +600,11 @@ class implementer_only(object):
classImplementsOnly(ob, *self.interfaces)
return ob
-def _implements(name, interfaces, classImplements):
+def _implements(name, interfaces, do_classImplements):
# This entire approach is invalid under Py3K. Don't even try to fix
# the coverage for this block there. :(
- frame = sys._getframe(2)
- locals = frame.f_locals
+ frame = sys._getframe(2) # pylint:disable=protected-access
+ locals = frame.f_locals # pylint:disable=redefined-builtin
# Try to make sure we were called from a class def. In 2.2.0 we can't
# check for __module__ since it doesn't seem to be added to the locals
@@ -630,7 +615,7 @@ def _implements(name, interfaces, classImplements):
if '__implements_advice_data__' in locals:
raise TypeError(name+" can be used only once in a class definition.")
- locals['__implements_advice_data__'] = interfaces, classImplements
+ locals['__implements_advice_data__'] = interfaces, do_classImplements
addClassAdvisor(_implements_advice, depth=3)
def implements(*interfaces):
@@ -737,7 +722,7 @@ ProvidesClass = Provides
# This is a memory optimization to allow objects to share specifications.
InstanceDeclarations = weakref.WeakValueDictionary()
-def Provides(*interfaces):
+def Provides(*interfaces): # pylint:disable=function-redefined
"""Cache instance declarations
Instance declarations are shared among instances that have the same
@@ -753,7 +738,7 @@ def Provides(*interfaces):
Provides.__safe_for_unpickling__ = True
-def directlyProvides(object, *interfaces):
+def directlyProvides(object, *interfaces): # pylint:disable=redefined-builtin
"""Declare interfaces declared directly for an object
The arguments after the object are one or more interfaces or interface
@@ -789,7 +774,7 @@ def directlyProvides(object, *interfaces):
object.__provides__ = Provides(cls, *interfaces)
-def alsoProvides(object, *interfaces):
+def alsoProvides(object, *interfaces): # pylint:disable=redefined-builtin
"""Declare interfaces declared directly for an object
The arguments after the object are one or more interfaces or interface
@@ -801,7 +786,7 @@ def alsoProvides(object, *interfaces):
directlyProvides(object, directlyProvidedBy(object), *interfaces)
-def noLongerProvides(object, interface):
+def noLongerProvides(object, interface): # pylint:disable=redefined-builtin
""" Removes a directly provided interface from an object.
"""
directlyProvides(object, directlyProvidedBy(object) - interface)
@@ -818,6 +803,8 @@ class ClassProvidesBase(SpecificationBase):
)
def __get__(self, inst, cls):
+ # member slots are set by subclass
+ # pylint:disable=no-member
if cls is self._cls:
# We only work if called on the class we were defined for
@@ -839,6 +826,10 @@ class ClassProvides(Declaration, ClassProvidesBase):
interfaces a bit quicker.
"""
+ __slots__ = (
+ '__args',
+ )
+
def __init__(self, cls, metacls, *interfaces):
self._cls = cls
self._implements = implementedBy(cls)
@@ -859,18 +850,18 @@ class ClassProvides(Declaration, ClassProvidesBase):
__get__ = ClassProvidesBase.__get__
-def directlyProvidedBy(object):
+def directlyProvidedBy(object): # pylint:disable=redefined-builtin
"""Return the interfaces directly provided by the given object
The value returned is an `~zope.interface.interfaces.IDeclaration`.
"""
provides = getattr(object, "__provides__", None)
- if (provides is None # no spec
- or
- # We might have gotten the implements spec, as an
- # optimization. If so, it's like having only one base, that we
- # lop off to exclude class-supplied declarations:
- isinstance(provides, Implements)
+ if (
+ provides is None # no spec
+ # We might have gotten the implements spec, as an
+ # optimization. If so, it's like having only one base, that we
+ # lop off to exclude class-supplied declarations:
+ or isinstance(provides, Implements)
):
return _empty
@@ -912,8 +903,8 @@ def classProvides(*interfaces):
if PYTHON3:
raise TypeError(_ADVICE_ERROR % 'provider')
- frame = sys._getframe(1)
- locals = frame.f_locals
+ frame = sys._getframe(1) # pylint:disable=protected-access
+ locals = frame.f_locals # pylint:disable=redefined-builtin
# Try to make sure we were called from a class def
if (locals is frame.f_globals) or ('__module__' not in locals):
@@ -971,8 +962,8 @@ def moduleProvides(*interfaces):
directlyProvides(sys.modules[__name__], I1)
"""
- frame = sys._getframe(1)
- locals = frame.f_locals
+ frame = sys._getframe(1) # pylint:disable=protected-access
+ locals = frame.f_locals # pylint:disable=redefined-builtin
# Try to make sure we were called from a class def
if (locals is not frame.f_globals) or ('__name__' not in locals):
diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py
index 34f54ba..e654b27 100644
--- a/src/zope/interface/interface.py
+++ b/src/zope/interface/interface.py
@@ -13,7 +13,7 @@
##############################################################################
"""Interface object implementation
"""
-
+# pylint:disable=protected-access
import sys
from types import MethodType
from types import FunctionType
@@ -21,8 +21,8 @@ import weakref
from zope.interface._compat import _use_c_impl
from zope.interface.exceptions import Invalid
-from zope.interface.ro import ro
-from zope.interface.ro import C3
+from zope.interface.ro import ro as calculate_ro
+from zope.interface import ro
__all__ = [
# Most of the public API from this module is directly exported
@@ -69,7 +69,7 @@ class Element(object):
#
#implements(IElement)
- def __init__(self, __name__, __doc__=''):
+ def __init__(self, __name__, __doc__=''): # pylint:disable=redefined-builtin
if not __doc__ and __name__.find(' ') >= 0:
__doc__ = __name__
__name__ = None
@@ -120,6 +120,9 @@ class Element(object):
getDirectTaggedValueTags = getTaggedValueTags
+SpecificationBasePy = object # filled by _use_c_impl.
+
+
@_use_c_impl
class SpecificationBase(object):
# This object is the base of the inheritance hierarchy for ClassProvides:
@@ -162,27 +165,125 @@ class SpecificationBase(object):
def isOrExtends(self, interface):
"""Is the interface the same as or extend the given interface
"""
- return interface in self._implied
+ return interface in self._implied # pylint:disable=no-member
__call__ = isOrExtends
+class NameAndModuleComparisonMixin(object):
+ # Internal use. Implement the basic sorting operators (but not (in)equality
+ # or hashing). Subclasses must provide ``__name__`` and ``__module__``
+ # attributes. Subclasses will be mutually comparable; but because equality
+ # and hashing semantics are missing from this class, take care in how
+ # you define those two attributes: If you stick with the default equality
+ # and hashing (identity based) you should make sure that all possible ``__name__``
+ # and ``__module__`` pairs are unique ACROSS ALL SUBCLASSES. (Actually, pretty
+ # much the same thing goes if you define equality and hashing to be based on
+ # those two attributes: they must still be consistent ACROSS ALL SUBCLASSES.)
+
+ # pylint:disable=assigning-non-slot
+ __slots__ = ()
+
+ def _compare(self, other):
+ """
+ Compare *self* to *other* based on ``__name__`` and ``__module__``.
+
+ Return 0 if they are equal, return 1 if *self* is
+ greater than *other*, and return -1 if *self* is less than
+ *other*.
+
+ If *other* does not have ``__name__`` or ``__module__``, then
+ return ``NotImplemented``.
+
+ .. caution::
+ This allows comparison to things well outside the type hierarchy,
+ perhaps not symmetrically.
+
+ For example, ``class Foo(object)`` and ``class Foo(Interface)``
+ in the same file would compare equal, depending on the order of
+ operands. Writing code like this by hand would be unusual, but it could
+ happen with dynamic creation of types and interfaces.
+
+ None is treated as a pseudo interface that implies the loosest
+ contact possible, no contract. For that reason, all interfaces
+ sort before None.
+ """
+ if other is self:
+ return 0
+
+ if other is None:
+ return -1
+
+ n1 = (self.__name__, self.__module__)
+ try:
+ n2 = (other.__name__, other.__module__)
+ except AttributeError:
+ return NotImplemented
+
+ # This spelling works under Python3, which doesn't have cmp().
+ return (n1 > n2) - (n1 < n2)
+
+ def __lt__(self, other):
+ c = self._compare(other)
+ if c is NotImplemented:
+ return c
+ return c < 0
+
+ def __le__(self, other):
+ c = self._compare(other)
+ if c is NotImplemented:
+ return c
+ return c <= 0
+
+ def __gt__(self, other):
+ c = self._compare(other)
+ if c is NotImplemented:
+ return c
+ return c > 0
+
+ def __ge__(self, other):
+ c = self._compare(other)
+ if c is NotImplemented:
+ return c
+ return c >= 0
+
+
@_use_c_impl
-class InterfaceBase(object):
+class InterfaceBase(NameAndModuleComparisonMixin, SpecificationBasePy):
"""Base class that wants to be replaced with a C base :)
"""
- __slots__ = ()
+ __slots__ = (
+ '__name__',
+ '__ibmodule__',
+ '_v_cached_hash',
+ )
+
+ def __init__(self, name=None, module=None):
+ self.__name__ = name
+ self.__ibmodule__ = module
def _call_conform(self, conform):
raise NotImplementedError
+ @property
+ def __module_property__(self):
+ # This is for _InterfaceMetaClass
+ return self.__ibmodule__
+
def __call__(self, obj, alternate=_marker):
"""Adapt an object to the interface
"""
try:
conform = getattr(obj, '__conform__', None)
- except:
+ # XXX: Ideally, we don't want to catch BaseException. Things
+ # like MemoryError, KeyboardInterrupt, and other BaseExceptions should
+ # get through. Here, and everywhere we have bare ``except:`` clauses.
+ # The bare ``except:`` clauses exist for compatibility with the C code in
+ # ``_zope_interface_coptimizations.c`` (``ib_call()`` in this case). Both
+ # implementations would need to change. But beware of things like proxies and
+ # Acquisition wrappers. See https://github.com/zopefoundation/zope.interface/issues/163
+ except: # pylint:disable=bare-except
conform = None
if conform is not None:
adapter = self._call_conform(conform)
@@ -193,13 +294,12 @@ class InterfaceBase(object):
if adapter is not None:
return adapter
- elif alternate is not _marker:
+ if alternate is not _marker:
return alternate
- else:
- raise TypeError("Could not adapt", obj, self)
+ raise TypeError("Could not adapt", obj, self)
def __adapt__(self, obj):
- """Adapt an object to the reciever
+ """Adapt an object to the receiver
"""
if self.providedBy(obj):
return obj
@@ -209,6 +309,30 @@ class InterfaceBase(object):
if adapter is not None:
return adapter
+ return None
+
+ def __hash__(self):
+ # pylint:disable=assigning-non-slot,attribute-defined-outside-init
+ try:
+ return self._v_cached_hash
+ except AttributeError:
+ self._v_cached_hash = hash((self.__name__, self.__module__))
+ return self._v_cached_hash
+
+ def __eq__(self, other):
+ c = self._compare(other)
+ if c is NotImplemented:
+ return c
+ return c == 0
+
+ def __ne__(self, other):
+ if other is self:
+ return False
+
+ c = self._compare(other)
+ if c is NotImplemented:
+ return c
+ return c != 0
adapter_hooks = _use_c_impl([], 'adapter_hooks')
@@ -322,29 +446,29 @@ class Specification(SpecificationBase):
#
# So we force the issue by mutating the resolution order.
- # TODO: Caching. Perhaps make ro.C3 able to re-use the computed ``__sro__``
- # instead of re-doing it for the entire tree.
- base_count = len(self._bases)
+ # Note that we let C3 use pre-computed __sro__ for our bases.
+ # This requires that by the time this method is invoked, our bases
+ # have settled their SROs. Thus, ``changed()`` must first
+ # update itself before telling its descendents of changes.
+ sro = calculate_ro(self, base_mros={
+ b: b.__sro__
+ for b in self.__bases__
+ })
+ root = self._ROOT
+ if root is not None and sro and sro[-1] is not root:
+ # In one dataset of 1823 Interface objects, 1117 ClassProvides objects,
+ # sro[-1] was root 4496 times, and only not root 118 times. So it's
+ # probably worth checking.
- if base_count == 1:
- # Fast path: One base makes it trivial to calculate
- # the MRO.
- sro = [self]
- sro.extend(self.__bases__[0].__sro__)
- else:
- sro = ro(self)
- if self._ROOT is not None:
# Once we don't have to deal with old-style classes,
# we can add a check and only do this if base_count > 1,
# if we tweak the bootstrapping for ``<implementedBy object>``
- root = self._ROOT
sro = [
x
for x in sro
if x is not root
]
sro.append(root)
- assert sro[-1] is root, sro
return sro
@@ -421,16 +545,137 @@ class Specification(SpecificationBase):
return default if attr is None else attr
-class InterfaceClass(Element, InterfaceBase, Specification):
- """Prototype (scarecrow) Interfaces Implementation."""
+class _InterfaceMetaClass(type):
+ # Handling ``__module__`` on ``InterfaceClass`` is tricky. We need
+ # to be able to read it on a type and get the expected string. We
+ # also need to be able to set it on an instance and get the value
+ # we set. So far so good. But what gets tricky is that we'd like
+ # to store the value in the C structure (``InterfaceBase.__ibmodule__``) for
+ # direct access during equality, sorting, and hashing. "No
+ # problem, you think, I'll just use a property" (well, the C
+ # equivalents, ``PyMemberDef`` or ``PyGetSetDef``).
+ #
+ # Except there is a problem. When a subclass is created, the
+ # metaclass (``type``) always automatically puts the expected
+ # string in the class's dictionary under ``__module__``, thus
+ # overriding the property inherited from the superclass. Writing
+ # ``Subclass.__module__`` still works, but
+ # ``Subclass().__module__`` fails.
+ #
+ # There are multiple ways to work around this:
+ #
+ # (1) Define ``InterfaceBase.__getattribute__`` to watch for
+ # ``__module__`` and return the C storage.
+ #
+ # This works, but slows down *all* attribute access (except,
+ # ironically, to ``__module__``) by about 25% (40ns becomes 50ns)
+ # (when implemented in C). Since that includes methods like
+ # ``providedBy``, that's probably not acceptable.
+ #
+ # All the other methods involve modifying subclasses. This can be
+ # done either on the fly in some cases, as instances are
+ # constructed, or by using a metaclass. These next few can be done on the fly.
+ #
+ # (2) Make ``__module__`` a descriptor in each subclass dictionary.
+ # It can't be a straight up ``@property`` descriptor, though, because accessing
+ # it on the class returns a ``property`` object, not the desired string.
+ #
+ # (3) Implement a data descriptor (``__get__`` and ``__set__``)
+ # that is both a subclass of string, and also does the redirect of
+ # ``__module__`` to ``__ibmodule__`` and does the correct thing
+ # with the ``instance`` argument to ``__get__`` is None (returns
+ # the class's value.) (Why must it be a subclass of string? Because
+ # when it' s in the class's dict, it's defined on an *instance* of the
+ # metaclass; descriptors in an instance's dict aren't honored --- their
+ # ``__get__`` is never invoked --- so it must also *be* the value we want
+ # returned.)
+ #
+ # This works, preserves the ability to read and write
+ # ``__module__``, and eliminates any penalty accessing other
+ # attributes. But it slows down accessing ``__module__`` of
+ # instances by 200% (40ns to 124ns), requires editing class dicts on the fly
+ # (in InterfaceClass.__init__), thus slightly slowing down all interface creation,
+ # and is ugly.
+ #
+ # (4) As in the last step, but make it a non-data descriptor (no ``__set__``).
+ #
+ # If you then *also* store a copy of ``__ibmodule__`` in
+ # ``__module__`` in the instance's dict, reading works for both
+ # class and instance and is full speed for instances. But the cost
+ # is storage space, and you can't write to it anymore, not without
+ # things getting out of sync.
+ #
+ # (Actually, ``__module__`` was never meant to be writable. Doing
+ # so would break BTrees and normal dictionaries, as well as the
+ # repr, maybe more.)
+ #
+ # That leaves us with a metaclass. (Recall that a class is an
+ # instance of its metaclass, so properties/descriptors defined in
+ # the metaclass are used when accessing attributes on the
+ # instance/class. We'll use that to define ``__module__``.) Here
+ # we can have our cake and eat it too: no extra storage, and
+ # C-speed access to the underlying storage. The only substantial
+ # cost is that metaclasses tend to make people's heads hurt. (But
+ # still less than the descriptor-is-string, hopefully.)
+
+ __slots__ = ()
+
+ def __new__(cls, name, bases, attrs):
+ # Figure out what module defined the interface.
+ # This is copied from ``InterfaceClass.__init__``;
+ # reviewers aren't sure how AttributeError or KeyError
+ # could be raised.
+ __module__ = sys._getframe(1).f_globals['__name__']
+ # Get the C optimized __module__ accessor and give it
+ # to the new class.
+ moduledescr = InterfaceBase.__dict__['__module__']
+ if isinstance(moduledescr, str):
+ # We're working with the Python implementation,
+ # not the C version
+ moduledescr = InterfaceBase.__dict__['__module_property__']
+ attrs['__module__'] = moduledescr
+ kind = type.__new__(cls, name, bases, attrs)
+ kind.__module = __module__
+ return kind
+
+ @property
+ def __module__(cls):
+ return cls.__module
+
+ def __repr__(cls):
+ return "<class '%s.%s'>" % (
+ cls.__module,
+ cls.__name__,
+ )
+
+
+_InterfaceClassBase = _InterfaceMetaClass(
+ 'InterfaceClass',
+ (InterfaceBase, Element, Specification),
+ {'__slots__': ()}
+)
+
+
+class InterfaceClass(_InterfaceClassBase):
+ """
+ Prototype (scarecrow) Interfaces Implementation.
+
+ Note that it is not possible to change the ``__name__`` or ``__module__``
+ after an instance of this object has been constructed.
+ """
# We can't say this yet because we don't have enough
# infrastructure in place.
#
#implements(IInterface)
- def __init__(self, name, bases=(), attrs=None, __doc__=None,
+ def __init__(self, name, bases=(), attrs=None, __doc__=None, # pylint:disable=redefined-builtin
__module__=None):
+ # We don't call our metaclass parent directly
+ # pylint:disable=non-parent-init-called
+ # pylint:disable=super-init-not-called
+ if not all(isinstance(base, InterfaceClass) for base in bases):
+ raise TypeError('Expected base interfaces')
if attrs is None:
attrs = {}
@@ -448,7 +693,10 @@ class InterfaceClass(Element, InterfaceBase, Specification):
except (AttributeError, KeyError): # pragma: no cover
pass
- self.__module__ = __module__
+ InterfaceBase.__init__(self, name, __module__)
+ # These asserts assisted debugging the metaclass
+ # assert '__module__' not in self.__dict__
+ # assert self.__ibmodule__ is self.__module__ is __module__
d = attrs.get('__doc__')
if d is not None:
@@ -467,35 +715,38 @@ class InterfaceClass(Element, InterfaceBase, Specification):
for key, val in tagged_data.items():
self.setTaggedValue(key, val)
- for base in bases:
- if not isinstance(base, InterfaceClass):
- raise TypeError('Expected base interfaces')
-
Specification.__init__(self, bases)
+ self.__attrs = self.__compute_attrs(attrs)
+
+ self.__identifier__ = "%s.%s" % (__module__, name)
+ def __compute_attrs(self, attrs):
# Make sure that all recorded attributes (and methods) are of type
# `Attribute` and `Method`
- for name, attr in list(attrs.items()):
- if name in ('__locals__', '__qualname__', '__annotations__'):
+ def update_value(aname, aval):
+ if isinstance(aval, Attribute):
+ aval.interface = self
+ if not aval.__name__:
+ aval.__name__ = aname
+ elif isinstance(aval, FunctionType):
+ aval = fromFunction(aval, self, name=aname)
+ else:
+ raise InvalidInterface("Concrete attribute, " + aname)
+ return aval
+
+ return {
+ aname: update_value(aname, aval)
+ for aname, aval in attrs.items()
+ if aname not in (
# __locals__: Python 3 sometimes adds this.
+ '__locals__',
# __qualname__: PEP 3155 (Python 3.3+)
+ '__qualname__',
# __annotations__: PEP 3107 (Python 3.0+)
- del attrs[name]
- continue
- if isinstance(attr, Attribute):
- attr.interface = self
- if not attr.__name__:
- attr.__name__ = name
- elif isinstance(attr, FunctionType):
- attrs[name] = fromFunction(attr, self, name=name)
- elif attr is _decorator_non_return:
- del attrs[name]
- else:
- raise InvalidInterface("Concrete attribute, " + name)
-
- self.__attrs = attrs
-
- self.__identifier__ = "%s.%s" % (self.__module__, self.__name__)
+ '__annotations__'
+ )
+ and aval is not _decorator_non_return
+ }
def interfaces(self):
"""Return an iterator for the interfaces in the specification.
@@ -509,7 +760,7 @@ class InterfaceClass(Element, InterfaceBase, Specification):
"""Same interface or extends?"""
return self == other or other.extends(self)
- def names(self, all=False):
+ def names(self, all=False): # pylint:disable=redefined-builtin
"""Return the attribute names defined by the interface."""
if not all:
return self.__attrs.keys()
@@ -524,7 +775,7 @@ class InterfaceClass(Element, InterfaceBase, Specification):
def __iter__(self):
return iter(self.names(all=True))
- def namesAndDescriptions(self, all=False):
+ def namesAndDescriptions(self, all=False): # pylint:disable=redefined-builtin
"""Return attribute names and descriptions defined by interface."""
if not all:
return self.__attrs.items()
@@ -564,8 +815,7 @@ class InterfaceClass(Element, InterfaceBase, Specification):
except Invalid as e:
if errors is None:
raise
- else:
- errors.append(e)
+ errors.append(e)
for base in self.__bases__:
try:
base.validateInvariants(obj, errors)
@@ -607,11 +857,11 @@ class InterfaceClass(Element, InterfaceBase, Specification):
return self._v_repr
except AttributeError:
name = self.__name__
- m = self.__module__
+ m = self.__ibmodule__
if m:
name = '%s.%s' % (m, name)
r = "<%s %s>" % (self.__class__.__name__, name)
- self._v_repr = r
+ self._v_repr = r # pylint:disable=attribute-defined-outside-init
return r
def _call_conform(self, conform):
@@ -636,75 +886,13 @@ class InterfaceClass(Element, InterfaceBase, Specification):
def __reduce__(self):
return self.__name__
- 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__.
- # I don't want to override __eq__ because I want the default
- # __eq__, which is really fast.
- """Make interfaces sortable
-
- TODO: It would ne nice if:
-
- More specific interfaces should sort before less specific ones.
- Otherwise, sort on name and module.
-
- But this is too complicated, and we're going to punt on it
- for now.
-
- For now, sort on interface and module name.
-
- None is treated as a pseudo interface that implies the loosest
- contact possible, no contract. For that reason, all interfaces
- sort before None.
-
- """
- 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):
- try:
- return self._v_cached_hash
- except AttributeError:
- self._v_cached_hash = hash((self.__name__, self.__module__))
- return self._v_cached_hash
-
- def __eq__(self, other):
- c = self.__cmp(other)
- return c == 0
-
- def __ne__(self, other):
- c = self.__cmp(other)
- return c != 0
-
- 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
-
-
Interface = InterfaceClass("Interface", __module__='zope.interface')
# Interface is the only member of its own SRO.
Interface._calculate_sro = lambda: (Interface,)
Interface.changed(Interface)
assert Interface.__sro__ == (Interface,)
Specification._ROOT = Interface
+ro._ROOT = Interface
class Attribute(Element):
@@ -726,7 +914,8 @@ class Attribute(Element):
of = ''
if self.interface is not None:
of = self.interface.__module__ + '.' + self.interface.__name__ + '.'
- return of + self.__name__ + self._get_str_info()
+ # self.__name__ may be None during construction (e.g., debugging)
+ return of + (self.__name__ or '<unknown>') + self._get_str_info()
def __repr__(self):
return "<%s.%s object at 0x%x %s>" % (
@@ -868,6 +1057,7 @@ def _wire():
classImplements(Specification, ISpecification)
# We import this here to deal with module dependencies.
+# pylint:disable=wrong-import-position
from zope.interface.declarations import implementedBy
from zope.interface.declarations import providedBy
from zope.interface.exceptions import InvalidInterface
diff --git a/src/zope/interface/ro.py b/src/zope/interface/ro.py
index dbffb53..7cb4f55 100644
--- a/src/zope/interface/ro.py
+++ b/src/zope/interface/ro.py
@@ -189,11 +189,25 @@ class _ClassBoolFromEnv(object):
return val
+class _StaticMRO(object):
+ # A previously resolved MRO, supplied by the caller.
+ # Used in place of calculating it.
+
+ had_inconsistency = None # We don't know...
+
+ def __init__(self, C, mro):
+ self.leaf = C
+ self.__mro = tuple(mro)
+
+ def mro(self):
+ return list(self.__mro)
+
+
class C3(object):
# Holds the shared state during computation of an MRO.
@staticmethod
- def resolver(C, strict):
+ def resolver(C, strict, base_mros):
strict = strict if strict is not None else C3.STRICT_RO
factory = C3
if strict:
@@ -201,7 +215,13 @@ class C3(object):
elif C3.TRACK_BAD_IRO:
factory = _TrackingC3
- return factory(C, {})
+ memo = {}
+ base_mros = base_mros or {}
+ for base, mro in base_mros.items():
+ assert base in C.__bases__
+ memo[base] = _StaticMRO(base, mro)
+
+ return factory(C, memo)
__mro = None
__legacy_ro = None
@@ -229,6 +249,9 @@ class C3(object):
self.bases_had_inconsistency = any(base.had_inconsistency for base in base_resolvers)
+ if len(C.__bases__) == 1:
+ self.__mro = [C] + memo[C.__bases__[0]].mro()
+
@property
def had_inconsistency(self):
return self.direct_inconsistency or self.bases_had_inconsistency
@@ -520,10 +543,9 @@ class _ROComparison(object):
left_lines = [str(x) for x in legacy_report]
right_lines = [str(x) for x in c3_report]
- # We have the same number of non-empty lines as we do items
- # in the resolution order.
- assert len(list(filter(None, left_lines))) == len(self.c3_ro)
- assert len(list(filter(None, right_lines))) == len(self.c3_ro)
+ # We have the same number of lines in the report; this is not
+ # necessarily the same as the number of items in either RO.
+ assert len(left_lines) == len(right_lines)
padding = ' ' * 2
max_left = max(len(x) for x in left_lines)
@@ -547,20 +569,16 @@ class _ROComparison(object):
return '\n'.join(lines)
-def ro(C, strict=None, log_changed_ro=None, use_legacy_ro=None):
+# Set to `Interface` once it is defined. This is used to
+# avoid logging false positives about changed ROs.
+_ROOT = None
+
+def ro(C, strict=None, base_mros=None, log_changed_ro=None, use_legacy_ro=None):
"""
ro(C) -> list
Compute the precedence list (mro) according to C3.
- As an implementation note, this always calculates the full MRO by
- examining all the bases recursively. If there are special cases
- that can reuse pre-calculated partial MROs, such as a
- ``__bases__`` of length one, the caller is responsible for
- optimizing that. (This is because this function doesn't know how
- to get the complete MRO of a base; it only knows how to get their
- ``__bases__``.)
-
:return: A fresh `list` object.
.. versionchanged:: 5.0.0
@@ -568,7 +586,9 @@ def ro(C, strict=None, log_changed_ro=None, use_legacy_ro=None):
keyword arguments. These are provisional and likely to be
removed in the future. They are most useful for testing.
"""
- resolver = C3.resolver(C, strict)
+ # The ``base_mros`` argument is for internal optimization and
+ # not documented.
+ resolver = C3.resolver(C, strict, base_mros)
mro = resolver.mro()
log_changed = log_changed_ro if log_changed_ro is not None else resolver.LOG_CHANGED_IRO
@@ -578,7 +598,16 @@ def ro(C, strict=None, log_changed_ro=None, use_legacy_ro=None):
legacy_ro = resolver.legacy_ro
assert isinstance(legacy_ro, list)
assert isinstance(mro, list)
- if legacy_ro != mro:
+ changed = legacy_ro != mro
+ if changed:
+ # Did only Interface move? The fix for issue #8 made that
+ # somewhat common. It's almost certainly not a problem, though,
+ # so allow ignoring it.
+ legacy_without_root = [x for x in legacy_ro if x is not _ROOT]
+ mro_without_root = [x for x in mro if x is not _ROOT]
+ changed = legacy_without_root != mro_without_root
+
+ if changed:
comparison = _ROComparison(resolver, mro, legacy_ro)
_logger().warning(
"Object %r has different legacy and C3 MROs:\n%s",
@@ -602,4 +631,4 @@ def is_consistent(C):
Check if the resolution order for *C*, as computed by :func:`ro`, is consistent
according to C3.
"""
- return not C3.resolver(C, False).had_inconsistency
+ return not C3.resolver(C, False, None).had_inconsistency
diff --git a/src/zope/interface/tests/__init__.py b/src/zope/interface/tests/__init__.py
index c37dffc..8b9ef25 100644
--- a/src/zope/interface/tests/__init__.py
+++ b/src/zope/interface/tests/__init__.py
@@ -31,3 +31,25 @@ class OptimizationTestMixin(object):
self.assertIsNot(used, fallback)
else:
self.assertIs(used, fallback)
+
+# Be sure cleanup functionality is available; classes that use the adapter hook
+# need to be sure to subclass ``CleanUp``.
+#
+# If zope.component is installed and imported when we run our tests
+# (import chain:
+# zope.testrunner->zope.security->zope.location->zope.component.api)
+# it adds an adapter hook that uses its global site manager. That can cause
+# leakage from one test to another unless its cleanup hooks are run. The symptoms can
+# be odd, especially if one test used C objects and the next used the Python
+# implementation. (For example, you can get strange TypeErrors or find inexplicable
+# comparisons being done.)
+try:
+ from zope.testing import cleanup
+except ImportError:
+ class CleanUp(object):
+ def cleanUp(self):
+ pass
+
+ setUp = tearDown = cleanUp
+else:
+ CleanUp = cleanup.CleanUp
diff --git a/src/zope/interface/tests/test_declarations.py b/src/zope/interface/tests/test_declarations.py
index 0cdb326..b0875c4 100644
--- a/src/zope/interface/tests/test_declarations.py
+++ b/src/zope/interface/tests/test_declarations.py
@@ -17,6 +17,7 @@ import unittest
from zope.interface._compat import _skip_under_py3k
from zope.interface.tests import OptimizationTestMixin
+from zope.interface.tests.test_interface import NameAndModuleComparisonTestsMixin
class _Py3ClassAdvice(object):
@@ -296,7 +297,8 @@ class TestImmutableDeclaration(unittest.TestCase):
self.assertIsNone(self._getEmpty().get('name'))
self.assertEqual(self._getEmpty().get('name', 42), 42)
-class TestImplements(unittest.TestCase):
+class TestImplements(NameAndModuleComparisonTestsMixin,
+ unittest.TestCase):
def _getTargetClass(self):
from zope.interface.declarations import Implements
@@ -305,6 +307,13 @@ class TestImplements(unittest.TestCase):
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
+ def _makeOneToCompare(self):
+ from zope.interface.declarations import implementedBy
+ class A(object):
+ pass
+
+ return implementedBy(A)
+
def test_ctor_no_bases(self):
impl = self._makeOne()
self.assertEqual(impl.inherit, None)
@@ -381,6 +390,27 @@ class TestImplements(unittest.TestCase):
self.assertTrue(proxy != implementedByB)
self.assertTrue(implementedByB != proxy)
+ def test_changed_deletes_super_cache(self):
+ impl = self._makeOne()
+ self.assertIsNone(impl._super_cache)
+ self.assertNotIn('_super_cache', impl.__dict__)
+
+ impl._super_cache = 42
+ self.assertIn('_super_cache', impl.__dict__)
+
+ impl.changed(None)
+ self.assertIsNone(impl._super_cache)
+ self.assertNotIn('_super_cache', impl.__dict__)
+
+ def test_changed_does_not_add_super_cache(self):
+ impl = self._makeOne()
+ self.assertIsNone(impl._super_cache)
+ self.assertNotIn('_super_cache', impl.__dict__)
+
+ impl.changed(None)
+ self.assertIsNone(impl._super_cache)
+ self.assertNotIn('_super_cache', impl.__dict__)
+
class Test_implementedByFallback(unittest.TestCase):
diff --git a/src/zope/interface/tests/test_interface.py b/src/zope/interface/tests/test_interface.py
index da7eb8c..7b7b7e8 100644
--- a/src/zope/interface/tests/test_interface.py
+++ b/src/zope/interface/tests/test_interface.py
@@ -13,11 +13,19 @@
##############################################################################
"""Test Interface implementation
"""
-# pylint:disable=protected-access
+# Things we let slide because it's a test
+# pylint:disable=protected-access,blacklisted-name,attribute-defined-outside-init
+# pylint:disable=too-many-public-methods,too-many-lines,abstract-method
+# pylint:disable=redefined-builtin,signature-differs,arguments-differ
+# Things you get inheriting from Interface
+# pylint:disable=inherit-non-class,no-self-argument,no-method-argument
+# Things you get using methods of an Interface 'subclass'
+# pylint:disable=no-value-for-parameter
import unittest
from zope.interface._compat import _skip_under_py3k
from zope.interface.tests import OptimizationTestMixin
+from zope.interface.tests import CleanUp
_marker = object()
@@ -99,7 +107,7 @@ class ElementTests(unittest.TestCase):
from zope.interface.interface import Element
return Element
- def _makeOne(self, name=None):
+ def _makeOne(self, name=None):
if name is None:
name = self.DEFAULT_NAME
return self._getTargetClass()(name)
@@ -166,7 +174,7 @@ class ElementTests(unittest.TestCase):
class GenericSpecificationBaseTests(unittest.TestCase):
# Tests that work with both implementations
def _getFallbackClass(self):
- from zope.interface.interface import SpecificationBasePy
+ from zope.interface.interface import SpecificationBasePy # pylint:disable=no-name-in-module
return SpecificationBasePy
_getTargetClass = _getFallbackClass
@@ -247,22 +255,129 @@ class SpecificationBasePyTests(GenericSpecificationBaseTests):
self.assertTrue(sb.providedBy(object()))
-class GenericInterfaceBaseTests(unittest.TestCase):
+class NameAndModuleComparisonTestsMixin(CleanUp):
+
+ def _makeOneToCompare(self):
+ return self._makeOne('a', 'b')
+
+ def __check_NotImplemented_comparison(self, name):
+ # Without the correct attributes of __name__ and __module__,
+ # comparison switches to the reverse direction.
+
+ import operator
+ ib = self._makeOneToCompare()
+ op = getattr(operator, name)
+ meth = getattr(ib, '__%s__' % name)
+
+ # If either the __name__ or __module__ attribute
+ # is missing from the other object, then we return
+ # NotImplemented.
+ class RaisesErrorOnMissing(object):
+ Exc = AttributeError
+ def __getattribute__(self, name):
+ try:
+ return object.__getattribute__(self, name)
+ except AttributeError:
+ exc = RaisesErrorOnMissing.Exc
+ raise exc(name)
+
+ class RaisesErrorOnModule(RaisesErrorOnMissing):
+ def __init__(self):
+ self.__name__ = 'foo'
+ @property
+ def __module__(self):
+ raise AttributeError
+
+ class RaisesErrorOnName(RaisesErrorOnMissing):
+ def __init__(self):
+ self.__module__ = 'foo'
+
+ self.assertEqual(RaisesErrorOnModule().__name__, 'foo')
+ self.assertEqual(RaisesErrorOnName().__module__, 'foo')
+ with self.assertRaises(AttributeError):
+ getattr(RaisesErrorOnModule(), '__module__')
+ with self.assertRaises(AttributeError):
+ getattr(RaisesErrorOnName(), '__name__')
+
+ for cls in RaisesErrorOnModule, RaisesErrorOnName:
+ self.assertIs(meth(cls()), NotImplemented)
+
+ # If the other object has a comparison function, returning
+ # NotImplemented means Python calls it.
+
+ class AllowsAnyComparison(RaisesErrorOnMissing):
+ def __eq__(self, other):
+ return True
+ __lt__ = __eq__
+ __le__ = __eq__
+ __gt__ = __eq__
+ __ge__ = __eq__
+ __ne__ = __eq__
+
+ self.assertTrue(op(ib, AllowsAnyComparison()))
+ self.assertIs(meth(AllowsAnyComparison()), NotImplemented)
+
+ # If it doesn't have the comparison, Python raises a TypeError.
+ class AllowsNoComparison(object):
+ __eq__ = None
+ __lt__ = __eq__
+ __le__ = __eq__
+ __gt__ = __eq__
+ __ge__ = __eq__
+ __ne__ = __eq__
+
+ self.assertIs(meth(AllowsNoComparison()), NotImplemented)
+ with self.assertRaises(TypeError):
+ op(ib, AllowsNoComparison())
+
+ # Errors besides AttributeError are passed
+ class MyException(Exception):
+ pass
+
+ RaisesErrorOnMissing.Exc = MyException
+
+ with self.assertRaises(MyException):
+ getattr(RaisesErrorOnModule(), '__module__')
+ with self.assertRaises(MyException):
+ getattr(RaisesErrorOnName(), '__name__')
+
+ for cls in RaisesErrorOnModule, RaisesErrorOnName:
+ with self.assertRaises(MyException):
+ op(ib, cls())
+ with self.assertRaises(MyException):
+ meth(cls())
+
+ def test__lt__NotImplemented(self):
+ self.__check_NotImplemented_comparison('lt')
+
+ def test__le__NotImplemented(self):
+ self.__check_NotImplemented_comparison('le')
+
+ def test__gt__NotImplemented(self):
+ self.__check_NotImplemented_comparison('gt')
+
+ def test__ge__NotImplemented(self):
+ self.__check_NotImplemented_comparison('ge')
+
+
+class InterfaceBaseTestsMixin(NameAndModuleComparisonTestsMixin):
# Tests for both C and Python implementation
+ def _getTargetClass(self):
+ raise NotImplementedError
+
def _getFallbackClass(self):
+ # pylint:disable=no-name-in-module
from zope.interface.interface import InterfaceBasePy
return InterfaceBasePy
- _getTargetClass = _getFallbackClass
-
- def _makeOne(self, object_should_provide):
+ def _makeOne(self, object_should_provide=False, name=None, module=None):
class IB(self._getTargetClass()):
def _call_conform(self, conform):
return conform(self)
def providedBy(self, obj):
return object_should_provide
- return IB()
+ return IB(name, module)
def test___call___w___conform___returning_value(self):
ib = self._makeOne(False)
@@ -270,10 +385,11 @@ class GenericInterfaceBaseTests(unittest.TestCase):
class _Adapted(object):
def __conform__(self, iface):
return conformed
- self.assertTrue(ib(_Adapted()) is conformed)
+ self.assertIs(ib(_Adapted()), conformed)
def test___call___wo___conform___ob_no_provides_w_alternate(self):
ib = self._makeOne(False)
+ __traceback_info__ = ib, self._getTargetClass()
adapted = object()
alternate = object()
self.assertIs(ib(adapted, alternate), alternate)
@@ -284,18 +400,20 @@ class GenericInterfaceBaseTests(unittest.TestCase):
self.assertRaises(TypeError, ib, adapted)
-
-class InterfaceBaseTests(GenericInterfaceBaseTests,
- OptimizationTestMixin):
+class InterfaceBaseTests(InterfaceBaseTestsMixin,
+ OptimizationTestMixin,
+ unittest.TestCase):
# Tests that work with the C implementation
def _getTargetClass(self):
from zope.interface.interface import InterfaceBase
return InterfaceBase
-class InterfaceBasePyTests(GenericInterfaceBaseTests):
+class InterfaceBasePyTests(InterfaceBaseTestsMixin, unittest.TestCase):
# Tests that only work with the Python implementation
+ _getTargetClass = InterfaceBaseTestsMixin._getFallbackClass
+
def test___call___w___conform___miss_ob_provides(self):
ib = self._makeOne(True)
class _Adapted(object):
@@ -316,7 +434,6 @@ class InterfaceBasePyTests(GenericInterfaceBaseTests):
_missed = []
def _hook_miss(iface, obj):
_missed.append((iface, obj))
- return None
def _hook_hit(iface, obj):
return obj
with _Monkey(interface, adapter_hooks=[_hook_miss, _hook_hit]):
@@ -524,7 +641,7 @@ class InterfaceClassTests(unittest.TestCase):
self.assertEqual(inst.__name__, 'ITesting')
self.assertEqual(inst.__doc__, '')
self.assertEqual(inst.__bases__, ())
- self.assertEqual(inst.names(), ATTRS.keys())
+ self.assertEqual(list(inst.names()), [])
def test_ctor_attrs_w___annotations__(self):
ATTRS = {'__annotations__': {}}
@@ -533,7 +650,7 @@ class InterfaceClassTests(unittest.TestCase):
self.assertEqual(inst.__name__, 'ITesting')
self.assertEqual(inst.__doc__, '')
self.assertEqual(inst.__bases__, ())
- self.assertEqual(inst.names(), ATTRS.keys())
+ self.assertEqual(list(inst.names()), [])
def test_ctor_attrs_w__decorator_non_return(self):
from zope.interface.interface import _decorator_non_return
@@ -668,8 +785,8 @@ class InterfaceClassTests(unittest.TestCase):
base = self._makeOne('IBase', attrs=BASE_ATTRS)
derived = self._makeOne('IDerived', bases=(base,), attrs=DERIVED_ATTRS)
self.assertEqual(sorted(derived.namesAndDescriptions(all=False)),
- [('baz', DERIVED_ATTRS['baz']),
- ])
+ [('baz', DERIVED_ATTRS['baz']),
+ ])
def test_namesAndDescriptions_w_all_True_no_bases(self):
from zope.interface.interface import Attribute
@@ -681,9 +798,9 @@ class InterfaceClassTests(unittest.TestCase):
}
one = self._makeOne(attrs=ATTRS)
self.assertEqual(sorted(one.namesAndDescriptions(all=False)),
- [('bar', ATTRS['bar']),
- ('foo', ATTRS['foo']),
- ])
+ [('bar', ATTRS['bar']),
+ ('foo', ATTRS['foo']),
+ ])
def test_namesAndDescriptions_w_all_True_simple(self):
from zope.interface.interface import Attribute
@@ -698,10 +815,10 @@ class InterfaceClassTests(unittest.TestCase):
base = self._makeOne('IBase', attrs=BASE_ATTRS)
derived = self._makeOne('IDerived', bases=(base,), attrs=DERIVED_ATTRS)
self.assertEqual(sorted(derived.namesAndDescriptions(all=True)),
- [('bar', BASE_ATTRS['bar']),
- ('baz', DERIVED_ATTRS['baz']),
- ('foo', BASE_ATTRS['foo']),
- ])
+ [('bar', BASE_ATTRS['bar']),
+ ('baz', DERIVED_ATTRS['baz']),
+ ('foo', BASE_ATTRS['foo']),
+ ])
def test_namesAndDescriptions_w_all_True_bases_w_same_names(self):
from zope.interface.interface import Attribute
@@ -719,10 +836,10 @@ class InterfaceClassTests(unittest.TestCase):
base = self._makeOne('IBase', attrs=BASE_ATTRS)
derived = self._makeOne('IDerived', bases=(base,), attrs=DERIVED_ATTRS)
self.assertEqual(sorted(derived.namesAndDescriptions(all=True)),
- [('bar', BASE_ATTRS['bar']),
- ('baz', DERIVED_ATTRS['baz']),
- ('foo', DERIVED_ATTRS['foo']),
- ])
+ [('bar', BASE_ATTRS['bar']),
+ ('baz', DERIVED_ATTRS['baz']),
+ ('foo', DERIVED_ATTRS['foo']),
+ ])
def test_getDescriptionFor_miss(self):
one = self._makeOne()
@@ -903,13 +1020,14 @@ class InterfaceClassTests(unittest.TestCase):
def test___hash___missing_required_attrs(self):
class Derived(self._getTargetClass()):
- def __init__(self):
+ def __init__(self): # pylint:disable=super-init-not-called
pass # Don't call base class.
derived = Derived()
with self.assertRaises(AttributeError):
hash(derived)
def test_comparison_with_None(self):
+ # pylint:disable=singleton-comparison,misplaced-comparison-constant
iface = self._makeOne()
self.assertTrue(iface < None)
self.assertTrue(iface <= None)
@@ -926,6 +1044,7 @@ class InterfaceClassTests(unittest.TestCase):
self.assertTrue(None > iface)
def test_comparison_with_same_instance(self):
+ # pylint:disable=comparison-with-itself
iface = self._makeOne()
self.assertFalse(iface < iface)
@@ -1002,6 +1121,13 @@ class InterfaceClassTests(unittest.TestCase):
ISpam.__class__ = MyInterfaceClass
self.assertEqual(ISpam(1), (1,))
+ def test__module__is_readonly(self):
+ inst = self._makeOne()
+ with self.assertRaises((AttributeError, TypeError)):
+ # CPython 2.7 raises TypeError. Everything else
+ # raises AttributeError.
+ inst.__module__ = 'different.module'
+
class InterfaceTests(unittest.TestCase):
@@ -1049,6 +1175,7 @@ class InterfaceTests(unittest.TestCase):
self.assertTrue(ICurrent.implementedBy(Current))
self.assertFalse(IOther.implementedBy(Current))
+ self.assertEqual(ICurrent, ICurrent)
self.assertTrue(ICurrent in implementedBy(Current))
self.assertFalse(IOther in implementedBy(Current))
self.assertTrue(ICurrent in providedBy(current))
@@ -1762,7 +1889,7 @@ class InterfaceTests(unittest.TestCase):
has_invariant.foo = 2
has_invariant.bar = 1
self._errorsEqual(has_invariant, 1,
- ['Please, Boo MUST be greater than Foo!'], IInvariant)
+ ['Please, Boo MUST be greater than Foo!'], IInvariant)
# and if we set foo to a positive number and boo to 0, we'll
# get both errors!
has_invariant.foo = 1
@@ -1781,27 +1908,25 @@ class InterfaceTests(unittest.TestCase):
def test___doc___element(self):
from zope.interface import Interface
from zope.interface import Attribute
- class I(Interface):
+ class IDocstring(Interface):
"xxx"
- self.assertEqual(I.__doc__, "xxx")
- self.assertEqual(list(I), [])
+ self.assertEqual(IDocstring.__doc__, "xxx")
+ self.assertEqual(list(IDocstring), [])
- class I(Interface):
+ class IDocstringAndAttribute(Interface):
"xxx"
__doc__ = Attribute('the doc')
- self.assertEqual(I.__doc__, "")
- self.assertEqual(list(I), ['__doc__'])
+ self.assertEqual(IDocstringAndAttribute.__doc__, "")
+ self.assertEqual(list(IDocstringAndAttribute), ['__doc__'])
@_skip_under_py3k
def testIssue228(self):
# Test for http://collector.zope.org/Zope3-dev/228
# Old style classes don't have a '__class__' attribute
# No old style classes in Python 3, so the test becomes moot.
- import sys
-
from zope.interface import Interface
class I(Interface):
@@ -1834,10 +1959,10 @@ class InterfaceTests(unittest.TestCase):
def __init__(self, min, max):
self.min, self.max = min, max
- IRange.validateInvariants(Range(1,2))
- IRange.validateInvariants(Range(1,1))
+ IRange.validateInvariants(Range(1, 2))
+ IRange.validateInvariants(Range(1, 1))
try:
- IRange.validateInvariants(Range(2,1))
+ IRange.validateInvariants(Range(2, 1))
except Invalid as e:
self.assertEqual(str(e), 'max < min')
@@ -2008,7 +2133,6 @@ class InterfaceTests(unittest.TestCase):
def test___call___w_adapter_hook(self):
from zope.interface import Interface
from zope.interface.interface import adapter_hooks
- old_hooks = adapter_hooks[:]
def _miss(iface, obj):
pass
@@ -2214,7 +2338,7 @@ class Test_fromFunction(unittest.TestCase):
self.assertEqual(info['kwargs'], None)
def test_w_optional_self(self):
- # XXX This is a weird case, trying to cover the following code in
+ # This is a weird case, trying to cover the following code in
# FUT::
#
# nr = na-len(defaults)
@@ -2254,7 +2378,7 @@ class Test_fromFunction(unittest.TestCase):
self.assertEqual(info['kwargs'], 'kw')
def test_full_spectrum(self):
- def _func(foo, bar='baz', *args, **kw):
+ def _func(foo, bar='baz', *args, **kw): # pylint:disable=keyword-arg-before-vararg
"DOCSTRING"
method = self._callFUT(_func)
info = method.getSignatureInfo()
@@ -2289,7 +2413,7 @@ class Test_fromMethod(unittest.TestCase):
def test_full_spectrum(self):
class Foo(object):
- def bar(self, foo, bar='baz', *args, **kw):
+ def bar(self, foo, bar='baz', *args, **kw): # pylint:disable=keyword-arg-before-vararg
"DOCSTRING"
method = self._callFUT(Foo.bar)
info = method.getSignatureInfo()
@@ -2345,7 +2469,7 @@ class _Monkey(object):
# context-manager for replacing module names in the scope of a test.
def __init__(self, module, **kw):
self.module = module
- self.to_restore = dict([(key, getattr(module, key)) for key in kw])
+ self.to_restore = {key: getattr(module, key) for key in kw}
for key, value in kw.items():
setattr(module, key, value)
diff --git a/src/zope/interface/tests/test_registry.py b/src/zope/interface/tests/test_registry.py
index db91c52..2df4ae8 100644
--- a/src/zope/interface/tests/test_registry.py
+++ b/src/zope/interface/tests/test_registry.py
@@ -2343,9 +2343,10 @@ class UtilityRegistrationTests(unittest.TestCase):
def _makeOne(self, component=None, factory=None):
from zope.interface.declarations import InterfaceClass
- class IFoo(InterfaceClass):
+ class InterfaceClassSubclass(InterfaceClass):
pass
- ifoo = IFoo('IFoo')
+
+ ifoo = InterfaceClassSubclass('IFoo')
class _Registry(object):
def __repr__(self):
return '_REGISTRY'
diff --git a/src/zope/interface/tests/test_ro.py b/src/zope/interface/tests/test_ro.py
index 3a516b5..ce0a6f4 100644
--- a/src/zope/interface/tests/test_ro.py
+++ b/src/zope/interface/tests/test_ro.py
@@ -375,6 +375,27 @@ Object <InterfaceClass zope.interface.tests.test_ro.A> has different legacy and
self.assertEqual(iro, legacy_iro)
+class TestC3(unittest.TestCase):
+ def _makeOne(self, C, strict=False, base_mros=None):
+ from zope.interface.ro import C3
+ return C3.resolver(C, strict, base_mros)
+
+ def test_base_mros_given(self):
+ c3 = self._makeOne(type(self), base_mros={unittest.TestCase: unittest.TestCase.__mro__})
+ memo = c3.memo
+ self.assertIn(unittest.TestCase, memo)
+ # We used the StaticMRO class
+ self.assertIsNone(memo[unittest.TestCase].had_inconsistency)
+
+ def test_one_base_optimization(self):
+ c3 = self._makeOne(type(self))
+ # Even though we didn't call .mro() yet, the MRO has been
+ # computed.
+ self.assertIsNotNone(c3._C3__mro) # pylint:disable=no-member
+ c3._merge = None
+ self.assertEqual(c3.mro(), list(type(self).__mro__))
+
+
class Test_ROComparison(unittest.TestCase):
class MockC3(object):
diff --git a/src/zope/interface/tests/test_sorting.py b/src/zope/interface/tests/test_sorting.py
index 73613d0..0e33f47 100644
--- a/src/zope/interface/tests/test_sorting.py
+++ b/src/zope/interface/tests/test_sorting.py
@@ -45,3 +45,20 @@ class Test(unittest.TestCase):
l = [I1, m1_I1]
l.sort()
self.assertEqual(l, [m1_I1, I1])
+
+ def test_I1_I2(self):
+ self.assertLess(I1.__name__, I2.__name__)
+ self.assertEqual(I1.__module__, I2.__module__)
+ self.assertEqual(I1.__module__, __name__)
+ self.assertLess(I1, I2)
+
+ def _makeI1(self):
+ class I1(Interface):
+ pass
+ return I1
+
+ def test_nested(self):
+ nested_I1 = self._makeI1()
+ self.assertEqual(I1, nested_I1)
+ self.assertEqual(nested_I1, I1)
+ self.assertEqual(hash(I1), hash(nested_I1))
diff --git a/tox.ini b/tox.ini
index 2c60a50..be90fe3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -43,7 +43,7 @@ commands =
coverage report -i
coverage html -i
coverage xml -i
-depends = py27,py27-pure,py35,py35-pure,py36,py37,py38,py38-cext,pypy,pypy3
+depends = py27,py27-pure,py35,py35-pure,py36,py37,py38,py38-cext,pypy,pypy3,docs
parallel_show_output = true
[testenv:docs]
@@ -51,7 +51,7 @@ basepython =
python3
commands =
sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
- sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest
+ coverage run -p -m sphinx -b doctest -d docs/_build/doctrees docs docs/_build/doctest
deps =
Sphinx
repoze.sphinx.autointerface