zope.component.hooks fails to completely cleanup during tests, causing bad adapter registrations

Bug #1100501 reported by Jason Madden
6
This bug affects 1 person
Affects Status Importance Assigned to Milestone
zope.component
Fix Released
Undecided
Unassigned

Bug Description

When zope.component.hooks (zope.component 4.0.2) is loaded, it installs a zope.testing
cleanup function. However, this cleanup function isn't adequate and
leaves stale data around, leading zope.interface adapters to
improperly adapt things (a particular problem when running test suites
that load different adapter configurations) This problem goes away if zope.site.site is
imported and installs its cleanup hooks.

Demonstration
=============

The easiest way I know to demonstrate this is with some code, presented in doctest form.

First we import the basic modules::

 >>> import pkg_resources # to make sure namespace imports work
 >>> from zope.testing import cleanup
 >>> from zope import interface
 >>> from zope import component
 >>> from zope.component import hooks

Next, we define an interface and two different adapters::

 >>> class II(interface.Interface): pass
 >>> class O(object): pass
 >>> class A1(object):
 ... def __init__( self, *args):
 ... pass
 ... def __repr__( self ):
 ... return "<A1>"
 >>> class A2(object):
 ... def __init__( self, *args):
 ... pass
 ... def __repr__( self ):
 ... return "<A2>"

We can now proceed to run our "unit tests". Each of these unit tests
will begin by installing the component hooks and providing some base
configuration in the form of registering the A1 adapter::

 >>> hooks.setHooks()
 >>> component.provideAdapter( A1, adapts=(O,), provides=II )

If we do nothing further, we can get get back the A1 adapter when we
ask for it from both zope.component, and thanks to the adapter hook, zope.interface::

 >>> component.getAdapter( O(), II )
 <A1>
 >>> II( O() )
 <A1>

Lets suppose some tests start with the base configuration and override
it, installing the second adapter::

 >>> component.provideAdapter( A2, adapts=(O,), provides=II )

This adapter can now be accessed in both of those places::

 >>> component.getAdapter( O(), II )
 <A2>
 >>> II( O() )
 <A2>

Finally, our test case shuts down and the cleanup is run::

 >>> cleanup.cleanUp()

To demonstrate the problem, let's begin the next test case run with the
same basic setup as before::

 >>> hooks.setHooks()
 >>> component.provideAdapter( A1, adapts=(O,), provides=II )

At this point, we would expect to get back A1 when we ask for
adapters. If we ask the global site manager directly for it, we're
alright::

 >>> component.getGlobalSiteManager().queryAdapter( O(), II )
 <A1>

But if we ask the (hooked) global API, we have a problem::

 >>> component.queryAdapter( O(), II )
 <A2>

And we haze the same problem if we ask zope.interface::

 >>> II( O() )
 <A2>

You can see that we get back the A2 registration, which should no
longer be here, as the cleanup hooks have run. zope.interface's
cleanup hooks reset the entire global registry, in fact, by re-running
its __init__ method. What's causing this?

A clue comes in the form of realizing that this doesn't happen all the
time. In fact, as soon as zope.site.site is imported, it no longer
happens at all::

 >>> from zope.site import site

zope.site.site installs a cleanup function that calls
zope.component.hooks.setSite to clear the site::

 >>> cleanup.cleanUp()

Now the next time a test runs, the ancient A2 registration is truly gone::

 >>> hooks.setHooks()
 >>> component.provideAdapter( A1, adapts=(O,), provides=II )

both from zope.component::

 >>> component.queryAdapter( O(), II )
 <A1>

and zope.interface::

 >>> II( O() )
 <A1>

Analysis
========

In a nutshell, what appears to be happening is that
zope.component.hooks.SiteInfo caches the adapter_hook of the current
site manager's `adapters` property the first time it is accessed.
However, the cleanup for the globalSiteManager completely *replaces*
its `adapters` property with a new object, leaving SiteInfo holding a
dangling reference to an adapter registry that is no longer installed
anywhere. Importing zope.site.site causes the
zope.component.hooks.setSite to be called to clear out the site, which
causes the cached adapter_hook to be deleted, thus letting it get a
new reference to the current adapter registry.

Perhaps the zope.component.hooks cleanup function should also clear the site? Or perhaps it should simply clear the adapter_hook cache?

Revision history for this message
Jason Madden (jamadden) wrote :

Transfered to GitHub to follow the code: https://github.com/zopefoundation/zope.component/pull/1

Revision history for this message
Tres Seaver (tseaver) wrote :
Changed in zope.component:
status: New → Fix Released
To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.