diff --git a/CHANGES.rst b/CHANGES.rst index 7075008a..70f85e95 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,11 +2,48 @@ Changes ========= -5.0.3 (unreleased) -================== - -- Nothing changed yet. - +5.1.0 (unreleased) +================== + +- Remove all bare ``except:`` statements. Previously, when accessing + special attributes such as ``__provides__``, ``__providedBy__``, + ``__class__`` and ``__conform__``, this package wrapped such access + in a bare ``except:`` statement, meaning that many errors could pass + silently; typically this would result in a fallback path being taken + and sometimes (like with ``providedBy()``) the result would be + non-sensical. This is especially true when those attributes are + implemented with descriptors. Now, only ``AttributeError`` is + caught. This makes errors more obvious. + + Obviously, this means that some exceptions will be propagated + differently than before. In particular, ``RuntimeError`` raised by + Acquisition in the case of circular containment will now be + propagated. Previously, when adapting such a broken object, a + ``TypeError`` would be the common result, but now it will be a more + informative ``RuntimeError``. + + In addition, ZODB errors like ``POSKeyError`` could now be + propagated where previously they would ignored by this package. + + See `issue 200 `_. + +- Require that the second argument (*bases*) to ``InterfaceClass`` is + a tuple. This only matters when directly using ``InterfaceClass`` to + create new interfaces dynamically. Previously, an individual + interface was allowed, but did not work correctly. Now it is + consistent with ``type`` and requires a tuple. + +- Let interfaces define custom ``__adapt__`` methods. This implements + the other side of the :pep:`246` adaptation protocol: objects being + adapted could already implement ``__conform__`` if they know about + the interface, and now interfaces can implement ``__adapt__`` if + they know about particular objects. There is no performance penalty + for interfaces that do not supply custom ``__adapt__`` methods. + + This includes the ability to add new methods, or override existing + interface methods using the new ``@interfacemethod`` decorator. + + See `issue 3 `_. 5.0.2 (2020-03-30) ================== diff --git a/benchmarks/micro.py b/benchmarks/micro.py index a9527dba..dce40cee 100644 --- a/benchmarks/micro.py +++ b/benchmarks/micro.py @@ -37,6 +37,21 @@ class DeepestInheritance(object): pass classImplements(DeepestInheritance, deep_ifaces[-1]) + +class ImplementsNothing(object): + pass + + +class HasConformReturnNone(object): + def __conform__(self, iface): + return None + + +class HasConformReturnObject(object): + def __conform__(self, iface): + return self + + def make_implementer(iface): c = type('Implementer' + iface.__name__, (object,), {}) classImplements(c, iface) @@ -77,16 +92,17 @@ def bench_sort(loops, objs): return pyperf.perf_counter() - t0 def bench_query_adapter(loops, components, objs=providers): + components_queryAdapter = components.queryAdapter # One time through to prime the caches for iface in ifaces: for provider in providers: - components.queryAdapter(provider, iface) + components_queryAdapter(provider, iface) t0 = pyperf.perf_counter() for _ in range(loops): for iface in ifaces: for provider in objs: - components.queryAdapter(provider, iface) + components_queryAdapter(provider, iface) return pyperf.perf_counter() - t0 @@ -106,8 +122,106 @@ def bench_getattr(loops, name, get=getattr): get(Interface, name) # 10 return pyperf.perf_counter() - t0 + +def bench_iface_call_no_conform_no_alternate_not_provided(loops): + inst = ImplementsNothing() + t0 = pyperf.perf_counter() + for _ in range(loops): + for _ in range(INNER): + for iface in ifaces: + try: + iface(inst) + except TypeError: + pass + else: + raise TypeError("Should have failed") + return pyperf.perf_counter() - t0 + + +def bench_iface_call_no_conform_w_alternate_not_provided(loops): + inst = ImplementsNothing() + t0 = pyperf.perf_counter() + for _ in range(loops): + for _ in range(INNER): + for iface in ifaces: + iface(inst, 42) + return pyperf.perf_counter() - t0 + + +def bench_iface_call_w_conform_return_none_not_provided(loops): + inst = HasConformReturnNone() + t0 = pyperf.perf_counter() + for _ in range(loops): + for _ in range(INNER): + for iface in ifaces: + iface(inst, 42) + return pyperf.perf_counter() - t0 + + +def bench_iface_call_w_conform_return_non_none_not_provided(loops): + inst = HasConformReturnObject() + t0 = pyperf.perf_counter() + for _ in range(loops): + for _ in range(INNER): + for iface in ifaces: + iface(inst) + return pyperf.perf_counter() - t0 + +def _bench_iface_call_simple(loops, inst): + t0 = pyperf.perf_counter() + for _ in range(loops): + for _ in range(INNER): + for iface in ifaces: + iface(inst) + return pyperf.perf_counter() - t0 + + +def bench_iface_call_no_conform_provided_wide(loops): + return _bench_iface_call_simple(loops, WideInheritance()) + + +def bench_iface_call_no_conform_provided_deep(loops): + return _bench_iface_call_simple(loops, DeepestInheritance()) + + runner = pyperf.Runner() +runner.bench_time_func( + 'call interface (provides; deep)', + bench_iface_call_no_conform_provided_deep, + inner_loops=INNER * len(ifaces) +) + +runner.bench_time_func( + 'call interface (provides; wide)', + bench_iface_call_no_conform_provided_wide, + inner_loops=INNER * len(ifaces) +) + +runner.bench_time_func( + 'call interface (no alternate, no conform, not provided)', + bench_iface_call_no_conform_no_alternate_not_provided, + inner_loops=INNER * len(ifaces) +) + +runner.bench_time_func( + 'call interface (alternate, no conform, not provided)', + bench_iface_call_no_conform_w_alternate_not_provided, + inner_loops=INNER * len(ifaces) +) + +runner.bench_time_func( + 'call interface (no alternate, valid conform, not provided)', + bench_iface_call_w_conform_return_non_none_not_provided, + inner_loops=INNER * len(ifaces) +) + +runner.bench_time_func( + 'call interface (alternate, invalid conform, not provided)', + bench_iface_call_w_conform_return_none_not_provided, + inner_loops=INNER * len(ifaces) +) + runner.bench_time_func( 'read __module__', # stored in C, accessed through __getattribute__ bench_getattr, diff --git a/docs/README.rst b/docs/README.rst index 831f19a6..0364a816 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -299,6 +299,9 @@ Note that class decorators using the ``@implementer(IFoo)`` syntax are only supported in Python 2.6 and later. .. autofunction:: implementer + :noindex: + + .. XXX: Duplicate description. Declaring provided interfaces ----------------------------- @@ -416,6 +419,9 @@ We can find out what interfaces are directly provided by an object: [] .. autofunction:: provider + :noindex: + + .. XXX: Duplicate description. Inherited declarations ---------------------- @@ -472,6 +478,7 @@ be used for this purpose: [] .. autofunction:: classImplements + :noindex: We can use ``classImplementsOnly`` to exclude inherited interfaces: @@ -485,7 +492,9 @@ We can use ``classImplementsOnly`` to exclude inherited interfaces: [] .. autofunction:: classImplementsOnly + :noindex: + .. XXX: Duplicate description. Declaration Objects ------------------- @@ -709,6 +718,8 @@ that lists the specification and all of it's ancestors: Tagged Values ============= +.. autofunction:: taggedValue + Interfaces and attribute descriptions support an extension mechanism, borrowed from UML, called "tagged values" that lets us store extra data: @@ -771,9 +782,12 @@ versions of functions. >>> IExtendsIWithTaggedValues.getDirectTaggedValue('squish') 'SQUASH' + Invariants ========== +.. autofunction:: invariant + Interfaces can express conditions that must hold for objects that provide them. These conditions are expressed using one or more invariants. Invariants are callable objects that will be called with @@ -849,7 +863,8 @@ Adaptation Interfaces can be called to perform adaptation. -The semantics are based on those of the PEP 246 ``adapt`` function. +The semantics are based on those of the :pep:`246` ``adapt`` +function. If an object cannot be adapted, then a ``TypeError`` is raised: @@ -883,7 +898,13 @@ If an object already implements the interface, then it will be returned: >>> I(obj) is obj True -If an object implements ``__conform__``, then it will be used: +:pep:`246` outlines a requirement: + + When the object knows about the [interface], and either considers + itself compliant, or knows how to wrap itself suitably. + +This is handled with ``__conform__``. If an object implements +``__conform__``, then it will be used: .. doctest:: @@ -922,21 +943,27 @@ Adapter hooks (see ``__adapt__``) will also be used, if present: >>> class I(zope.interface.Interface): ... pass -Interfaces implement the PEP 246 ``__adapt__`` method. +Interfaces implement the :pep:`246` ``__adapt__`` method to satisfy +the requirement: -This method is normally not called directly. It is called by the PEP -246 adapt framework and by the interface ``__call__`` operator. + When the [interface] knows about the object, and either the object + already complies or the [interface] knows how to suitably wrap the + object. + +This method is normally not called directly. It is called by the +:pep:`246` adapt framework and by the interface ``__call__`` operator. The ``adapt`` method is responsible for adapting an object to the reciever. -The default version returns ``None``: +The default version returns ``None`` (because by default no interface +"knows how to suitably wrap the object"): .. doctest:: >>> I.__adapt__(0) -unless the object given provides the interface: +unless the object given provides the interface ("the object already complies"): .. doctest:: @@ -975,6 +1002,28 @@ Hooks can be uninstalled by removing them from the list: >>> I.__adapt__(0) +It is possible to replace or customize the ``__adapt___`` +functionality for particular interfaces. + +.. doctest:: + + >>> class ICustomAdapt(zope.interface.Interface): + ... @zope.interface.interfacemethod + ... def __adapt__(self, obj): + ... if isinstance(obj, str): + ... return obj + ... return super(type(ICustomAdapt), self).__adapt__(obj) + + >>> @zope.interface.implementer(ICustomAdapt) + ... class CustomAdapt(object): + ... pass + >>> ICustomAdapt('a string') + 'a string' + >>> ICustomAdapt(CustomAdapt()) + + +.. seealso:: :func:`zope.interface.interfacemethod` + .. [#create] The main reason we subclass ``Interface`` is to cause the Python class statement to create an interface, rather than a class. diff --git a/docs/api/declarations.rst b/docs/api/declarations.rst index 26847b04..fa265e7a 100644 --- a/docs/api/declarations.rst +++ b/docs/api/declarations.rst @@ -18,6 +18,26 @@ carefully at each object it documents, including providing examples. .. currentmodule:: zope.interface +Declaring Interfaces +==================== + +To declare an interface itself, extend the ``Interface`` base class. + +.. autointerface:: Interface + :noindex: + +.. autofunction:: taggedValue + :noindex: + + .. documented more thoroughly in README.rst + +.. autofunction:: invariant + :noindex: + + .. documented in README.rst + +.. autofunction:: interfacemethod + Declaring The Interfaces of Objects =================================== diff --git a/docs/conf.py b/docs/conf.py index 464f6522..59dac0d8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -264,6 +264,12 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'https://docs.python.org/': None} -autodoc_default_flags = ['members', 'show-inheritance'] -autoclass_content = 'both' +# Sphinx 1.8+ prefers this to `autodoc_default_flags`. It's documented that +# either True or None mean the same thing as just setting the flag, but +# only None works in 1.8 (True works in 2.0) +autodoc_default_options = { + 'members': None, + 'show-inheritance': None, +} autodoc_member_order = 'bysource' +autoclass_content = 'both' diff --git a/src/zope/interface/__init__.py b/src/zope/interface/__init__.py index e282fbda..3372103e 100644 --- a/src/zope/interface/__init__.py +++ b/src/zope/interface/__init__.py @@ -49,7 +49,7 @@ def meth(arg1, arg2): See the module doc strings for more information. """ __docformat__ = 'restructuredtext' - +# pylint:disable=wrong-import-position,unused-import from zope.interface.interface import Interface from zope.interface.interface import _wire @@ -75,8 +75,11 @@ def meth(arg1, arg2): from zope.interface.declarations import noLongerProvides from zope.interface.declarations import providedBy from zope.interface.declarations import provider + from zope.interface.exceptions import Invalid + from zope.interface.interface import Attribute +from zope.interface.interface import interfacemethod from zope.interface.interface import invariant from zope.interface.interface import taggedValue diff --git a/src/zope/interface/_zope_interface_coptimizations.c b/src/zope/interface/_zope_interface_coptimizations.c index 4e09614c..346e7f2d 100644 --- a/src/zope/interface/_zope_interface_coptimizations.c +++ b/src/zope/interface/_zope_interface_coptimizations.c @@ -49,6 +49,8 @@ static PyObject *str_registry, *strro, *str_generation, *strchanged; static PyObject *str__self__; static PyObject *str__module__; static PyObject *str__name__; +static PyObject *str__adapt__; +static PyObject *str_CALL_CUSTOM_ADAPT; static PyTypeObject *Implements; @@ -178,23 +180,46 @@ getObjectSpecification(PyObject *ignored, PyObject *ob) PyObject *cls, *result; result = PyObject_GetAttr(ob, str__provides__); - if (result != NULL && PyObject_TypeCheck(result, &SpecificationBaseType)) - return result; - - - PyErr_Clear(); + if (!result) + { + if (!PyErr_ExceptionMatches(PyExc_AttributeError)) + { + /* Propagate non AttributeError exceptions. */ + return NULL; + } + PyErr_Clear(); + } + else + { + int is_instance = -1; + is_instance = PyObject_IsInstance(result, (PyObject*)&SpecificationBaseType); + if (is_instance < 0) + { + /* Propagate all errors */ + return NULL; + } + if (is_instance) + { + return result; + } + } /* We do a getattr here so as not to be defeated by proxies */ cls = PyObject_GetAttr(ob, str__class__); if (cls == NULL) - { + { + if (!PyErr_ExceptionMatches(PyExc_AttributeError)) + { + /* Propagate non-AttributeErrors */ + return NULL; + } PyErr_Clear(); if (imported_declarations == 0 && import_declarations() < 0) return NULL; Py_INCREF(empty); return empty; - } + } result = implementedBy(NULL, cls); Py_DECREF(cls); @@ -205,10 +230,20 @@ static PyObject * providedBy(PyObject *ignored, PyObject *ob) { PyObject *result, *cls, *cp; - + int is_instance = -1; result = NULL; - if (PyObject_TypeCheck(ob, &PySuper_Type)) + is_instance = PyObject_IsInstance(ob, (PyObject*)&PySuper_Type); + if (is_instance < 0) + { + if (!PyErr_ExceptionMatches(PyExc_AttributeError)) + { + /* Propagate non-AttributeErrors */ + return NULL; + } + PyErr_Clear(); + } + if (is_instance) { return implementedBy(NULL, ob); } @@ -216,10 +251,15 @@ providedBy(PyObject *ignored, PyObject *ob) result = PyObject_GetAttr(ob, str__providedBy__); if (result == NULL) - { + { + if (!PyErr_ExceptionMatches(PyExc_AttributeError)) + { + return NULL; + } + PyErr_Clear(); return getObjectSpecification(NULL, ob); - } + } /* We want to make sure we have a spec. We can't do a type check @@ -737,63 +777,101 @@ static struct PyMethodDef ib_methods[] = { }; /* - def __call__(self, obj, alternate=_marker): - conform = getattr(obj, '__conform__', None) - if conform is not None: - adapter = self._call_conform(conform) - if adapter is not None: - return adapter - - adapter = self.__adapt__(obj) - + def __call__(self, obj, alternate=_marker): + try: + conform = obj.__conform__ + except AttributeError: # pylint:disable=bare-except + conform = None + + if conform is not None: + adapter = self._call_conform(conform) if adapter is not None: return adapter - elif alternate is not _marker: - return alternate - else: - raise TypeError("Could not adapt", obj, self) + + adapter = self.__adapt__(obj) + + if adapter is not None: + return adapter + if alternate is not _marker: + return alternate + raise TypeError("Could not adapt", obj, self) + */ static PyObject * -ib_call(PyObject *self, PyObject *args, PyObject *kwargs) +IB_call(PyObject *self, PyObject *args, PyObject *kwargs) { - PyObject *conform, *obj, *alternate=NULL, *adapter; - + PyObject *conform, *obj, *alternate, *adapter; static char *kwlist[] = {"obj", "alternate", NULL}; + conform = obj = alternate = adapter = NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", kwlist, &obj, &alternate)) return NULL; conform = PyObject_GetAttr(obj, str__conform__); - if (conform != NULL) - { + if (conform == NULL) + { + if (!PyErr_ExceptionMatches(PyExc_AttributeError)) + { + /* Propagate non-AttributeErrors */ + return NULL; + } + PyErr_Clear(); + + Py_INCREF(Py_None); + conform = Py_None; + } + + if (conform != Py_None) + { adapter = PyObject_CallMethodObjArgs(self, str_call_conform, conform, NULL); Py_DECREF(conform); if (adapter == NULL || adapter != Py_None) - return adapter; + return adapter; Py_DECREF(adapter); - } + } + else + { + Py_DECREF(conform); + } + + /* We differ from the Python code here. For speed, instead of always calling + self.__adapt__(), we check to see if the type has defined it. Checking in + the dict for __adapt__ isn't sufficient because there's no cheap way to + tell if it's the __adapt__ that InterfaceBase itself defines (our type + will *never* be InterfaceBase, we're always subclassed by + InterfaceClass). Instead, we cooperate with InterfaceClass in Python to + set a flag in a new subclass when this is necessary. */ + if (PyDict_GetItem(self->ob_type->tp_dict, str_CALL_CUSTOM_ADAPT)) + { + /* Doesn't matter what the value is. Simply being present is enough. */ + adapter = PyObject_CallMethodObjArgs(self, str__adapt__, obj, NULL); + } else - PyErr_Clear(); + { + adapter = __adapt__(self, obj); + } - adapter = __adapt__(self, obj); if (adapter == NULL || adapter != Py_None) - return adapter; + { + return adapter; + } Py_DECREF(adapter); if (alternate != NULL) - { + { Py_INCREF(alternate); return alternate; - } + } adapter = Py_BuildValue("sOO", "Could not adapt", obj, self); if (adapter != NULL) - { + { PyErr_SetObject(PyExc_TypeError, adapter); Py_DECREF(adapter); - } + } return NULL; } @@ -985,7 +1063,7 @@ static PyTypeObject InterfaceBaseType = { /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ (hashfunc)IB_hash, - /* tp_call */ (ternaryfunc)ib_call, + /* tp_call */ (ternaryfunc)IB_call, /* tp_str */ (reprfunc)0, /* tp_getattro */ (getattrofunc)0, /* tp_setattro */ (setattrofunc)0, @@ -1963,6 +2041,8 @@ init(void) DEFINE_STRING(__self__); DEFINE_STRING(__name__); DEFINE_STRING(__module__); + DEFINE_STRING(__adapt__); + DEFINE_STRING(_CALL_CUSTOM_ADAPT); #undef DEFINE_STRING adapter_hooks = PyList_New(0); if (adapter_hooks == NULL) diff --git a/src/zope/interface/common/__init__.py b/src/zope/interface/common/__init__.py index a8bedf06..b40c3178 100644 --- a/src/zope/interface/common/__init__.py +++ b/src/zope/interface/common/__init__.py @@ -259,5 +259,5 @@ def getRegisteredConformers(self): return set(itertools.chain(registered, self.__extra_classes)) -ABCInterface = ABCInterfaceClass.__new__(ABCInterfaceClass, None, None, None) +ABCInterface = ABCInterfaceClass.__new__(ABCInterfaceClass, 'ABCInterfaceClass', (), {}) InterfaceClass.__init__(ABCInterface, 'ABCInterface', (Interface,), {}) diff --git a/src/zope/interface/declarations.py b/src/zope/interface/declarations.py index e84a728e..b42c906e 100644 --- a/src/zope/interface/declarations.py +++ b/src/zope/interface/declarations.py @@ -984,6 +984,7 @@ def moduleProvides(*interfaces): # XXX: is this a fossil? Nobody calls it, no unit tests exercise it, no # doctests import it, and the package __init__ doesn't import it. +# (Answer: Versions of zope.container prior to 4.4.0 called this.) def ObjectSpecification(direct, cls): """Provide object specifications @@ -994,16 +995,17 @@ def ObjectSpecification(direct, cls): @_use_c_impl def getObjectSpecification(ob): try: - provides = getattr(ob, '__provides__', None) - except: + provides = ob.__provides__ + except AttributeError: provides = None + if provides is not None: if isinstance(provides, SpecificationBase): return provides try: cls = ob.__class__ - except: + except AttributeError: # We can't get the class, so just consider provides return _empty return implementedBy(cls) @@ -1028,7 +1030,7 @@ def providedBy(ob): return implementedBy(ob) r = ob.__providedBy__ - except: + except AttributeError: # Not set yet. Fall back to lower-level thing that computes it return getObjectSpecification(ob) diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py index ac556339..ff26d333 100644 --- a/src/zope/interface/interface.py +++ b/src/zope/interface/interface.py @@ -20,6 +20,7 @@ import weakref from zope.interface._compat import _use_c_impl +from zope.interface._compat import PYTHON2 as PY2 from zope.interface.exceptions import Invalid from zope.interface.ro import ro as calculate_ro from zope.interface import ro @@ -36,7 +37,10 @@ CO_VARARGS = 4 CO_VARKEYWORDS = 8 +# Put in the attrs dict of an interface by ``taggedValue`` and ``invariants`` TAGGED_DATA = '__interface_tagged_values__' +# Put in the attrs dict of an interface by ``interfacemethod`` +INTERFACE_METHODS = '__interface_methods__' _decorator_non_return = object() _marker = object() @@ -275,16 +279,10 @@ def __call__(self, obj, alternate=_marker): """Adapt an object to the interface """ try: - conform = getattr(obj, '__conform__', None) - # 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 = obj.__conform__ + except AttributeError: conform = None + if conform is not None: adapter = self._call_conform(conform) if adapter is not None: @@ -657,6 +655,21 @@ def __repr__(cls): ) +def interfacemethod(func): + """ + Convert a method specification to an actual method of the interface. + + This is a decorator that functions like `staticmethod` et al. + + The primary use of this decorator is to allow interface definitions to + define the ``__adapt__`` method. + """ + f_locals = sys._getframe(1).f_locals + methods = f_locals.setdefault(INTERFACE_METHODS, {}) + methods[func.__name__] = func + return _decorator_non_return + + class InterfaceClass(_InterfaceClassBase): """ Prototype (scarecrow) Interfaces Implementation. @@ -670,6 +683,57 @@ class InterfaceClass(_InterfaceClassBase): # #implements(IInterface) + def __new__(cls, name=None, bases=(), attrs=None, __doc__=None, # pylint:disable=redefined-builtin + __module__=None): + assert isinstance(bases, tuple) + attrs = attrs or {} + needs_custom_class = attrs.pop(INTERFACE_METHODS, None) + if needs_custom_class: + needs_custom_class.update( + {'__classcell__': attrs.pop('__classcell__')} + if '__classcell__' in attrs + else {} + ) + if '__adapt__' in needs_custom_class: + # We need to tell the C code to call this. + needs_custom_class['_CALL_CUSTOM_ADAPT'] = 1 + + if issubclass(cls, _InterfaceClassWithCustomMethods): + cls_bases = (cls,) + elif cls is InterfaceClass: + cls_bases = (_InterfaceClassWithCustomMethods,) + else: + cls_bases = (cls, _InterfaceClassWithCustomMethods) + + cls = type(cls)( # pylint:disable=self-cls-assignment + name + "", + cls_bases, + needs_custom_class + ) + elif PY2 and bases and len(bases) > 1: + bases_with_custom_methods = tuple( + type(b) + for b in bases + if issubclass(type(b), _InterfaceClassWithCustomMethods) + ) + + # If we have a subclass of InterfaceClass in *bases*, + # Python 3 is smart enough to pass that as *cls*, but Python + # 2 just passes whatever the first base in *bases* is. This means that if + # we have multiple inheritance, and one of our bases has already defined + # a custom method like ``__adapt__``, we do the right thing automatically + # and extend it on Python 3, but not necessarily on Python 2. To fix this, we need + # to run the MRO algorithm and get the most derived base manually. + # Note that this only works for consistent resolution orders + if bases_with_custom_methods: + cls = type( # pylint:disable=self-cls-assignment + name + "", + bases_with_custom_methods, + {} + ).__mro__[1] # Not the class we created, the most derived. + + return _InterfaceClassBase.__new__(cls) + def __init__(self, name, bases=(), attrs=None, __doc__=None, # pylint:disable=redefined-builtin __module__=None): # We don't call our metaclass parent directly @@ -744,7 +808,7 @@ def update_value(aname, aval): # __qualname__: PEP 3155 (Python 3.3+) '__qualname__', # __annotations__: PEP 3107 (Python 3.0+) - '__annotations__' + '__annotations__', ) and aval is not _decorator_non_return } @@ -895,6 +959,11 @@ def __reduce__(self): Specification._ROOT = Interface ro._ROOT = Interface +class _InterfaceClassWithCustomMethods(InterfaceClass): + """ + Marker class for interfaces with custom methods that override InterfaceClass methods. + """ + class Attribute(Element): """Attribute descriptions diff --git a/src/zope/interface/interfaces.py b/src/zope/interface/interfaces.py index cb455e08..6321d0c4 100644 --- a/src/zope/interface/interfaces.py +++ b/src/zope/interface/interfaces.py @@ -451,46 +451,134 @@ def __nonzero__(): """ class IInterfaceDeclaration(Interface): - """Declare and check the interfaces of objects + """ + Declare and check the interfaces of objects. The functions defined in this interface are used to declare the - interfaces that objects provide and to query the interfaces that have - been declared. + interfaces that objects provide and to query the interfaces that + have been declared. Interfaces can be declared for objects in two ways: - - Interfaces are declared for instances of the object's class + - Interfaces are declared for instances of the object's class - - Interfaces are declared for the object directly. + - Interfaces are declared for the object directly. The interfaces declared for an object are, therefore, the union of interfaces declared for the object directly and the interfaces declared for instances of the object's class. Note that we say that a class implements the interfaces provided - by it's instances. An instance can also provide interfaces - directly. The interfaces provided by an object are the union of + by it's instances. An instance can also provide interfaces + directly. The interfaces provided by an object are the union of the interfaces provided directly and the interfaces implemented by the class. + + This interface is implemented by :mod:`zope.interface`. """ + ### + # Defining interfaces + ### + + Interface = Attribute("The base class used to create new interfaces") + + def taggedValue(key, value): + """ + Attach a tagged value to an interface while defining the interface. + + This is a way of executing :meth:`IElement.setTaggedValue` from + the definition of the interface. For example:: + + class IFoo(Interface): + taggedValue('key', 'value') + + .. seealso:: `zope.interface.taggedValue` + """ + + def invariant(checker_function): + """ + Attach an invariant checker function to an interface while defining it. + + Invariants can later be validated against particular implementations by + calling :meth:`IInterface.validateInvariants`. + + For example:: + + def check_range(ob): + if ob.max < ob.min: + range ValueError + + class IRange(Interface): + min = Attribute("The min value") + max = Attribute("The max value") + + invariant(check_range) + + .. seealso:: `zope.interface.invariant` + """ + + def interfacemethod(method): + """ + A decorator that transforms a method specification into an + implementation method. + + This is used to override methods of ``Interface`` or provide new methods. + Definitions using this decorator will not appear in :meth:`IInterface.names()`. + It is possible to have an implementation method and a method specification + of the same name. + + For example:: + + class IRange(Interface): + @interfacemethod + def __adapt__(self, obj): + if isinstance(obj, range): + # Return the builtin ``range`` as-is + return obj + return super(type(IRange), self).__adapt__(obj) + + You can use ``super`` to call the parent class functionality. Note that + the zero-argument version (``super().__adapt__``) works on Python 3.6 and above, but + prior to that the two-argument version must be used, and the class must be explicitly + passed as the first argument. + + .. versionadded:: 5.1.0 + .. seealso:: `zope.interface.interfacemethod` + """ + + ### + # Querying interfaces + ### + def providedBy(ob): - """Return the interfaces provided by an object + """ + Return the interfaces provided by an object. This is the union of the interfaces directly provided by an object and interfaces implemented by it's class. The value returned is an `IDeclaration`. + + .. seealso:: `zope.interface.providedBy` """ def implementedBy(class_): - """Return the interfaces implemented for a class' instances + """ + Return the interfaces implemented for a class's instances. The value returned is an `IDeclaration`. + + .. seealso:: `zope.interface.implementedBy` """ + ### + # Declaring interfaces + ### + def classImplements(class_, *interfaces): - """Declare additional interfaces implemented for instances of a class + """ + Declare additional interfaces implemented for instances of a class. The arguments after the class are one or more interfaces or interface specifications (`IDeclaration` objects). @@ -508,7 +596,14 @@ class C(A, B): Instances of ``C`` provide ``I1``, ``I2``, and whatever interfaces - instances of ``A`` and ``B`` provide. + instances of ``A`` and ``B`` provide. This is equivalent to:: + + @implementer(I1, I2) + class C(A, B): + pass + + .. seealso:: `zope.interface.classImplements` + .. seealso:: `zope.interface.implementer` """ def classImplementsFirst(cls, interface): @@ -517,14 +612,19 @@ def classImplementsFirst(cls, interface): """ def implementer(*interfaces): - """Create a decorator for declaring interfaces implemented by a factory. + """ + Create a decorator for declaring interfaces implemented by a + factory. A callable is returned that makes an implements declaration on objects passed to it. + + .. seealso:: :meth:`classImplements` """ def classImplementsOnly(class_, *interfaces): - """Declare the only interfaces implemented by instances of a class + """ + Declare the only interfaces implemented by instances of a class. The arguments after the class are one or more interfaces or interface specifications (`IDeclaration` objects). @@ -542,30 +642,41 @@ class C(A, B): Instances of ``C`` provide only ``I1``, ``I2``, and regardless of whatever interfaces instances of ``A`` and ``B`` implement. + + .. seealso:: `zope.interface.classImplementsOnly` """ def implementer_only(*interfaces): - """Create a decorator for declaring the only interfaces implemented + """ + Create a decorator for declaring the only interfaces implemented. A callable is returned that makes an implements declaration on objects passed to it. + + .. seealso:: `zope.interface.implementer_only` """ def directlyProvidedBy(object): - """Return the interfaces directly provided by the given object + """ + Return the interfaces directly provided by the given object. The value returned is an `IDeclaration`. + + .. seealso:: `zope.interface.directlyProvidedBy` """ def directlyProvides(object, *interfaces): - """Declare interfaces declared directly for an object + """ + Declare interfaces declared directly for an object. The arguments after the object are one or more interfaces or interface specifications (`IDeclaration` objects). - The interfaces given (including the interfaces in the - specifications) replace interfaces previously - declared for the object. + .. caution:: + The interfaces given (including the interfaces in the + specifications) *replace* interfaces previously + declared for the object. See :meth:`alsoProvides` to add + additional interfaces. Consider the following example:: @@ -594,21 +705,31 @@ class C(A, B): directlyProvides(ob, directlyProvidedBy(ob), I2) adds I2 to the interfaces directly provided by ob. + + .. seealso:: `zope.interface.directlyProvides` """ def alsoProvides(object, *interfaces): - """Declare additional interfaces directly for an object:: + """ + Declare additional interfaces directly for an object. + + For example:: alsoProvides(ob, I1) is equivalent to:: directlyProvides(ob, directlyProvidedBy(ob), I1) + + .. seealso:: `zope.interface.alsoProvides` """ def noLongerProvides(object, interface): - """Remove an interface from the list of an object's directly - provided interfaces:: + """ + Remove an interface from the list of an object's directly provided + interfaces. + + For example:: noLongerProvides(ob, I1) @@ -619,10 +740,17 @@ def noLongerProvides(object, interface): with the exception that if ``I1`` is an interface that is provided by ``ob`` through the class's implementation, `ValueError` is raised. + + .. seealso:: `zope.interface.noLongerProvides` """ def implements(*interfaces): - """Declare interfaces implemented by instances of a class + """ + Declare interfaces implemented by instances of a class. + + .. deprecated:: 5.0 + This only works for Python 2. The `implementer` decorator + is preferred for all versions. This function is called in a class definition (Python 2.x only). @@ -655,14 +783,15 @@ class C(A, B): Instances of ``C`` implement ``I1``, ``I2``, and whatever interfaces instances of ``A`` and ``B`` implement. - - .. deprecated:: 5.0 - This only works for Python 2. The `implementer` decorator - is preferred for all versions. """ def implementsOnly(*interfaces): - """Declare the only interfaces implemented by instances of a class + """ + Declare the only interfaces implemented by instances of a class. + + .. deprecated:: 5.0 + This only works for Python 2. The `implementer_only` decorator + is preferred for all versions. This function is called in a class definition (Python 2.x only). @@ -691,14 +820,15 @@ class C(A, B): Instances of ``C`` implement ``I1``, ``I2``, regardless of what instances of ``A`` and ``B`` implement. - - .. deprecated:: 5.0 - This only works for Python 2. The `implementer_only` decorator - is preferred for all versions. """ def classProvides(*interfaces): - """Declare interfaces provided directly by a class + """ + Declare interfaces provided directly by a class. + + .. deprecated:: 5.0 + This only works for Python 2. The `provider` decorator + is preferred for all versions. This function is called in a class definition. @@ -725,17 +855,18 @@ class has an direct interface specification. In other words, it is directlyProvides(theclass, I1) after the class has been created. - - .. deprecated:: 5.0 - This only works for Python 2. The `provider` decorator - is preferred for all versions. """ def provider(*interfaces): - """A class decorator version of `classProvides`""" + """ + A class decorator version of `classProvides`. + + .. seealso:: `zope.interface.provider` + """ def moduleProvides(*interfaces): - """Declare interfaces provided by a module + """ + Declare interfaces provided by a module. This function is used in a module definition. @@ -757,16 +888,21 @@ def moduleProvides(*interfaces): is equivalent to:: directlyProvides(sys.modules[__name__], I1) + + .. seealso:: `zope.interface.moduleProvides` """ def Declaration(*interfaces): - """Create an interface specification + """ + Create an interface specification. The arguments are one or more interfaces or interface specifications (`IDeclaration` objects). - A new interface specification (`IDeclaration`) with - the given interfaces is returned. + A new interface specification (`IDeclaration`) with the given + interfaces is returned. + + .. seealso:: `zope.interface.Declaration` """ class IAdapterRegistry(Interface): diff --git a/src/zope/interface/tests/__init__.py b/src/zope/interface/tests/__init__.py index 8b9ef258..6a11218b 100644 --- a/src/zope/interface/tests/__init__.py +++ b/src/zope/interface/tests/__init__.py @@ -32,6 +32,66 @@ def test_optimizations(self): else: self.assertIs(used, fallback) + +class MissingSomeAttrs(object): + """ + Helper for tests that raises a specific exception + for attributes that are missing. This is usually not + an AttributeError, and this object is used to test that + those errors are not improperly caught and treated like + an AttributeError. + """ + + def __init__(self, exc_kind, **other_attrs): + self.__exc_kind = exc_kind + d = object.__getattribute__(self, '__dict__') + d.update(other_attrs) + + def __getattribute__(self, name): + # Note that we ignore objects found in the class dictionary. + d = object.__getattribute__(self, '__dict__') + try: + return d[name] + except KeyError: + raise d['_MissingSomeAttrs__exc_kind'](name) + + EXCEPTION_CLASSES = ( + TypeError, + RuntimeError, + BaseException, + ValueError, + ) + + @classmethod + def test_raises(cls, unittest, test_func, expected_missing, **other_attrs): + """ + Loop through various exceptions, calling *test_func* inside a ``assertRaises`` block. + + :param test_func: A callable of one argument, the instance of this + class. + :param str expected_missing: The attribute that should fail with the exception. + This is used to ensure that we're testing the path we think we are. + :param other_attrs: Attributes that should be provided on the test object. + Must not contain *expected_missing*. + """ + assert isinstance(expected_missing, str) + assert expected_missing not in other_attrs + for exc in cls.EXCEPTION_CLASSES: + ob = cls(exc, **other_attrs) + with unittest.assertRaises(exc) as ex: + test_func(ob) + + unittest.assertEqual(ex.exception.args[0], expected_missing) + + # Now test that the AttributeError for that expected_missing is *not* raised. + ob = cls(AttributeError, **other_attrs) + try: + test_func(ob) + except AttributeError as e: + unittest.assertNotIn(expected_missing, str(e)) + except Exception: # pylint:disable=broad-except + pass + # Be sure cleanup functionality is available; classes that use the adapter hook # need to be sure to subclass ``CleanUp``. # diff --git a/src/zope/interface/tests/test_adapter.py b/src/zope/interface/tests/test_adapter.py index 0456b1c8..8ff96f06 100644 --- a/src/zope/interface/tests/test_adapter.py +++ b/src/zope/interface/tests/test_adapter.py @@ -17,21 +17,32 @@ from zope.interface.tests import OptimizationTestMixin +# pylint:disable=inherit-non-class,protected-access,too-many-lines +# pylint:disable=attribute-defined-outside-init,blacklisted-name def _makeInterfaces(): from zope.interface import Interface - class IB0(Interface): pass - class IB1(IB0): pass - class IB2(IB0): pass - class IB3(IB2, IB1): pass - class IB4(IB1, IB2): pass - - class IF0(Interface): pass - class IF1(IF0): pass - - class IR0(Interface): pass - class IR1(IR0): pass + class IB0(Interface): + pass + class IB1(IB0): + pass + class IB2(IB0): + pass + class IB3(IB2, IB1): + pass + class IB4(IB1, IB2): + pass + + class IF0(Interface): + pass + class IF1(IF0): + pass + + class IR0(Interface): + pass + class IR1(IR0): + pass return IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 @@ -51,7 +62,7 @@ def add_extendor(self, provided): self._extendors += (provided,) def remove_extendor(self, provided): self._extendors = tuple([x for x in self._extendors - if x != provided]) + if x != provided]) for name in BaseAdapterRegistry._delegated: setattr(_CUT.LookupClass, name, object()) return _CUT @@ -80,13 +91,14 @@ def test__generation_after_calling_changed(self): self.assertEqual(registry._v_lookup._changed, (registry, orig,)) def test__generation_after_changing___bases__(self): - class _Base(object): pass + class _Base(object): + pass registry = self._makeOne() registry.__bases__ = (_Base,) self.assertEqual(registry._generation, 2) def test_register(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() registry.register([IB0], IR0, '', 'A1') self.assertEqual(registry.registered([IB0], IR0, ''), 'A1') @@ -94,20 +106,20 @@ def test_register(self): self.assertEqual(registry._generation, 2) def test_register_with_invalid_name(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() with self.assertRaises(ValueError): registry.register([IB0], IR0, object(), 'A1') def test_register_with_value_None_unregisters(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() registry.register([None], IR0, '', 'A1') registry.register([None], IR0, '', None) self.assertEqual(len(registry._adapters), 0) def test_register_with_same_value(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() _value = object() registry.register([None], IR0, '', _value) @@ -120,7 +132,7 @@ def test_registered_empty(self): self.assertEqual(registry.registered([None], None, ''), None) def test_registered_non_empty_miss(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() registry.register([IB1], None, '', 'A1') self.assertEqual(registry.registered([IB2], None, ''), None) @@ -136,21 +148,21 @@ def test_unregister_empty(self): self.assertEqual(registry.registered([None], None, ''), None) def test_unregister_non_empty_miss_on_required(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() registry.register([IB1], None, '', 'A1') registry.unregister([IB2], None, '') #doesn't raise self.assertEqual(registry.registered([IB1], None, ''), 'A1') def test_unregister_non_empty_miss_on_name(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() registry.register([IB1], None, '', 'A1') registry.unregister([IB1], None, 'nonesuch') #doesn't raise self.assertEqual(registry.registered([IB1], None, ''), 'A1') def test_unregister_with_value_not_None_miss(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() orig = object() nomatch = object() @@ -159,7 +171,7 @@ def test_unregister_with_value_not_None_miss(self): self.assertTrue(registry.registered([IB1], None, '') is orig) def test_unregister_hit_clears_empty_subcomponents(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() one = object() another = object() @@ -177,7 +189,7 @@ def test_unsubscribe_empty(self): self.assertEqual(registry.registered([None], None, ''), None) def test_unsubscribe_hit(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() orig = object() registry.subscribe([IB1], None, orig) @@ -185,7 +197,7 @@ def test_unsubscribe_hit(self): self.assertEqual(len(registry._subscribers), 0) def test_unsubscribe_after_multiple(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() first = object() second = object() @@ -202,18 +214,18 @@ def test_unsubscribe_after_multiple(self): self.assertEqual(len(registry._subscribers), 0) def test_unsubscribe_w_None_after_multiple(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() first = object() second = object() - third = object() + registry.subscribe([IB1], None, first) registry.subscribe([IB1], None, second) registry.unsubscribe([IB1], None) self.assertEqual(len(registry._subscribers), 0) def test_unsubscribe_non_empty_miss_on_required(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() registry.subscribe([IB1], None, 'A1') self.assertEqual(len(registry._subscribers), 2) @@ -221,7 +233,7 @@ def test_unsubscribe_non_empty_miss_on_required(self): self.assertEqual(len(registry._subscribers), 2) def test_unsubscribe_non_empty_miss_on_value(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() registry.subscribe([IB1], None, 'A1') self.assertEqual(len(registry._subscribers), 2) @@ -229,7 +241,7 @@ def test_unsubscribe_non_empty_miss_on_value(self): self.assertEqual(len(registry._subscribers), 2) def test_unsubscribe_with_value_not_None_miss(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() orig = object() nomatch = object() @@ -241,7 +253,7 @@ def _instance_method_notify_target(self): self.fail("Example method, not intended to be called.") def test_unsubscribe_instance_method(self): - IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() + IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable registry = self._makeOne() self.assertEqual(len(registry._subscribers), 0) registry.subscribe([IB1], None, self._instance_method_notify_target) @@ -252,13 +264,14 @@ def test_unsubscribe_instance_method(self): class LookupBaseFallbackTests(unittest.TestCase): def _getFallbackClass(self): - from zope.interface.adapter import LookupBaseFallback + from zope.interface.adapter import LookupBaseFallback # pylint:disable=no-name-in-module return LookupBaseFallback _getTargetClass = _getFallbackClass def _makeOne(self, uc_lookup=None, uc_lookupAll=None, uc_subscriptions=None): + # pylint:disable=function-redefined if uc_lookup is None: def uc_lookup(self, required, provided, name): pass @@ -285,7 +298,7 @@ def test_lookup_miss_no_default(self): _called_with = [] def _lookup(self, required, provided, name): _called_with.append((required, provided, name)) - return None + lb = self._makeOne(uc_lookup=_lookup) found = lb.lookup(('A',), 'B', 'C') self.assertTrue(found is None) @@ -296,7 +309,7 @@ def test_lookup_miss_w_default(self): _default = object() def _lookup(self, required, provided, name): _called_with.append((required, provided, name)) - return None + lb = self._makeOne(uc_lookup=_lookup) found = lb.lookup(('A',), 'B', 'C', _default) self.assertTrue(found is _default) @@ -384,7 +397,7 @@ def test_lookup1_miss_no_default(self): _called_with = [] def _lookup(self, required, provided, name): _called_with.append((required, provided, name)) - return None + lb = self._makeOne(uc_lookup=_lookup) found = lb.lookup1('A', 'B', 'C') self.assertTrue(found is None) @@ -395,7 +408,7 @@ def test_lookup1_miss_w_default(self): _default = object() def _lookup(self, required, provided, name): _called_with.append((required, provided, name)) - return None + lb = self._makeOne(uc_lookup=_lookup) found = lb.lookup1('A', 'B', 'C', _default) self.assertTrue(found is _default) @@ -406,7 +419,7 @@ def test_lookup1_miss_w_default_negative_cache(self): _default = object() def _lookup(self, required, provided, name): _called_with.append((required, provided, name)) - return None + lb = self._makeOne(uc_lookup=_lookup) found = lb.lookup1('A', 'B', 'C', _default) self.assertTrue(found is _default) @@ -479,7 +492,7 @@ def test_adapter_hook_hit_factory_returns_None(self): _f_called_with = [] def _factory(context): _f_called_with.append(context) - return None + def _lookup(self, required, provided, name): return _factory req, prv, _default = object(), object(), object() @@ -588,13 +601,14 @@ def _getTargetClass(self): class VerifyingBaseFallbackTests(unittest.TestCase): def _getFallbackClass(self): - from zope.interface.adapter import VerifyingBaseFallback + from zope.interface.adapter import VerifyingBaseFallback # pylint:disable=no-name-in-module return VerifyingBaseFallback _getTargetClass = _getFallbackClass def _makeOne(self, registry, uc_lookup=None, uc_lookupAll=None, uc_subscriptions=None): + # pylint:disable=function-redefined if uc_lookup is None: def uc_lookup(self, required, provided, name): raise NotImplementedError() @@ -641,7 +655,7 @@ def _lookup(self, required, provided, name): found = lb.lookup(('A',), 'B', 'C') self.assertTrue(found is b) self.assertEqual(_called_with, - [(('A',), 'B', 'C'), (('A',), 'B', 'C')]) + [(('A',), 'B', 'C'), (('A',), 'B', 'C')]) self.assertEqual(_results, [c]) def test_lookup1(self): @@ -662,7 +676,7 @@ def _lookup(self, required, provided, name): found = lb.lookup1('A', 'B', 'C') self.assertTrue(found is b) self.assertEqual(_called_with, - [(('A',), 'B', 'C'), (('A',), 'B', 'C')]) + [(('A',), 'B', 'C'), (('A',), 'B', 'C')]) self.assertEqual(_results, [c]) def test_adapter_hook(self): @@ -782,12 +796,12 @@ def test_ctor_w_registry_provided(self): from zope.interface import Interface from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) alb = self._makeOne(registry) self.assertEqual(sorted(alb._extendors.keys()), sorted([IBar, IFoo, Interface])) - self.assertEqual(alb._extendors[IFoo], [IFoo]) + self.assertEqual(alb._extendors[IFoo], [IFoo, IBar]) self.assertEqual(alb._extendors[IBar], [IBar]) self.assertEqual(sorted(alb._extendors[Interface]), sorted([IFoo, IBar])) @@ -815,8 +829,7 @@ class FauxWeakref(object): def __init__(self, here): self._here = here def __call__(self): - if self._here: - return self + return self if self._here else None def unsubscribe(self, target): self._unsub = target gone = FauxWeakref(False) @@ -834,14 +847,14 @@ def test_init_extendors_after_registry_update(self): from zope.interface import Interface from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry() alb = self._makeOne(registry) registry._provided = [IFoo, IBar] alb.init_extendors() self.assertEqual(sorted(alb._extendors.keys()), sorted([IBar, IFoo, Interface])) - self.assertEqual(alb._extendors[IFoo], [IFoo]) + self.assertEqual(alb._extendors[IFoo], [IFoo, IBar]) self.assertEqual(alb._extendors[IBar], [IBar]) self.assertEqual(sorted(alb._extendors[Interface]), sorted([IFoo, IBar])) @@ -850,14 +863,14 @@ def test_add_extendor(self): from zope.interface import Interface from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry() alb = self._makeOne(registry) alb.add_extendor(IFoo) alb.add_extendor(IBar) self.assertEqual(sorted(alb._extendors.keys()), sorted([IBar, IFoo, Interface])) - self.assertEqual(alb._extendors[IFoo], [IFoo]) + self.assertEqual(alb._extendors[IFoo], [IFoo, IBar]) self.assertEqual(alb._extendors[IBar], [IBar]) self.assertEqual(sorted(alb._extendors[Interface]), sorted([IFoo, IBar])) @@ -866,13 +879,13 @@ def test_remove_extendor(self): from zope.interface import Interface from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) alb = self._makeOne(registry) alb.remove_extendor(IFoo) self.assertEqual(sorted(alb._extendors.keys()), sorted([IFoo, IBar, Interface])) - self.assertEqual(alb._extendors[IFoo], []) + self.assertEqual(alb._extendors[IFoo], [IBar]) self.assertEqual(alb._extendors[IBar], [IBar]) self.assertEqual(sorted(alb._extendors[Interface]), sorted([IBar])) @@ -882,7 +895,7 @@ def test_remove_extendor(self): def test__uncached_lookup_empty_ro(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry() alb = self._makeOne(registry) result = alb._uncached_lookup((IFoo,), IBar) @@ -893,7 +906,7 @@ def test__uncached_lookup_empty_ro(self): def test__uncached_lookup_order_miss(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() registry.ro.append(subr) @@ -904,7 +917,7 @@ def test__uncached_lookup_order_miss(self): def test__uncached_lookup_extendors_miss(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry() subr = self._makeSubregistry() subr._adapters = [{}, {}] #utilities, single adapters @@ -917,7 +930,7 @@ def test__uncached_lookup_extendors_miss(self): def test__uncached_lookup_components_miss_wrong_iface(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) IQux = InterfaceClass('IQux') registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() @@ -936,10 +949,10 @@ def test__uncached_lookup_components_miss_wrong_iface(self): def test__uncached_lookup_components_miss_wrong_name(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() - irrelevant = object() + wrongname = object() subr._adapters = [ #utilities, single adapters {}, @@ -955,7 +968,7 @@ def test__uncached_lookup_components_miss_wrong_name(self): def test__uncached_lookup_simple_hit(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() _expected = object() @@ -972,7 +985,7 @@ def test__uncached_lookup_simple_hit(self): def test__uncached_lookup_repeated_hit(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() _expected = object() @@ -992,7 +1005,7 @@ def test_queryMultiAdaptor_lookup_miss(self): from zope.interface.declarations import implementer from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) @implementer(IFoo) class Foo(object): pass @@ -1012,28 +1025,33 @@ class Foo(object): self.assertTrue(result is _default) def test_queryMultiAdapter_errors_on_attribute_access(self): - # Which leads to using the _empty singleton as "requires" - # argument. See https://github.com/zopefoundation/zope.interface/issues/162 + # Any error on attribute access previously lead to using the _empty singleton as "requires" + # argument (See https://github.com/zopefoundation/zope.interface/issues/162) + # but after https://github.com/zopefoundation/zope.interface/issues/200 + # they get propagated. from zope.interface.interface import InterfaceClass + from zope.interface.tests import MissingSomeAttrs + IFoo = InterfaceClass('IFoo') registry = self._makeRegistry() alb = self._makeOne(registry) alb.lookup = alb._uncached_lookup - class UnexpectedErrorsLikeAcquisition(object): - def __getattribute__(self, name): - raise RuntimeError("Acquisition does this. Ha-ha!") + def test(ob): + return alb.queryMultiAdapter( + (ob,), + IFoo, + ) - result = alb.queryMultiAdapter( - (UnexpectedErrorsLikeAcquisition(),), - IFoo, - ) + PY3 = str is not bytes + MissingSomeAttrs.test_raises(self, test, + expected_missing='__class__' if PY3 else '__providedBy__') def test_queryMultiAdaptor_factory_miss(self): from zope.interface.declarations import implementer from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) @implementer(IFoo) class Foo(object): pass @@ -1044,7 +1062,7 @@ class Foo(object): _called_with = [] def _factory(context): _called_with.append(context) - return None + subr._adapters = [ #utilities, single adapters {}, {IFoo: {IBar: {'': _factory}}}, @@ -1062,7 +1080,7 @@ def test_queryMultiAdaptor_factory_hit(self): from zope.interface.declarations import implementer from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) @implementer(IFoo) class Foo(object): pass @@ -1113,7 +1131,7 @@ def factory(*args): def test__uncached_lookupAll_empty_ro(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry() alb = self._makeOne(registry) result = alb._uncached_lookupAll((IFoo,), IBar) @@ -1124,7 +1142,7 @@ def test__uncached_lookupAll_empty_ro(self): def test__uncached_lookupAll_order_miss(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() registry.ro.append(subr) @@ -1136,7 +1154,7 @@ def test__uncached_lookupAll_order_miss(self): def test__uncached_lookupAll_extendors_miss(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry() subr = self._makeSubregistry() subr._adapters = [{}, {}] #utilities, single adapters @@ -1149,7 +1167,7 @@ def test__uncached_lookupAll_extendors_miss(self): def test__uncached_lookupAll_components_miss(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) IQux = InterfaceClass('IQux') registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() @@ -1167,7 +1185,7 @@ def test__uncached_lookupAll_components_miss(self): def test__uncached_lookupAll_simple_hit(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() _expected = object() @@ -1185,7 +1203,7 @@ def test__uncached_lookupAll_simple_hit(self): def test_names(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() _expected = object() @@ -1204,7 +1222,7 @@ def test_names(self): def test__uncached_subscriptions_empty_ro(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry() alb = self._makeOne(registry) result = alb._uncached_subscriptions((IFoo,), IBar) @@ -1215,7 +1233,7 @@ def test__uncached_subscriptions_empty_ro(self): def test__uncached_subscriptions_order_miss(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() registry.ro.append(subr) @@ -1227,7 +1245,7 @@ def test__uncached_subscriptions_order_miss(self): def test__uncached_subscriptions_extendors_miss(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry() subr = self._makeSubregistry() subr._subscribers = [{}, {}] #utilities, single adapters @@ -1240,7 +1258,7 @@ def test__uncached_subscriptions_extendors_miss(self): def test__uncached_subscriptions_components_miss_wrong_iface(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) IQux = InterfaceClass('IQux') registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() @@ -1258,7 +1276,7 @@ def test__uncached_subscriptions_components_miss_wrong_iface(self): def test__uncached_subscriptions_components_miss_wrong_name(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() wrongname = object() @@ -1275,7 +1293,7 @@ def test__uncached_subscriptions_components_miss_wrong_name(self): def test__uncached_subscriptions_simple_hit(self): from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) registry = self._makeRegistry(IFoo, IBar) subr = self._makeSubregistry() class Foo(object): @@ -1296,7 +1314,7 @@ def test_subscribers_wo_provided(self): from zope.interface.declarations import implementer from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) @implementer(IFoo) class Foo(object): pass @@ -1325,7 +1343,7 @@ def test_subscribers_w_provided(self): from zope.interface.declarations import implementer from zope.interface.interface import InterfaceClass IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar', IFoo) + IBar = InterfaceClass('IBar', (IFoo,)) @implementer(IFoo) class Foo(object): pass @@ -1343,7 +1361,7 @@ def _factory2(context): return _exp2 def _side_effect_only(context): _called.setdefault('_side_effect_only', []).append(context) - return None + subr._subscribers = [ #utilities, single adapters {}, {IFoo: {IBar: {'': (_factory1, _factory2, _side_effect_only)}}}, @@ -1441,13 +1459,12 @@ def test__convert_None_to_Interface_w_other(self): self.assertTrue(_convert_None_to_Interface(other) is other) def test__normalize_name_str(self): - import sys from zope.interface.adapter import _normalize_name STR = b'str' - if sys.version_info[0] < 3: - self.assertEqual(_normalize_name(STR), unicode(STR)) - else: - self.assertEqual(_normalize_name(STR), str(STR, 'ascii')) + UNICODE = u'str' + norm = _normalize_name(STR) + self.assertEqual(norm, UNICODE) + self.assertIsInstance(norm, type(UNICODE)) def test__normalize_name_unicode(self): from zope.interface.adapter import _normalize_name diff --git a/src/zope/interface/tests/test_declarations.py b/src/zope/interface/tests/test_declarations.py index 9344968b..83815d7e 100644 --- a/src/zope/interface/tests/test_declarations.py +++ b/src/zope/interface/tests/test_declarations.py @@ -16,31 +16,36 @@ import unittest from zope.interface._compat import _skip_under_py3k +from zope.interface._compat import PYTHON3 from zope.interface.tests import OptimizationTestMixin +from zope.interface.tests import MissingSomeAttrs from zope.interface.tests.test_interface import NameAndModuleComparisonTestsMixin +# pylint:disable=inherit-non-class,too-many-lines,protected-access +# pylint:disable=blacklisted-name,attribute-defined-outside-init class _Py3ClassAdvice(object): def _run_generated_code(self, code, globs, locs, fails_under_py3k=True, ): + # pylint:disable=exec-used,no-member import warnings - from zope.interface._compat import PYTHON3 with warnings.catch_warnings(record=True) as log: warnings.resetwarnings() if not PYTHON3: exec(code, globs, locs) self.assertEqual(len(log), 0) # no longer warn return True + + try: + exec(code, globs, locs) + except TypeError: + return False else: - try: - exec(code, globs, locs) - except TypeError: - return False - else: - if fails_under_py3k: - self.fail("Didn't raise TypeError") + if fails_under_py3k: + self.fail("Didn't raise TypeError") + return None class NamedTests(unittest.TestCase): @@ -52,7 +57,7 @@ def test_class(self): class Foo(object): pass - self.assertEqual(Foo.__component_name__, u'foo') + self.assertEqual(Foo.__component_name__, u'foo') # pylint:disable=no-member def test_function(self): from zope.interface.declarations import named @@ -71,7 +76,7 @@ class Foo(object): foo = Foo() named(u'foo')(foo) - self.assertEqual(foo.__component_name__, u'foo') + self.assertEqual(foo.__component_name__, u'foo') # pylint:disable=no-member class EmptyDeclarationTests(unittest.TestCase): @@ -155,8 +160,6 @@ def test_changed_w_existing__v_attrs(self): self.assertIsNone(decl._v_attrs) def test___contains__w_self(self): - from zope.interface.interface import InterfaceClass - IFoo = InterfaceClass('IFoo') decl = self._makeOne() self.assertNotIn(decl, decl) @@ -241,7 +244,7 @@ def test___sub___unrelated_interface(self): IBar = InterfaceClass('IBar') before = self._makeOne(IFoo) after = before - IBar - self.assertIsInstance(after, self._getTargetClass()) + self.assertIsInstance(after, self._getTargetClass()) self.assertEqual(list(after), [IFoo]) def test___sub___related_interface(self): @@ -265,7 +268,7 @@ def test___add___unrelated_interface(self): IBar = InterfaceClass('IBar') before = self._makeOne(IFoo) after = before + IBar - self.assertIsInstance(after, self._getTargetClass()) + self.assertIsInstance(after, self._getTargetClass()) self.assertEqual(list(after), [IFoo, IBar]) def test___add___related_interface(self): @@ -379,7 +382,7 @@ class B(object): self.assertEqual(implementedBy(A), implementedBy(A)) self.assertEqual(hash(implementedBy(A)), hash(implementedBy(A))) self.assertTrue(implementedBy(A) < None) - self.assertTrue(None > implementedBy(A)) + self.assertTrue(None > implementedBy(A)) # pylint:disable=misplaced-comparison-constant self.assertTrue(implementedBy(A) < implementedBy(B)) self.assertTrue(implementedBy(A) > IFoo) self.assertTrue(implementedBy(A) <= implementedBy(B)) @@ -414,7 +417,7 @@ class B(object): # The order of arguments to the operators matters, # test both - self.assertTrue(implementedByA == implementedByA) + self.assertTrue(implementedByA == implementedByA) # pylint:disable=comparison-with-itself self.assertTrue(implementedByA != implementedByB) self.assertTrue(implementedByB != implementedByA) @@ -451,6 +454,7 @@ def test_changed_does_not_add_super_cache(self): class Test_implementedByFallback(unittest.TestCase): def _getTargetClass(self): + # pylint:disable=no-name-in-module from zope.interface.declarations import implementedByFallback return implementedByFallback @@ -527,10 +531,10 @@ def test_builtins_added_to_cache(self): self.assertEqual(list(self._callFUT(dict)), []) for typ in (tuple, list, dict): spec = specs[typ] - self.assertIsInstance(spec, Implements) + self.assertIsInstance(spec, Implements) self.assertEqual(repr(spec), - '' - % (_BUILTINS, typ.__name__)) + '' + % (_BUILTINS, typ.__name__)) def test_builtins_w_existing_cache(self): from zope.interface import declarations @@ -574,9 +578,9 @@ def __call__(self): spec = self._callFUT(foo) self.assertEqual(spec.__name__, 'zope.interface.tests.test_declarations.foo') - self.assertTrue(spec.inherit is foo) - self.assertTrue(foo.__implemented__ is spec) - self.assertTrue(foo.__providedBy__ is objectSpecificationDescriptor) + self.assertIs(spec.inherit, foo) + self.assertIs(foo.__implemented__, spec) + self.assertIs(foo.__providedBy__, objectSpecificationDescriptor) # pylint:disable=no-member self.assertNotIn('__provides__', foo.__dict__) def test_w_None_no_bases_w_class(self): @@ -586,11 +590,11 @@ class Foo(object): spec = self._callFUT(Foo) self.assertEqual(spec.__name__, 'zope.interface.tests.test_declarations.Foo') - self.assertTrue(spec.inherit is Foo) - self.assertTrue(Foo.__implemented__ is spec) - self.assertIsInstance(Foo.__providedBy__, ClassProvides) - self.assertIsInstance(Foo.__provides__, ClassProvides) - self.assertEqual(Foo.__provides__, Foo.__providedBy__) + self.assertIs(spec.inherit, Foo) + self.assertIs(Foo.__implemented__, spec) + self.assertIsInstance(Foo.__providedBy__, ClassProvides) # pylint:disable=no-member + self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member + self.assertEqual(Foo.__provides__, Foo.__providedBy__) # pylint:disable=no-member def test_w_existing_Implements(self): from zope.interface.declarations import Implements @@ -793,14 +797,14 @@ class Foo(object): pass ifoo = InterfaceClass('IFoo') self._callFUT(Foo, ifoo) - spec = Foo.__implemented__ + spec = Foo.__implemented__ # pylint:disable=no-member self.assertEqual(spec.__name__, 'zope.interface.tests.test_declarations.Foo') - self.assertTrue(spec.inherit is None) - self.assertTrue(Foo.__implemented__ is spec) - self.assertIsInstance(Foo.__providedBy__, ClassProvides) - self.assertIsInstance(Foo.__provides__, ClassProvides) - self.assertEqual(Foo.__provides__, Foo.__providedBy__) + self.assertIsNone(spec.inherit) + self.assertIs(Foo.__implemented__, spec) # pylint:disable=no-member + self.assertIsInstance(Foo.__providedBy__, ClassProvides) # pylint:disable=no-member + self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member + self.assertEqual(Foo.__provides__, Foo.__providedBy__) # pylint:disable=no-member def test_w_existing_Implements(self): from zope.interface.declarations import Implements @@ -832,14 +836,14 @@ class Foo(object): pass IFoo = InterfaceClass('IFoo') self._callFUT(Foo, IFoo) - spec = Foo.__implemented__ + spec = Foo.__implemented__ # pylint:disable=no-member self.assertEqual(spec.__name__, 'zope.interface.tests.test_declarations.Foo') - self.assertTrue(spec.inherit is Foo) - self.assertTrue(Foo.__implemented__ is spec) - self.assertIsInstance(Foo.__providedBy__, ClassProvides) - self.assertIsInstance(Foo.__provides__, ClassProvides) - self.assertEqual(Foo.__provides__, Foo.__providedBy__) + self.assertIs(spec.inherit, Foo) + self.assertIs(Foo.__implemented__, spec) # pylint:disable=no-member + self.assertIsInstance(Foo.__providedBy__, ClassProvides) # pylint:disable=no-member + self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member + self.assertEqual(Foo.__provides__, Foo.__providedBy__) # pylint:disable=no-member def test_w_existing_Implements(self): from zope.interface.declarations import Implements @@ -860,25 +864,30 @@ class Foo(object): def test_w_existing_Implements_w_bases(self): from zope.interface.declarations import Implements from zope.interface.interface import InterfaceClass - IFoo = InterfaceClass('IFoo') - IBar = InterfaceClass('IBar') - IBaz = InterfaceClass('IBaz', IFoo) - b_impl = Implements(IBaz) - impl = Implements(IFoo) - impl.declared = (IFoo,) - class Base1(object): - __implemented__ = b_impl - class Base2(object): - __implemented__ = b_impl - class Foo(Base1, Base2): - __implemented__ = impl - impl.inherit = Foo - self._callFUT(Foo, IBar) + IRoot = InterfaceClass('IRoot') + ISecondRoot = InterfaceClass('ISecondRoot') + IExtendsRoot = InterfaceClass('IExtendsRoot', (IRoot,)) + + impl_root = Implements.named('Root', IRoot) + impl_root.declared = (IRoot,) + + class Root1(object): + __implemented__ = impl_root + class Root2(object): + __implemented__ = impl_root + + impl_extends_root = Implements.named('ExtendsRoot1', IExtendsRoot) + impl_extends_root.declared = (IExtendsRoot,) + class ExtendsRoot(Root1, Root2): + __implemented__ = impl_extends_root + impl_extends_root.inherit = ExtendsRoot + + self._callFUT(ExtendsRoot, ISecondRoot) # Same spec, now different values - self.assertTrue(Foo.__implemented__ is impl) - self.assertEqual(impl.inherit, Foo) - self.assertEqual(impl.declared, (IFoo, IBar,)) - self.assertEqual(impl.__bases__, (IFoo, IBar, b_impl)) + self.assertIs(ExtendsRoot.__implemented__, impl_extends_root) + self.assertEqual(impl_extends_root.inherit, ExtendsRoot) + self.assertEqual(impl_extends_root.declared, (IExtendsRoot, ISecondRoot,)) + self.assertEqual(impl_extends_root.__bases__, (IExtendsRoot, ISecondRoot, impl_root)) class Test__implements_advice(unittest.TestCase): @@ -896,8 +905,8 @@ class Foo(object): __implements_advice_data__ = ((IFoo,), classImplements) self._callFUT(Foo) self.assertNotIn('__implements_advice_data__', Foo.__dict__) - self.assertIsInstance(Foo.__implemented__, Implements) - self.assertEqual(list(Foo.__implemented__), [IFoo]) + self.assertIsInstance(Foo.__implemented__, Implements) # pylint:disable=no-member + self.assertEqual(list(Foo.__implemented__), [IFoo]) # pylint:disable=no-member class Test_implementer(unittest.TestCase): @@ -919,14 +928,14 @@ class Foo: decorator = self._makeOne(IFoo) returned = decorator(Foo) self.assertTrue(returned is Foo) - spec = Foo.__implemented__ + spec = Foo.__implemented__ # pylint:disable=no-member self.assertEqual(spec.__name__, 'zope.interface.tests.test_declarations.Foo') - self.assertTrue(spec.inherit is Foo) - self.assertTrue(Foo.__implemented__ is spec) - self.assertIsInstance(Foo.__providedBy__, ClassProvides) - self.assertIsInstance(Foo.__provides__, ClassProvides) - self.assertEqual(Foo.__provides__, Foo.__providedBy__) + self.assertIs(spec.inherit, Foo) + self.assertIs(Foo.__implemented__, spec) # pylint:disable=no-member + self.assertIsInstance(Foo.__providedBy__, ClassProvides) # pylint:disable=no-member + self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member + self.assertEqual(Foo.__provides__, Foo.__providedBy__) # pylint:disable=no-member def test_newstyle_class(self): from zope.interface.declarations import ClassProvides @@ -937,14 +946,14 @@ class Foo(object): decorator = self._makeOne(IFoo) returned = decorator(Foo) self.assertTrue(returned is Foo) - spec = Foo.__implemented__ + spec = Foo.__implemented__ # pylint:disable=no-member self.assertEqual(spec.__name__, 'zope.interface.tests.test_declarations.Foo') - self.assertTrue(spec.inherit is Foo) - self.assertTrue(Foo.__implemented__ is spec) - self.assertIsInstance(Foo.__providedBy__, ClassProvides) - self.assertIsInstance(Foo.__provides__, ClassProvides) - self.assertEqual(Foo.__provides__, Foo.__providedBy__) + self.assertIs(spec.inherit, Foo) + self.assertIs(Foo.__implemented__, spec) # pylint:disable=no-member + self.assertIsInstance(Foo.__providedBy__, ClassProvides) # pylint:disable=no-member + self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member + self.assertEqual(Foo.__provides__, Foo.__providedBy__) # pylint:disable=no-member def test_nonclass_cannot_assign_attr(self): from zope.interface.interface import InterfaceClass @@ -961,10 +970,10 @@ class Foo(object): decorator = self._makeOne(IFoo) returned = decorator(foo) self.assertTrue(returned is foo) - spec = foo.__implemented__ + spec = foo.__implemented__ # pylint:disable=no-member self.assertEqual(spec.__name__, 'zope.interface.tests.test_declarations.?') - self.assertTrue(spec.inherit is None) - self.assertTrue(foo.__implemented__ is spec) + self.assertIsNone(spec.inherit,) + self.assertIs(foo.__implemented__, spec) # pylint:disable=no-member class Test_implementer_only(unittest.TestCase): @@ -989,7 +998,7 @@ def test_method(self): IFoo = InterfaceClass('IFoo') decorator = self._makeOne(IFoo) class Bar: - def _method(): + def _method(self): raise NotImplementedError() self.assertRaises(ValueError, decorator, Bar._method) @@ -1034,7 +1043,6 @@ class Test_implementsOnly(unittest.TestCase, _Py3ClassAdvice): def test_simple(self): import warnings from zope.interface.declarations import implementsOnly - from zope.interface._compat import PYTHON3 from zope.interface.interface import InterfaceClass IFoo = InterfaceClass("IFoo") globs = {'implementsOnly': implementsOnly, @@ -1048,7 +1056,7 @@ def test_simple(self): with warnings.catch_warnings(record=True) as log: warnings.resetwarnings() try: - exec(CODE, globs, locs) + exec(CODE, globs, locs) # pylint:disable=exec-used except TypeError: self.assertTrue(PYTHON3, "Must be Python 3") else: @@ -1107,7 +1115,6 @@ def test_called_twice_from_class(self): import warnings from zope.interface.declarations import implements from zope.interface.interface import InterfaceClass - from zope.interface._compat import PYTHON3 IFoo = InterfaceClass("IFoo") IBar = InterfaceClass("IBar") globs = {'implements': implements, 'IFoo': IFoo, 'IBar': IBar} @@ -1120,7 +1127,7 @@ def test_called_twice_from_class(self): with warnings.catch_warnings(record=True) as log: warnings.resetwarnings() try: - exec(CODE, globs, locs) + exec(CODE, globs, locs) # pylint:disable=exec-used except TypeError: if not PYTHON3: self.assertEqual(len(log), 0) # no longer warn @@ -1245,8 +1252,8 @@ class Foo(object): pass obj = Foo() self._callFUT(obj, IFoo) - self.assertIsInstance(obj.__provides__, ProvidesClass) - self.assertEqual(list(obj.__provides__), [IFoo]) + self.assertIsInstance(obj.__provides__, ProvidesClass) # pylint:disable=no-member + self.assertEqual(list(obj.__provides__), [IFoo]) # pylint:disable=no-member def test_w_class(self): from zope.interface.declarations import ClassProvides @@ -1255,8 +1262,8 @@ def test_w_class(self): class Foo(object): pass self._callFUT(Foo, IFoo) - self.assertIsInstance(Foo.__provides__, ClassProvides) - self.assertEqual(list(Foo.__provides__), [IFoo]) + self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member + self.assertEqual(list(Foo.__provides__), [IFoo]) # pylint:disable=no-member @_skip_under_py3k def test_w_non_descriptor_aware_metaclass(self): @@ -1292,7 +1299,7 @@ def __setattr__(self, name, value): the_dict[name] = value obj = Foo() self._callFUT(obj, IFoo) - self.assertIsInstance(the_dict['__provides__'], ProvidesClass) + self.assertIsInstance(the_dict['__provides__'], ProvidesClass) self.assertEqual(list(the_dict['__provides__']), [IFoo]) @@ -1310,8 +1317,8 @@ class Foo(object): pass obj = Foo() self._callFUT(obj, IFoo) - self.assertIsInstance(obj.__provides__, ProvidesClass) - self.assertEqual(list(obj.__provides__), [IFoo]) + self.assertIsInstance(obj.__provides__, ProvidesClass) # pylint:disable=no-member + self.assertEqual(list(obj.__provides__), [IFoo]) # pylint:disable=no-member def test_w_existing_provides(self): from zope.interface.declarations import directlyProvides @@ -1324,8 +1331,8 @@ class Foo(object): obj = Foo() directlyProvides(obj, IFoo) self._callFUT(obj, IBar) - self.assertIsInstance(obj.__provides__, ProvidesClass) - self.assertEqual(list(obj.__provides__), [IFoo, IBar]) + self.assertIsInstance(obj.__provides__, ProvidesClass) # pylint:disable=no-member + self.assertEqual(list(obj.__provides__), [IFoo, IBar]) # pylint:disable=no-member class Test_noLongerProvides(unittest.TestCase): @@ -1341,7 +1348,7 @@ class Foo(object): pass obj = Foo() self._callFUT(obj, IFoo) - self.assertEqual(list(obj.__provides__), []) + self.assertEqual(list(obj.__provides__), []) # pylint:disable=no-member def test_w_existing_provides_hit(self): from zope.interface.declarations import directlyProvides @@ -1352,7 +1359,7 @@ class Foo(object): obj = Foo() directlyProvides(obj, IFoo) self._callFUT(obj, IFoo) - self.assertEqual(list(obj.__provides__), []) + self.assertEqual(list(obj.__provides__), []) # pylint:disable=no-member def test_w_existing_provides_miss(self): from zope.interface.declarations import directlyProvides @@ -1364,7 +1371,7 @@ class Foo(object): obj = Foo() directlyProvides(obj, IFoo) self._callFUT(obj, IBar) - self.assertEqual(list(obj.__provides__), [IFoo]) + self.assertEqual(list(obj.__provides__), [IFoo]) # pylint:disable=no-member def test_w_iface_implemented_by_class(self): from zope.interface.declarations import implementer @@ -1380,6 +1387,7 @@ class Foo(object): class ClassProvidesBaseFallbackTests(unittest.TestCase): def _getTargetClass(self): + # pylint:disable=no-name-in-module from zope.interface.declarations import ClassProvidesBaseFallback return ClassProvidesBaseFallback @@ -1406,8 +1414,8 @@ def test_w_same_class_via_instance(self): class Foo(object): pass foo = Foo() - cpbp = Foo.__provides__ = self._makeOne(Foo, IFoo) - self.assertTrue(foo.__provides__ is IFoo) + Foo.__provides__ = self._makeOne(Foo, IFoo) + self.assertIs(foo.__provides__, IFoo) def test_w_different_class(self): from zope.interface.interface import InterfaceClass @@ -1417,7 +1425,7 @@ class Foo(object): class Bar(Foo): pass bar = Bar() - cpbp = Foo.__provides__ = self._makeOne(Foo, IFoo) + Foo.__provides__ = self._makeOne(Foo, IFoo) self.assertRaises(AttributeError, getattr, Bar, '__provides__') self.assertRaises(AttributeError, getattr, bar, '__provides__') @@ -1431,6 +1439,7 @@ def _getTargetClass(self): return ClassProvidesBase def _getFallbackClass(self): + # pylint:disable=no-name-in-module from zope.interface.declarations import ClassProvidesBaseFallback return ClassProvidesBaseFallback @@ -1522,12 +1531,12 @@ class Foo(object): class Test_classProvides(unittest.TestCase, _Py3ClassAdvice): + # pylint:disable=exec-used def test_called_from_function(self): import warnings from zope.interface.declarations import classProvides from zope.interface.interface import InterfaceClass - from zope.interface._compat import PYTHON3 IFoo = InterfaceClass("IFoo") globs = {'classProvides': classProvides, 'IFoo': IFoo} locs = {} @@ -1547,7 +1556,6 @@ def test_called_twice_from_class(self): import warnings from zope.interface.declarations import classProvides from zope.interface.interface import InterfaceClass - from zope.interface._compat import PYTHON3 IFoo = InterfaceClass("IFoo") IBar = InterfaceClass("IBar") globs = {'classProvides': classProvides, 'IFoo': IFoo, 'IBar': IBar} @@ -1601,11 +1609,12 @@ def test_w_class(self): @self._makeOne(IFoo) class Foo(object): pass - self.assertIsInstance(Foo.__provides__, ClassProvides) - self.assertEqual(list(Foo.__provides__), [IFoo]) + self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member + self.assertEqual(list(Foo.__provides__), [IFoo]) # pylint:disable=no-member class Test_moduleProvides(unittest.TestCase): + # pylint:disable=exec-used def test_called_from_function(self): from zope.interface.declarations import moduleProvides @@ -1655,7 +1664,7 @@ def test_called_twice_from_module_scope(self): IFoo = InterfaceClass("IFoo") globs = {'__name__': 'zope.interface.tests.foo', 'moduleProvides': moduleProvides, 'IFoo': IFoo} - locs = {} + CODE = "\n".join([ 'moduleProvides(IFoo)', 'moduleProvides(IFoo)', @@ -1667,6 +1676,7 @@ def test_called_twice_from_module_scope(self): class Test_getObjectSpecificationFallback(unittest.TestCase): def _getFallbackClass(self): + # pylint:disable=no-name-in-module from zope.interface.declarations import getObjectSpecificationFallback return getObjectSpecificationFallback @@ -1700,7 +1710,7 @@ def foo(): raise NotImplementedError() directlyProvides(foo, IFoo) spec = self._callFUT(foo) - self.assertTrue(spec is foo.__provides__) + self.assertIs(spec, foo.__provides__) # pylint:disable=no-member def test_existing_provides_is_not_spec(self): def foo(): @@ -1738,6 +1748,38 @@ class Foo(object): spec = self._callFUT(foo) self.assertEqual(list(spec), []) + def test_catches_only_AttributeError_on_provides(self): + MissingSomeAttrs.test_raises(self, self._callFUT, expected_missing='__provides__') + + def test_catches_only_AttributeError_on_class(self): + MissingSomeAttrs.test_raises(self, self._callFUT, expected_missing='__class__', + __provides__=None) + + def test_raises_AttributeError_when_provides_fails_type_check_AttributeError(self): + # isinstance(ob.__provides__, SpecificationBase) is not + # protected inside any kind of block. + + class Foo(object): + __provides__ = MissingSomeAttrs(AttributeError) + + # isinstance() ignores AttributeError on __class__ + self._callFUT(Foo()) + + def test_raises_AttributeError_when_provides_fails_type_check_RuntimeError(self): + # isinstance(ob.__provides__, SpecificationBase) is not + # protected inside any kind of block. + class Foo(object): + __provides__ = MissingSomeAttrs(RuntimeError) + + if PYTHON3: + with self.assertRaises(RuntimeError) as exc: + self._callFUT(Foo()) + + self.assertEqual('__class__', exc.exception.args[0]) + else: + # Python 2 catches everything. + self._callFUT(Foo()) + class Test_getObjectSpecification(Test_getObjectSpecificationFallback, OptimizationTestMixin): @@ -1751,6 +1793,7 @@ def _getTargetClass(self): class Test_providedByFallback(unittest.TestCase): def _getFallbackClass(self): + # pylint:disable=no-name-in-module from zope.interface.declarations import providedByFallback return providedByFallback @@ -1978,6 +2021,19 @@ class Derived(M1, M2): self.assertEqual(list(self._callFUT(sm2)), [IBase]) + def test_catches_only_AttributeError_on_providedBy(self): + MissingSomeAttrs.test_raises(self, self._callFUT, + expected_missing='__providedBy__', + __class__=object) + + def test_catches_only_AttributeError_on_class(self): + # isinstance() tries to get the __class__, which is non-obvious, + # so it must be protected too. + PY3 = str is not bytes + MissingSomeAttrs.test_raises(self, self._callFUT, + expected_missing='__class__' if PY3 else '__providedBy__') + + class Test_providedBy(Test_providedByFallback, OptimizationTestMixin): @@ -1991,6 +2047,7 @@ def _getTargetClass(self): class ObjectSpecificationDescriptorFallbackTests(unittest.TestCase): def _getFallbackClass(self): + # pylint:disable=no-name-in-module from zope.interface.declarations \ import ObjectSpecificationDescriptorFallback return ObjectSpecificationDescriptorFallback @@ -2059,7 +2116,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_interface.py b/src/zope/interface/tests/test_interface.py index 7b7b7e8d..4bbed1a7 100644 --- a/src/zope/interface/tests/test_interface.py +++ b/src/zope/interface/tests/test_interface.py @@ -24,6 +24,7 @@ import unittest from zope.interface._compat import _skip_under_py3k +from zope.interface.tests import MissingSomeAttrs from zope.interface.tests import OptimizationTestMixin from zope.interface.tests import CleanUp @@ -396,8 +397,13 @@ def test___call___wo___conform___ob_no_provides_w_alternate(self): def test___call___w___conform___ob_no_provides_wo_alternate(self): ib = self._makeOne(False) - adapted = object() - self.assertRaises(TypeError, ib, adapted) + with self.assertRaises(TypeError) as exc: + ib(object()) + + self.assertIn('Could not adapt', str(exc.exception)) + + def test___call___w_no_conform_catches_only_AttributeError(self): + MissingSomeAttrs.test_raises(self, self._makeOne(), expected_missing='__conform__') class InterfaceBaseTests(InterfaceBaseTestsMixin, @@ -1312,9 +1318,9 @@ def testInterfaceExtendsInterface(self): new = Interface.__class__ FunInterface = new('FunInterface') - BarInterface = new('BarInterface', [FunInterface]) + BarInterface = new('BarInterface', (FunInterface,)) BobInterface = new('BobInterface') - BazInterface = new('BazInterface', [BobInterface, BarInterface]) + BazInterface = new('BazInterface', (BobInterface, BarInterface,)) self.assertTrue(BazInterface.extends(BobInterface)) self.assertTrue(BazInterface.extends(BarInterface)) @@ -2155,6 +2161,128 @@ class C(object): finally: adapter_hooks[:] = old_adapter_hooks + def test___call___w_overridden_adapt(self): + from zope.interface import Interface + from zope.interface import interfacemethod + from zope.interface import implementer + + class I(Interface): + + @interfacemethod + def __adapt__(self, obj): + return 42 + + @implementer(I) + class O(object): + pass + + self.assertEqual(42, I(object())) + # __adapt__ supercedes providedBy() if defined. + self.assertEqual(42, I(O())) + + def test___call___w_overridden_adapt_call_super(self): + import sys + from zope.interface import Interface + from zope.interface import interfacemethod + from zope.interface import implementer + + class I(Interface): + + @interfacemethod + def __adapt__(self, obj): + if not self.providedBy(obj): + return 42 + if sys.version_info[:2] > (3, 5): + # Python 3.5 raises 'RuntimeError: super() __class__ is not a type' + return super().__adapt__(obj) + + return super(type(I), self).__adapt__(obj) + + @implementer(I) + class O(object): + pass + + self.assertEqual(42, I(object())) + o = O() + self.assertIs(o, I(o)) + + def test___adapt___as_method_and_implementation(self): + from zope.interface import Interface + from zope.interface import interfacemethod + + class I(Interface): + @interfacemethod + def __adapt__(self, obj): + return 42 + + def __adapt__(to_adapt): + "This is a protocol" + + self.assertEqual(42, I(object())) + self.assertEqual(I['__adapt__'].getSignatureString(), '(to_adapt)') + + def test___adapt__inheritance_and_type(self): + from zope.interface import Interface + from zope.interface import interfacemethod + + class IRoot(Interface): + """Root""" + + class IWithAdapt(IRoot): + @interfacemethod + def __adapt__(self, obj): + return 42 + + class IOther(IRoot): + """Second branch""" + + class IUnrelated(Interface): + """Unrelated""" + + class IDerivedAdapt(IUnrelated, IWithAdapt, IOther): + """Inherits an adapt""" + # Order of "inheritance" matters here. + + class IDerived2Adapt(IDerivedAdapt): + """Overrides an inherited custom adapt.""" + @interfacemethod + def __adapt__(self, obj): + return 24 + + self.assertEqual(42, IDerivedAdapt(object())) + for iface in IRoot, IWithAdapt, IOther, IUnrelated, IDerivedAdapt: + self.assertEqual(__name__, iface.__module__) + + for iface in IRoot, IOther, IUnrelated: + self.assertEqual(type(IRoot), type(Interface)) + + # But things that implemented __adapt__ got a new type + self.assertNotEqual(type(Interface), type(IWithAdapt)) + self.assertEqual(type(IWithAdapt), type(IDerivedAdapt)) + self.assertIsInstance(IWithAdapt, type(Interface)) + + self.assertEqual(24, IDerived2Adapt(object())) + self.assertNotEqual(type(IDerived2Adapt), type(IDerivedAdapt)) + self.assertIsInstance(IDerived2Adapt, type(IDerivedAdapt)) + + def test_interfacemethod_is_general(self): + from zope.interface import Interface + from zope.interface import interfacemethod + + class I(Interface): + + @interfacemethod + def __call__(self, obj): + """Replace an existing method""" + return 42 + + @interfacemethod + def this_is_new(self): + return 42 + + self.assertEqual(I(self), 42) + self.assertEqual(I.this_is_new(), 42) + class AttributeTests(ElementTests):