summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2020-03-11 13:41:59 -0500
committerJason Madden <jamadden@gmail.com>2020-03-18 12:27:20 -0500
commitd9f06470f9c45d0710c00e680806a3577b5617f1 (patch)
tree759b91ac0b416649357870fce864397a07fd6678
parent413e716f9fb48338829435c4f243133589eb9fe6 (diff)
downloadzope-interface-d9f06470f9c45d0710c00e680806a3577b5617f1.tar.gz
Use a descriptor for __module__
This makes the rest of the attribute access fast again, but slows down __module__. +-------------------------------------------+------------+-------------------------------+ | Benchmark | 38-master3 | 38-faster-descr | +===========================================+============+===============================+ | read __module__ | 41.1 ns | 123 ns: 2.99x slower (+199%) | +-------------------------------------------+------------+-------------------------------+ | read __name__ | 41.3 ns | 39.9 ns: 1.04x faster (-3%) | +-------------------------------------------+------------+-------------------------------+ | read __doc__ | 41.8 ns | 42.4 ns: 1.01x slower (+1%) | +-------------------------------------------+------------+-------------------------------+ | query adapter (no registrations) | 3.85 ms | 2.95 ms: 1.30x faster (-23%) | +-------------------------------------------+------------+-------------------------------+ | query adapter (all trivial registrations) | 4.59 ms | 3.67 ms: 1.25x faster (-20%) | +-------------------------------------------+------------+-------------------------------+ | contains (empty dict) | 136 ns | 54.8 ns: 2.48x faster (-60%) | +-------------------------------------------+------------+-------------------------------+ | contains (populated dict) | 137 ns | 55.7 ns: 2.46x faster (-59%) | +-------------------------------------------+------------+-------------------------------+ | contains (populated list) | 40.2 us | 2.86 us: 14.03x faster (-93%) | +-------------------------------------------+------------+-------------------------------+ Not significant (1): read providedBy
-rw-r--r--src/zope/interface/_compat.py1
-rw-r--r--src/zope/interface/_zope_interface_coptimizations.c36
-rw-r--r--src/zope/interface/interface.py89
-rw-r--r--src/zope/interface/tests/test_registry.py5
4 files changed, 83 insertions, 48 deletions
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 adc7351..5a4fc92 100644
--- a/src/zope/interface/_zope_interface_coptimizations.c
+++ b/src/zope/interface/_zope_interface_coptimizations.c
@@ -818,6 +818,9 @@ IB_dealloc(IB* 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}
};
@@ -936,32 +939,21 @@ cleanup:
}
-static PyObject*
-IB_getattro(IB* self, PyObject* name)
-{
- int cmpresult;
- cmpresult = PyObject_RichCompareBool(name, str__module__, Py_EQ);
- if (cmpresult == -1)
- return NULL;
-
- if (cmpresult) { // __module__
- if (self->__module__) {
- Py_INCREF(self->__module__);
- return self->__module__;
- }
- }
-
- // Wasn't __module__, *or* it was, but it was unset.
- return PyObject_GenericGetAttr(OBJECT(self), name);
-}
-
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__ = Py_None;
+ self->__module__ = module ? module : Py_None;
Py_INCREF(self->__module__);
- self->__name__ = Py_None;
+ self->__name__ = name ? name : Py_None;
Py_INCREF(self->__name__);
return 0;
}
@@ -985,7 +977,7 @@ static PyTypeObject InterfaceBaseType = {
/* tp_hash */ (hashfunc)IB_hash,
/* tp_call */ (ternaryfunc)ib_call,
/* tp_str */ (reprfunc)0,
- /* tp_getattro */ (getattrofunc)IB_getattro,
+ /* tp_getattro */ (getattrofunc)0,
/* tp_setattro */ (setattrofunc)0,
/* tp_as_buffer */ 0,
/* tp_flags */ Py_TPFLAGS_DEFAULT
diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py
index d17c8dc..aa66c22 100644
--- a/src/zope/interface/interface.py
+++ b/src/zope/interface/interface.py
@@ -20,6 +20,7 @@ from types import FunctionType
import weakref
from zope.interface._compat import _use_c_impl
+from zope.interface._compat import PYTHON3 as PY3
from zope.interface.exceptions import Invalid
from zope.interface.ro import ro as calculate_ro
from zope.interface import ro
@@ -238,6 +239,56 @@ class NameAndModuleComparisonMixin(object):
return c >= 0
+class _ModuleDescriptor(str):
+ # Descriptor for ``__module__``, used in InterfaceBase and subclasses.
+ #
+ # We store the module value in ``__ibmodule__`` and provide access
+ # to it under ``__module__`` through this descriptor. This is
+ # because we want to store ``__module__`` in the C structure (for
+ # speed of equality and sorting), but it's very hard to do
+ # that. Using PyMemberDef or PyGetSetDef (the C
+ # versions of properties) doesn't work without adding
+ # metaclasses: creating a new subclass puts a ``__module__``
+ # string in the class dict that overrides the descriptor that
+ # would access the C structure data.
+ #
+ # We must also preserve access to the *real* ``__module__`` of the
+ # class.
+ #
+ # Our solution is to watch for new subclasses and manually move
+ # this descriptor into them at creation time. We could use a
+ # metaclass, but this seems safer; using ``__getattribute__`` to
+ # alias the two imposed a 25% penalty on every attribute/method
+ # lookup, even when implemented in C.
+
+ # type.__repr__ accesses self.__dict__['__module__']
+ # and checks to see if it's a native string. If it's not,
+ # the repr just uses the __name__. So for things to work out nicely
+ # it's best for us to subclass str.
+ if PY3:
+ # Python 2 doesn't allow non-empty __slots__ for str
+ # subclasses.
+ __slots__ = ('_class_module',)
+
+ def __init__(self, class_module):
+ str.__init__(self)
+ self._class_module = class_module
+
+ def __get__(self, inst, kind):
+ if inst is None:
+ return self._class_module
+ return inst.__ibmodule__
+
+ def __set__(self, inst, val):
+ # Setting __module__ after construction is undefined. There are
+ # numerous things that cache based on it, either directly or indirectly.
+ # Nonetheless, it is allowed.
+ inst.__ibmodule__ = val
+
+ def __str__(self):
+ return self._class_module
+
+
@_use_c_impl
class InterfaceBase(NameAndModuleComparisonMixin, SpecificationBasePy):
"""Base class that wants to be replaced with a C base :)
@@ -246,36 +297,17 @@ class InterfaceBase(NameAndModuleComparisonMixin, SpecificationBasePy):
__slots__ = (
'__name__',
'__ibmodule__',
+ '_v_cached_hash',
)
def __init__(self, name=None, module=None):
- # pylint:disable=assigning-non-slot
self.__name__ = name
- # We store the module value in ``__ibmodule__`` and provide access
- # to it under ``__module__`` through ``__getattribute__``. This is
- # because we want to store __module__ in the C structure (for
- # speed of equality and sorting), but it's very hard to do
- # that any other way. Using PyMemberDef or PyGetSetDef (the C
- # versions of properties) doesn't work without adding
- # metaclasses: creating a new subclass puts a ``__module__``
- # string in the class dict that overrides the descriptor that
- # would access the C structure data.
- #
- # We could use a metaclass to override this behaviour, but it's probably
- # safer to use ``__getattribute__``.
- #
- # Setting ``__module__`` after construction is undefined.
- # There are numerous things that cache that value directly or
- # indirectly (and long have).
self.__ibmodule__ = module
def _call_conform(self, conform):
raise NotImplementedError
- def __getattribute__(self, name):
- if name == '__module__':
- return self.__ibmodule__
- return object.__getattribute__(self, name)
+ __module__ = _ModuleDescriptor(__name__)
def __call__(self, obj, alternate=_marker):
"""Adapt an object to the interface
@@ -559,12 +591,18 @@ class InterfaceClass(InterfaceBase, Element, Specification):
#
#implements(IInterface)
+ def __new__(cls, *args, **kwargs):
+ if not isinstance(
+ cls.__dict__.get('__module__'),
+ _ModuleDescriptor):
+ cls.__module__ = _ModuleDescriptor(cls.__dict__['__module__'])
+ return super(InterfaceClass, cls).__new__(cls)
+
def __init__(self, name, bases=(), attrs=None, __doc__=None, # pylint:disable=redefined-builtin
__module__=None):
if not all(isinstance(base, InterfaceClass) for base in bases):
raise TypeError('Expected base interfaces')
- InterfaceBase.__init__(self)
if attrs is None:
attrs = {}
@@ -581,8 +619,10 @@ class InterfaceClass(InterfaceBase, Element, Specification):
except (AttributeError, KeyError): # pragma: no cover
pass
- self.__ibmodule__ = __module__
+ InterfaceBase.__init__(self, name, __module__)
+ assert '__module__' not in self.__dict__
+ assert self.__module__ == __module__, (self.__module__, __module__, self.__ibmodule__)
d = attrs.get('__doc__')
if d is not None:
if not isinstance(d, Attribute):
@@ -799,7 +839,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>" % (
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'