Add info on the interaction of weakrefs and interface hashing.

This commit is contained in:
Jason Madden 2020-09-30 10:40:14 -05:00
parent b749fc0f17
commit 8cf31ebd6b
No known key found for this signature in database
GPG Key ID: 349F84431A08B99E
2 changed files with 85 additions and 0 deletions

View File

@ -19,6 +19,27 @@
garbage collection times. See `issue 216
<https://github.com/zopefoundation/zope.interface/issues/216>`_.
.. caution::
This leak could prevent interfaces used as the bases of
other interfaces from being garbage collected. Those interfaces
will now be collected.
One way in which this would manifest was that ``weakref.ref``
objects (and things built upon them, like
``Weak[Key|Value]Dictionary``) would continue to have access to
the original object even if there were no other visible
references to Python and the original object *should* have been
collected. This could be especially problematic for the
``WeakKeyDictionary`` when combined with dynamic or local
(created in the scope of a function) interfaces, since interfaces
are hashed based just on their name and module name. See the
linked issue for an example of a resulting ``KeyError``.
Note that such potential errors are not new, they are just once
again a possibility.
5.1.0 (2020-04-08)
==================

View File

@ -161,6 +161,70 @@ Exmples for :meth:`.Specification.extends`:
>>> I2.extends(I2, strict=False)
True
Equality, Hashing, and Comparisons
----------------------------------
Specifications (including their notable subclass `Interface`), are
hashed and compared 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.
.. doctest::
>>> 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
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.
.. doctest::
>>> 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.
Interface
=========