Skip to content

Latest commit

 

History

History
380 lines (308 loc) · 9.71 KB

specifications.rst

File metadata and controls

380 lines (308 loc) · 9.71 KB

Interface Specifications

.. currentmodule:: zope.interface.interfaces


This document discusses the actual interface objects themselves. We begin with a basic concept of specifying an object's behaviour (with an ISpecification), and then we describe the way we write such a specification (IInterface). Combinations of specifications (e.g., an object that provides multiple interfaces) are covered by IDeclaration.

Specification

Specification objects implement the API defined by :class:`ISpecification`:

.. autointerface:: ISpecification
   :members:
   :member-order: bysource

.. autoclass:: zope.interface.interface.Specification
   :no-members:

For example:

>>> from zope.interface.interface import Specification
>>> from zope.interface import Interface
>>> class I1(Interface):
...     pass
>>> class I2(I1):
...     pass
>>> class I3(I2):
...     pass
>>> [i.__name__ for i in I1.__bases__]
['Interface']
>>> [i.__name__ for i in I2.__bases__]
['I1']
>>> I3.extends(I1)
True
>>> I2.__bases__ = (Interface, )
>>> [i.__name__ for i in I2.__bases__]
['Interface']
>>> I3.extends(I1)
False

Exmples for :meth:`.Specification.providedBy`:

>>> from zope.interface import *
>>> class I1(Interface):
...     pass
>>> @implementer(I1)
... class C(object):
...     pass
>>> c = C()
>>> class X(object):
...     pass
>>> x = X()
>>> I1.providedBy(x)
False
>>> I1.providedBy(C)
False
>>> I1.providedBy(c)
True
>>> directlyProvides(x, I1)
>>> I1.providedBy(x)
True
>>> directlyProvides(C, I1)
>>> I1.providedBy(C)
True

Examples for :meth:`.Specification.isOrExtends`:

>>> from zope.interface import Interface
>>> from zope.interface.declarations import Declaration
>>> class I1(Interface): pass
...
>>> class I2(I1): pass
...
>>> class I3(Interface): pass
...
>>> class I4(I3): pass
...
>>> spec = Declaration()
>>> int(spec.extends(Interface))
1
>>> spec = Declaration(I2)
>>> int(spec.extends(Interface))
1
>>> int(spec.extends(I1))
1
>>> int(spec.extends(I2))
1
>>> int(spec.extends(I3))
0
>>> int(spec.extends(I4))
0

Examples for :meth:`.Specification.interfaces`:

>>> from zope.interface import Interface
>>> class I1(Interface): pass
...
>>> class I2(I1): pass
...
>>> class I3(Interface): pass
...
>>> class I4(I3): pass
...
>>> spec = Specification((I2, I3))
>>> spec = Specification((I4, spec))
>>> i = spec.interfaces()
>>> [x.getName() for x in i]
['I4', 'I2', 'I3']
>>> list(i)
[]

Exmples for :meth:`.Specification.extends`:

>>> from zope.interface import Interface
>>> from zope.interface.declarations import Declaration
>>> class I1(Interface): pass
...
>>> class I2(I1): pass
...
>>> class I3(Interface): pass
...
>>> class I4(I3): pass
...
>>> spec = Declaration()
>>> int(spec.extends(Interface))
1
>>> spec = Declaration(I2)
>>> int(spec.extends(Interface))
1
>>> int(spec.extends(I1))
1
>>> int(spec.extends(I2))
1
>>> int(spec.extends(I3))
0
>>> int(spec.extends(I4))
0
>>> I2.extends(I2)
False
>>> I2.extends(I2, False)
True
>>> I2.extends(I2, strict=False)
True

Equality, Hashing, and Comparisons

Specifications (including their notable subclass Interface), are hashed and compared (sorted) based solely on their __name__ and __module__, not including any information about their enclosing scope, if any (e.g., their __qualname__). This means that any two objects created with the same name and module are considered equal and map to the same value in a dictionary.

>>> from zope.interface import Interface
>>> class I1(Interface): pass
>>> orig_I1 = I1
>>> class I1(Interface): pass
>>> I1 is orig_I1
False
>>> I1 == orig_I1
True
>>> d = {I1: 42}
>>> d[orig_I1]
42
>>> def make_nested():
...     class I1(Interface): pass
...     return I1
>>> nested_I1 = make_nested()
>>> I1 == orig_I1 == nested_I1
True

Caveats

While this behaviour works will with :ref:`pickling (persistence) <global_persistence>`, it has some potential downsides to be aware of.

Weak References

The first downside involves weak references. Because weak references hash the same as their underlying object, this can lead to surprising results when weak references are involved, especially if there are cycles involved or if the garbage collector is not based on reference counting (e.g., PyPy). For example, if you redefine an interface named the same as an interface being used in a WeakKeyDictionary, you can get a KeyError, even if you put the new interface into the dictionary.

>>> from zope.interface import Interface
>>> import gc
>>> from weakref import WeakKeyDictionary
>>> wr_dict = WeakKeyDictionary()
>>> class I1(Interface): pass
>>> wr_dict[I1] = 42
>>> orig_I1 = I1 # Make sure it stays alive
>>> class I1(Interface): pass
>>> wr_dict[I1] = 2020
>>> del orig_I1
>>> _ = gc.collect() # Sometime later, gc runs and makes sure the original is gone
>>> wr_dict[I1] # Cleaning up the original weakref removed the new one
Traceback (most recent call last):
...
KeyError: ...

This is mostly likely a problem in test cases where it is tempting to use the same named interfaces in different test methods. If references to them escape, especially if they are used as the bases of other interfaces, you may find surprising KeyError exceptions. For this reason, it is best to use distinct names for local interfaces within the same test module.

Providing Dynamic Interfaces

If you return an interface created inside a function or method, or otherwise let it escape outside the bounds of that function (such as by having an object provide it), it's important to be aware that it will compare and hash equal to any other interface defined in that same module with the same name. This includes interface objects created by other invocations of that function.

This can lead to surprising results when querying against those interfaces. We can demonstrate by creating a module-level interface with a common name, and checking that it is provided by an object:

>>> from zope.interface import Interface, alsoProvides, providedBy
>>> class ICommon(Interface):
...     pass
>>> class Obj(object):
...     pass
>>> obj = Obj()
>>> alsoProvides(obj, ICommon)
>>> len(list(providedBy(obj)))
1
>>> ICommon.providedBy(obj)
True

Next, in the same module, we will define a function that dynamically creates an interface of the same name and adds it to an object.

>>> def add_interfaces(obj):
...     class ICommon(Interface):
...         pass
...     class I2(Interface):
...         pass
...     alsoProvides(obj, ICommon, I2)
...     return ICommon
...
>>> dynamic_ICommon = add_interfaces(obj)

The two instances are not identical, but they are equal, and obj provides them both:

>>> ICommon is dynamic_ICommon
False
>>> ICommon == dynamic_ICommon
True
>>> ICommon.providedBy(obj)
True
>>> dynamic_ICommon.providedBy(obj)
True

At this point, we've effectively called alsoProvides(obj, ICommon, dynamic_ICommon, I2), where the last two interfaces were locally defined in the function. So checking how many interfaces obj now provides should return three, right?

>>> len(list(providedBy(obj)))
2

Because ICommon == dynamic_ICommon due to having the same __name__ and __module__, only one of them is actually provided by the object, for a total of two provided interfaces. (Exactly which one is undefined.) Likewise, if we run the same function again, obj will still only provide two interfaces

>>> _ = add_interfaces(obj)
>>> len(list(providedBy(obj)))
2

Interface

Interfaces are a particular type of ISpecification and implement the API defined by :class:`IInterface`.

Before we get there, we need to discuss two related concepts. The first is that of an "element", which provides us a simple way to query for information generically (this is important because we'll see that IInterface implements this interface):

.. autointerface:: IElement

   Objects that have basic documentation and tagged values.

   Known derivatives include :class:`IAttribute` and its derivative
   :class:`IMethod`; these have no notion of inheritance.
   :class:`IInterface` is also a derivative, and it does have a
   notion of inheritance, expressed through its ``__bases__`` and
   ordered in its ``__iro__`` (both defined by
   :class:`ISpecification`).


.. autoclass:: zope.interface.interface.Element
   :no-members:

Next, we look at IAttribute and IMethod. These make up the content, or body, of an Interface.

.. autointerface:: zope.interface.interfaces.IAttribute
.. autoclass:: zope.interface.interface.Attribute
   :no-members:

.. autointerface:: IMethod
.. autoclass:: zope.interface.interface.Method
   :no-members:

Finally we can look at the definition of IInterface.

.. autointerface:: IInterface

.. autointerface:: zope.interface.Interface

Usage

Exmples for :meth:`InterfaceClass.extends`:

>>> from zope.interface import Interface
>>> class I1(Interface): pass
...
>>>
>>> i = I1.interfaces()
>>> [x.getName() for x in i]
['I1']
>>> list(i)
[]