Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make PersistentAdapterRegistry use PersistentMapping and PersistentList instead of dict and tuple #51

Closed
jamadden opened this issue Mar 16, 2021 · 7 comments · Fixed by #53

Comments

@jamadden
Copy link
Member

PersistentComponents already uses PersistentMapping and PersistentList for its internal structures:

def _init_registrations(self):
self._utility_registrations = PersistentMapping()
self._adapter_registrations = PersistentMapping()
self._subscription_registrations = PersistentList()
self._handler_registrations = PersistentList()

Beginning with zopefoundation/zope.interface#228 in what will be zope.interface 5.3, it's possible to control the data structures an adapter registry uses.

I think it would be good to make the PersistentAdapterRegistry use these same data structures. This way, when a registry is unpickled, you don't have to also create ghosts for all the components that have been registered/subscribed to it, and when a registry is pickled, you don't have to write a huge object tree, only the parts that have been changed. This makes it much for scalable. (BTrees are even more scalable, but may not support certain use cases.)

Doing this for new objects is easy, it's just a matter of setting certain fields in the class (and implementing two trivial methods).

For existing objects, though, it will take a bit more care because the internal mapping objects are mutated in place. This means we have two cases to consider:

  • An old dict holding a reference to a new PersistentMapping which is mutated. This one should be easy, and nothing should need done: mutating a PersistentMapping will automatically persist the PersistentMapping.
  • A new PersistentMapping holding a reference to an old dict which is mutated. That would be bad, because mutating the dict wouldn't know that the persistent parent needs to be saved. I'm not seeing how we could actually get into that state, though. By definition, a new PersistentMapping will only have other new PersistentMapping objects as values.

There is a rebuild() method that can re-create the entire data structures based on the new types; for those wishing to do a complete migration, that would be an effective way to minimize what needs pickled in future changes.

One more wrinkle: Right now, because the old data structures were non-persistent, making any change (registering, unregistering, etc) causes the PersistentAdapterRegistry to mark itself as changed:

def changed(self, originally_changed):
if originally_changed is self:
self._p_changed = True
super(PersistentAdapterRegistry, self).changed(originally_changed)

In a new object, or one that has been rebuild() and hence consists of only persistent data structures, this isn't necessary. It would be a nice optimization to handle those cases.

@jamadden
Copy link
Member Author

There's a large test suite for much of this in zope.interface.tests.test_adapter:CustomTypesBaseAdapterRegistryTests which I'd like to reuse.

I can think of one example where this is done, and that's ZODB: The RelStorage tests extend the ZODB tests.

This would probably at least need a comment in test_adapter.py that this is being done.

Is there a better way?

@jamadden
Copy link
Member Author

In a new object, or one that has been rebuild() and hence consists of only persistent data structures, this isn't necessary. It would be a nice optimization to handle those cases.

Scratch that. Because the _generation attribute changes each time we get changed anyway, there's nothing to optimize.

@agroszer
Copy link
Contributor

agroszer commented Jul 5, 2021

Hey @jamadden

Seems like migrating old tuples breaks:

  File "/ve/lib/python3.9/site-packages/zope/interface/registry.py", line 272, in unregisterUtility
    self._utility_registrations_cache.unregisterUtility(
  File "/ve/lib/python3.9/site-packages/zope/interface/registry.py", line 143, in unregisterUtility
    self._utilities.unsubscribe((), provided, component)
  File "/ve/lib/python3.9/site-packages/zope/interface/adapter.py", line 501, in unsubscribe
    new = self._removeValueFromLeaf(old, value)
  File "/ve/lib/python3.9/site-packages/zope/component/persistentregistry.py", line 75, in _removeValueFromLeaf
    existing_leaf_sequence[:] = without_removed
TypeError: 'tuple' object does not support item assignment

@agroszer
Copy link
Contributor

agroszer commented Jul 5, 2021

Is that rebuild() LocalSiteManager.rebuildUtilityRegistryFromLocalCache() ?

@agroszer
Copy link
Contributor

agroszer commented Jul 8, 2021

@jamadden 👋

@agroszer agroszer reopened this Jul 8, 2021
@jamadden
Copy link
Member Author

jamadden commented Jul 9, 2021

Sorry, I'm way behind on my notifications.

Yes, rebuild() ought to take care of the issue, but the intent was to migrate transparently. I guess the scenario where you unsubscribe something first without subscribing something is untested and thus broken. It should be easy to fix.

@jamadden
Copy link
Member Author

jamadden commented Jul 9, 2021

Is that rebuild() LocalSiteManager.rebuildUtilityRegistryFromLocalCache() ?

No, it's BaseAdapterRegistry.rebuild(), e.g., getSiteManager().utilities.rebuild().

Components.rebuildUtilityRegistryFromLocalCache solves corruption issues where the high-level internal data structures are functional, but out of sync with each other (.utilities doesn't match the data in ._utility_registrations). This should only happen because of bugs in (subclasses of) Components.

BaseAdapterRegistry.rebuild re-creates the lower-level data structures themselves, typically to transform them to a new type.

mauritsvanrees added a commit to plone/plone.app.users that referenced this issue Mar 8, 2022
Same test failure:

```
File "/Users/maurits/community/plone-coredev/6.0/src/plone.app.users/plone/app/users/tests/password.rst", line 27, in password.rst
Failed example:
    browser.open('http://nohost/plone/' + view_name)
Expected:
    Traceback (most recent call last):
    ...
    zExceptions.unauthorized.Unauthorized: ...You are not authorized to access this resource...
Got:
    Traceback (most recent call last):
      File "/Users/maurits/.pyenv/versions/3.9.9/lib/python3.9/doctest.py", line 1334, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest password.rst[8]>", line 1, in <module>
        browser.open('http://nohost/plone/' + view_name)
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 256, in open
        self._processRequest(url, make_request)
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 282, in _processRequest
        resp = make_request(reqargs)
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 253, in make_request
        return self.testapp.get(url, **args)
      File "/Users/maurits/shared-eggs/cp39/WebTest-3.0.0-py3.9.egg/webtest/app.py", line 324, in get
        return self.do_request(req, status=status,
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 93, in do_request
        response = super(TestbrowserApp, self).do_request(req, status,
      File "/Users/maurits/shared-eggs/cp39/WebTest-3.0.0-py3.9.egg/webtest/app.py", line 620, in do_request
        res = req.get_response(app, catch_exc_info=True)
      File "/Users/maurits/shared-eggs/cp39/WebOb-1.8.7-py3.9.egg/webob/request.py", line 1309, in send
        status, headers, app_iter, exc_info = self.call_application(
      File "/Users/maurits/shared-eggs/cp39/WebOb-1.8.7-py3.9.egg/webob/request.py", line 1278, in call_application
        app_iter = application(self.environ, start_response)
      File "/Users/maurits/shared-eggs/cp39/WebTest-3.0.0-py3.9.egg/webtest/lint.py", line 196, in lint_app
        iterator = application(environ, start_response_wrapper)
      File "/Users/maurits/shared-eggs/cp39/plone.testing-8.0.3-py3.9.egg/plone/testing/_z2_testbrowser.py", line 39, in wrapped_func
        return func(*args, **kw)
      File "/Users/maurits/shared-eggs/cp39/plone.testing-8.0.3-py3.9.egg/plone/testing/_z2_testbrowser.py", line 66, in __call__
        wsgi_result = publish(environ, start_response)
      File "/Users/maurits/community/plone-coredev/6.0/src/Zope/src/ZPublisher/WSGIPublisher.py", line 391, in publish_module
        request.close()
      File "/Users/maurits/community/plone-coredev/6.0/src/Zope/src/ZPublisher/BaseRequest.py", line 215, in close
        notify(EndRequestEvent(None, self))
      File "/Users/maurits/shared-eggs/cp39/zope.event-4.5.0-py3.9.egg/zope/event/__init__.py", line 32, in notify
        subscriber(event)
      File "/Users/maurits/shared-eggs/cp39/zope.component-5.0.1-py3.9.egg/zope/component/event.py", line 27, in dispatch
        component_subscribers(event, None)
      File "/Users/maurits/shared-eggs/cp39/zope.component-5.0.1-py3.9.egg/zope/component/_api.py", line 134, in subscribers
        return sitemanager.subscribers(objects, interface)
      File "/Users/maurits/shared-eggs/cp39/zope.interface-5.4.0-py3.9-macosx-10.15-x86_64.egg/zope/interface/registry.py", line 448, in subscribers
        return self.adapters.subscribers(objects, provided)
      File "/Users/maurits/shared-eggs/cp39/zope.interface-5.4.0-py3.9-macosx-10.15-x86_64.egg/zope/interface/adapter.py", line 895, in subscribers
        subscriptions = self.subscriptions([providedBy(o) for o in objects], provided)
      File "/Users/maurits/shared-eggs/cp39/zope.interface-5.4.0-py3.9-macosx-10.15-x86_64.egg/zope/interface/adapter.py", line 877, in _uncached_subscriptions
        if order >= len(byorder):
      File "/Users/maurits/.pyenv/versions/3.9.9/lib/python3.9/collections/__init__.py", line 1169, in __len__
        return len(self.data)
      File "/Users/maurits/shared-eggs/cp39/ZODB-5.6.0-py3.9.egg/ZODB/Connection.py", line 785, in setstate
        raise ConnectionStateError(msg)
    ZODB.POSException.ConnectionStateError: Shouldn't load state for persistent.list.PersistentList 0x3953f62e87808eb2 when the connection is closed
```

Should be somehow caused by this zope.component PR:
zopefoundation/zope.component#51

From what I can follow from the traceback and running around in the pdb, it goes wrong in this part of `ZPublisher/BaseRequest.py`:

```
    def close(self):
        try:
            notify(EndRequestEvent(None, self))
        finally:
            # subscribers might need the zodb, so `clear` must come afterwards
            # (since `self._held=None` might close the connection, see above)
            self.clear()
```

The notify fails, even when I comment out the two subscribers that I found (`Products.Five` and `plone.app.contentrules`).
Probably some other code has already called `self.clear()` before this.
A reason for this can be that several subrequests are made for the Diazo theme, and this seems to call `self.clear()` directly.
Whatever the reason, the comment is true: subscribers might need the zodb.
In fact, with zope.component 5, the list of subscribers itself, even when it is empty, is something that we get from the zodb.
So it fails here.

When I run `bin/instance fg` and visit the same page anonymously (`@@change-password`) it works fine:
I am Unauthorized and get redirected, which is also what the test expects.
So in practice all seems fine.

So what somehow triggers the ZODB error, is that this is a test, and the first browser visit is to a page that raises an Unauthorized.
The fix/workaround is to first visit the root of the site.  Then somehow all is well.

Note: last time I tried this, most tests in most packages passed just fine.
`plone.app.users` was an exception, and there were about two other packages I think.
I will follow up with more PRs, but this one should be fine, also for use with our current outdated zope.component 4.6.2.
mauritsvanrees added a commit to plone/plone.app.users that referenced this issue Mar 8, 2022
Sample test failure:

```
File "/Users/maurits/community/plone-coredev/6.0/src/plone.app.users/plone/app/users/tests/password.rst", line 27, in password.rst
Failed example:
    browser.open('http://nohost/plone/' + view_name)
Expected:
    Traceback (most recent call last):
    ...
    zExceptions.unauthorized.Unauthorized: ...You are not authorized to access this resource...
Got:
    Traceback (most recent call last):
      File "/Users/maurits/.pyenv/versions/3.9.9/lib/python3.9/doctest.py", line 1334, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest password.rst[8]>", line 1, in <module>
        browser.open('http://nohost/plone/' + view_name)
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 256, in open
        self._processRequest(url, make_request)
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 282, in _processRequest
        resp = make_request(reqargs)
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 253, in make_request
        return self.testapp.get(url, **args)
      File "/Users/maurits/shared-eggs/cp39/WebTest-3.0.0-py3.9.egg/webtest/app.py", line 324, in get
        return self.do_request(req, status=status,
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 93, in do_request
        response = super(TestbrowserApp, self).do_request(req, status,
      File "/Users/maurits/shared-eggs/cp39/WebTest-3.0.0-py3.9.egg/webtest/app.py", line 620, in do_request
        res = req.get_response(app, catch_exc_info=True)
      File "/Users/maurits/shared-eggs/cp39/WebOb-1.8.7-py3.9.egg/webob/request.py", line 1309, in send
        status, headers, app_iter, exc_info = self.call_application(
      File "/Users/maurits/shared-eggs/cp39/WebOb-1.8.7-py3.9.egg/webob/request.py", line 1278, in call_application
        app_iter = application(self.environ, start_response)
      File "/Users/maurits/shared-eggs/cp39/WebTest-3.0.0-py3.9.egg/webtest/lint.py", line 196, in lint_app
        iterator = application(environ, start_response_wrapper)
      File "/Users/maurits/shared-eggs/cp39/plone.testing-8.0.3-py3.9.egg/plone/testing/_z2_testbrowser.py", line 39, in wrapped_func
        return func(*args, **kw)
      File "/Users/maurits/shared-eggs/cp39/plone.testing-8.0.3-py3.9.egg/plone/testing/_z2_testbrowser.py", line 66, in __call__
        wsgi_result = publish(environ, start_response)
      File "/Users/maurits/community/plone-coredev/6.0/src/Zope/src/ZPublisher/WSGIPublisher.py", line 391, in publish_module
        request.close()
      File "/Users/maurits/community/plone-coredev/6.0/src/Zope/src/ZPublisher/BaseRequest.py", line 215, in close
        notify(EndRequestEvent(None, self))
      File "/Users/maurits/shared-eggs/cp39/zope.event-4.5.0-py3.9.egg/zope/event/__init__.py", line 32, in notify
        subscriber(event)
      File "/Users/maurits/shared-eggs/cp39/zope.component-5.0.1-py3.9.egg/zope/component/event.py", line 27, in dispatch
        component_subscribers(event, None)
      File "/Users/maurits/shared-eggs/cp39/zope.component-5.0.1-py3.9.egg/zope/component/_api.py", line 134, in subscribers
        return sitemanager.subscribers(objects, interface)
      File "/Users/maurits/shared-eggs/cp39/zope.interface-5.4.0-py3.9-macosx-10.15-x86_64.egg/zope/interface/registry.py", line 448, in subscribers
        return self.adapters.subscribers(objects, provided)
      File "/Users/maurits/shared-eggs/cp39/zope.interface-5.4.0-py3.9-macosx-10.15-x86_64.egg/zope/interface/adapter.py", line 895, in subscribers
        subscriptions = self.subscriptions([providedBy(o) for o in objects], provided)
      File "/Users/maurits/shared-eggs/cp39/zope.interface-5.4.0-py3.9-macosx-10.15-x86_64.egg/zope/interface/adapter.py", line 877, in _uncached_subscriptions
        if order >= len(byorder):
      File "/Users/maurits/.pyenv/versions/3.9.9/lib/python3.9/collections/__init__.py", line 1169, in __len__
        return len(self.data)
      File "/Users/maurits/shared-eggs/cp39/ZODB-5.6.0-py3.9.egg/ZODB/Connection.py", line 785, in setstate
        raise ConnectionStateError(msg)
    ZODB.POSException.ConnectionStateError: Shouldn't load state for persistent.list.PersistentList 0x3953f62e87808eb2 when the connection is closed
```

Should be somehow caused by this zope.component PR:
zopefoundation/zope.component#51

From what I can follow from the traceback and running around in the pdb, it goes wrong in this part of `ZPublisher/BaseRequest.py`:

```
    def close(self):
        try:
            notify(EndRequestEvent(None, self))
        finally:
            # subscribers might need the zodb, so `clear` must come afterwards
            # (since `self._held=None` might close the connection, see above)
            self.clear()
```

The notify fails, even when I comment out the two subscribers that I found (`Products.Five` and `plone.app.contentrules`).
Probably some other code has already called `self.clear()` before this.
A reason for this can be that several subrequests are made for the Diazo theme, and this seems to call `self.clear()` directly.
Whatever the reason, the comment is true: subscribers might need the zodb.
In fact, with zope.component 5, the list of subscribers itself, even when it is empty, is something that we get from the zodb.
So it fails here.

When I run `bin/instance fg` and visit the same page anonymously (`@@change-password`) it works fine:
I am Unauthorized and get redirected, which is also what the test expects.
So in practice all seems fine.

So what somehow triggers the ZODB error, is that this is a test, and the first browser visit is to a page that raises an Unauthorized.
The fix/workaround is to first visit the root of the site.  Then somehow all is well.

Note: last time I tried this, most tests in most packages passed just fine.
`plone.app.users` was an exception, and there were about two other packages I think.
I will follow up with more PRs, but this one should be fine, also for use with our current outdated zope.component 4.6.2.
mister-roboto pushed a commit to plone/buildout.coredev that referenced this issue Mar 16, 2022
Branch: refs/heads/master
Date: 2022-03-08T20:46:41+01:00
Author: Maurits van Rees (mauritsvanrees) <maurits@vanrees.org>
Commit: plone/plone.app.users@e99f375

Fixed tests when run with zope.component 5+.

Sample test failure:

```
File "/Users/maurits/community/plone-coredev/6.0/src/plone.app.users/plone/app/users/tests/password.rst", line 27, in password.rst
Failed example:
    browser.open('http://nohost/plone/' + view_name)
Expected:
    Traceback (most recent call last):
    ...
    zExceptions.unauthorized.Unauthorized: ...You are not authorized to access this resource...
Got:
    Traceback (most recent call last):
      File "/Users/maurits/.pyenv/versions/3.9.9/lib/python3.9/doctest.py", line 1334, in __run
        exec(compile(example.source, filename, "single",
      File "&lt;doctest password.rst[8]&gt;", line 1, in &lt;module&gt;
        browser.open('http://nohost/plone/' + view_name)
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 256, in open
        self._processRequest(url, make_request)
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 282, in _processRequest
        resp = make_request(reqargs)
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 253, in make_request
        return self.testapp.get(url, **args)
      File "/Users/maurits/shared-eggs/cp39/WebTest-3.0.0-py3.9.egg/webtest/app.py", line 324, in get
        return self.do_request(req, status=status,
      File "/Users/maurits/shared-eggs/cp39/zope.testbrowser-5.5.1-py3.9.egg/zope/testbrowser/browser.py", line 93, in do_request
        response = super(TestbrowserApp, self).do_request(req, status,
      File "/Users/maurits/shared-eggs/cp39/WebTest-3.0.0-py3.9.egg/webtest/app.py", line 620, in do_request
        res = req.get_response(app, catch_exc_info=True)
      File "/Users/maurits/shared-eggs/cp39/WebOb-1.8.7-py3.9.egg/webob/request.py", line 1309, in send
        status, headers, app_iter, exc_info = self.call_application(
      File "/Users/maurits/shared-eggs/cp39/WebOb-1.8.7-py3.9.egg/webob/request.py", line 1278, in call_application
        app_iter = application(self.environ, start_response)
      File "/Users/maurits/shared-eggs/cp39/WebTest-3.0.0-py3.9.egg/webtest/lint.py", line 196, in lint_app
        iterator = application(environ, start_response_wrapper)
      File "/Users/maurits/shared-eggs/cp39/plone.testing-8.0.3-py3.9.egg/plone/testing/_z2_testbrowser.py", line 39, in wrapped_func
        return func(*args, **kw)
      File "/Users/maurits/shared-eggs/cp39/plone.testing-8.0.3-py3.9.egg/plone/testing/_z2_testbrowser.py", line 66, in __call__
        wsgi_result = publish(environ, start_response)
      File "/Users/maurits/community/plone-coredev/6.0/src/Zope/src/ZPublisher/WSGIPublisher.py", line 391, in publish_module
        request.close()
      File "/Users/maurits/community/plone-coredev/6.0/src/Zope/src/ZPublisher/BaseRequest.py", line 215, in close
        notify(EndRequestEvent(None, self))
      File "/Users/maurits/shared-eggs/cp39/zope.event-4.5.0-py3.9.egg/zope/event/__init__.py", line 32, in notify
        subscriber(event)
      File "/Users/maurits/shared-eggs/cp39/zope.component-5.0.1-py3.9.egg/zope/component/event.py", line 27, in dispatch
        component_subscribers(event, None)
      File "/Users/maurits/shared-eggs/cp39/zope.component-5.0.1-py3.9.egg/zope/component/_api.py", line 134, in subscribers
        return sitemanager.subscribers(objects, interface)
      File "/Users/maurits/shared-eggs/cp39/zope.interface-5.4.0-py3.9-macosx-10.15-x86_64.egg/zope/interface/registry.py", line 448, in subscribers
        return self.adapters.subscribers(objects, provided)
      File "/Users/maurits/shared-eggs/cp39/zope.interface-5.4.0-py3.9-macosx-10.15-x86_64.egg/zope/interface/adapter.py", line 895, in subscribers
        subscriptions = self.subscriptions([providedBy(o) for o in objects], provided)
      File "/Users/maurits/shared-eggs/cp39/zope.interface-5.4.0-py3.9-macosx-10.15-x86_64.egg/zope/interface/adapter.py", line 877, in _uncached_subscriptions
        if order &gt;= len(byorder):
      File "/Users/maurits/.pyenv/versions/3.9.9/lib/python3.9/collections/__init__.py", line 1169, in __len__
        return len(self.data)
      File "/Users/maurits/shared-eggs/cp39/ZODB-5.6.0-py3.9.egg/ZODB/Connection.py", line 785, in setstate
        raise ConnectionStateError(msg)
    ZODB.POSException.ConnectionStateError: Shouldn't load state for persistent.list.PersistentList 0x3953f62e87808eb2 when the connection is closed
```

Should be somehow caused by this zope.component PR:
zopefoundation/zope.component#51

From what I can follow from the traceback and running around in the pdb, it goes wrong in this part of `ZPublisher/BaseRequest.py`:

```
    def close(self):
        try:
            notify(EndRequestEvent(None, self))
        finally:
            # subscribers might need the zodb, so `clear` must come afterwards
            # (since `self._held=None` might close the connection, see above)
            self.clear()
```

The notify fails, even when I comment out the two subscribers that I found (`Products.Five` and `plone.app.contentrules`).
Probably some other code has already called `self.clear()` before this.
A reason for this can be that several subrequests are made for the Diazo theme, and this seems to call `self.clear()` directly.
Whatever the reason, the comment is true: subscribers might need the zodb.
In fact, with zope.component 5, the list of subscribers itself, even when it is empty, is something that we get from the zodb.
So it fails here.

When I run `bin/instance fg` and visit the same page anonymously (`@@change-password`) it works fine:
I am Unauthorized and get redirected, which is also what the test expects.
So in practice all seems fine.

So what somehow triggers the ZODB error, is that this is a test, and the first browser visit is to a page that raises an Unauthorized.
The fix/workaround is to first visit the root of the site.  Then somehow all is well.

Note: last time I tried this, most tests in most packages passed just fine.
`plone.app.users` was an exception, and there were about two other packages I think.
I will follow up with more PRs, but this one should be fine, also for use with our current outdated zope.component 4.6.2.

Files changed:
A news/500.bugfix
M plone/app/users/tests/password.rst
M plone/app/users/tests/personal_preferences.rst
M plone/app/users/tests/userdata.rst
Repository: plone.app.users

Branch: refs/heads/master
Date: 2022-03-16T09:26:08+01:00
Author: Maurits van Rees (mauritsvanrees) <maurits@vanrees.org>
Commit: plone/plone.app.users@eb12ba1

Merge pull request #107 from plone/maurits-zope-component-5

Fixed tests when run with zope.component 5+.

Files changed:
A news/500.bugfix
M plone/app/users/tests/password.rst
M plone/app/users/tests/personal_preferences.rst
M plone/app/users/tests/userdata.rst
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants