Skip to content

Commit 8f11164

Browse files
jensensclaude
andauthored
refactor(brain): drop _catalog from PGCatalogBrain (#156)
Catalog-independent brains cache cleanly across request boundaries. Catalog reference moves onto the transient CatalogSearchResults (needed there for ZODB prefetch _p_jar access), not onto individual brains. - getURL now uses only zope.globalrequest.getRequest(); no catalog lookup - getObject / _unrestrictedGetObject resolve the traversal root via getSite().getPhysicalRoot() with a request.PARENTS fallback - PGCatalogBrain.__slots__ drops _catalog; __init__ accepts catalog=None for backwards API compatibility but ignores it - Brain tests mock plone.pgcatalog.brain._traversal_root instead of wiring a fake catalog onto the brain Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 784406f commit 8f11164

4 files changed

Lines changed: 186 additions & 95 deletions

File tree

CHANGES.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## 1.0.0b63
4+
5+
### Changed
6+
7+
- ``PGCatalogBrain`` no longer stores a reference to the catalog tool
8+
and no longer consults ``self._catalog.REQUEST`` for URL rendering.
9+
``getURL`` uses ``zope.globalrequest.getRequest()``; ``getObject``
10+
and ``_unrestrictedGetObject`` resolve a traversal root lazily via
11+
a new ``_traversal_root()`` helper (``getSite().getPhysicalRoot()``
12+
first, ``getRequest().PARENTS[-1]`` as fallback). This keeps brains
13+
catalog-independent so callers can cache / pickle / re-queue them
14+
without dragging the Acquisition chain along. The catalog
15+
reference that the ZODB-prefetch batch needs has moved onto the
16+
transient ``CatalogSearchResults`` container, where it belongs.
17+
18+
The ``catalog=`` keyword on ``PGCatalogBrain.__init__`` is accepted
19+
but ignored — kept until the next major version for signature
20+
compatibility with direct callers.
21+
322
## 1.0.0b62
423

524
### Fixed

src/plone/pgcatalog/brain.py

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from plone.pgcatalog.extraction import decode_meta
1313
from Products.ZCatalog.interfaces import ICatalogBrain
1414
from ZODB.utils import p64
15+
from zope.component.hooks import getSite
16+
from zope.globalrequest import getRequest
1517
from zope.interface import implementer
1618
from zope.interface.common.sequence import IFiniteSequence
1719
from ZTUtils.Lazy import Lazy
@@ -25,12 +27,40 @@
2527
_PREFETCH_BATCH = int(os.environ.get("PGCATALOG_PREFETCH_BATCH", "100"))
2628

2729

30+
def _traversal_root():
31+
"""Return a traversable Zope root (Application), or None.
32+
33+
Brains avoid holding a reference to the catalog tool so they can
34+
be cached / pickled / re-queued without dragging the Acquisition
35+
chain along. The root is resolved at call time via
36+
``zope.component.hooks.getSite`` first (works in request and
37+
thread contexts that have a local-site hook), then falls back to
38+
the Zope traversal root from ``zope.globalrequest.getRequest``.
39+
40+
Returns ``None`` when neither is available — the caller (``getObject``
41+
/ ``_unrestrictedGetObject``) then reports "object not found"
42+
rather than raising, matching the long-standing ZCatalog contract.
43+
"""
44+
site = getSite()
45+
if site is not None:
46+
return site.getPhysicalRoot()
47+
request = getRequest()
48+
if request is not None:
49+
parents = getattr(request, "PARENTS", None)
50+
if parents:
51+
return parents[-1]
52+
return None
53+
54+
2855
@implementer(ICatalogBrain)
2956
class PGCatalogBrain:
3057
"""Lightweight catalog brain backed by a PostgreSQL row.
3158
3259
Implements the essential ICatalogBrain interface without requiring
33-
Zope Acquisition or Record infrastructure.
60+
Zope Acquisition or Record infrastructure. Deliberately does NOT
61+
store a reference to the catalog tool — traversal resolves the
62+
portal root lazily via ``_traversal_root()``, so a brain remains
63+
safe to cache / pickle across request boundaries.
3464
3565
Supports two modes:
3666
- **Eager** (default): row contains ``idx`` dict, metadata access is direct.
@@ -40,14 +70,18 @@ class PGCatalogBrain:
4070
4171
Args:
4272
row: dict with keys zoid, path (and optionally idx)
43-
catalog: reference to the catalog tool (for getObject traversal)
73+
catalog: accepted for backward compatibility with callers still
74+
passing the catalog tool — ignored. Kept until the next
75+
major version for signature compatibility.
4476
"""
4577

46-
__slots__ = ("_catalog", "_result_set", "_row")
78+
__slots__ = ("_result_set", "_row")
4779

4880
def __init__(self, row, catalog=None):
81+
# ``catalog`` is accepted for backward compat with callers
82+
# still passing the tool — intentionally unused.
83+
del catalog
4984
self._row = row
50-
self._catalog = catalog
5185
self._result_set = None
5286

5387
# -- ICatalogBrain methods -----------------------------------------------
@@ -59,13 +93,15 @@ def getPath(self):
5993
def getURL(self, relative=0):
6094
"""Generate a URL for this record.
6195
62-
In standalone mode (no request), returns the path.
63-
When integrated with Zope (Phase 6), uses request.physicalPathToURL.
96+
Uses ``zope.globalrequest.getRequest()`` — no reference to the
97+
catalog tool. Keeping brains catalog-independent lets callers
98+
cache / pickle / re-queue them without dragging the acquisition
99+
chain along. Returns the plain path in standalone / script
100+
mode when no request is active.
64101
"""
65-
if self._catalog is not None:
66-
request = getattr(self._catalog, "REQUEST", None)
67-
if request is not None:
68-
return request.physicalPathToURL(self.getPath(), relative)
102+
request = getRequest()
103+
if request is not None:
104+
return request.physicalPathToURL(self.getPath(), relative)
69105
return self.getPath()
70106

71107
def _maybe_prefetch(self):
@@ -83,10 +119,7 @@ def _maybe_prefetch(self):
83119
result_set._maybe_prefetch_objects(self)
84120

85121
def getObject(self):
86-
"""Return the object for this record.
87-
88-
Requires a catalog with traversal support (Phase 6 integration).
89-
Returns None if the object cannot be found.
122+
"""Return the object for this record, or None if not found.
90123
91124
Mirrors upstream ``Products.ZCatalog.CatalogBrains.AbstractCatalogBrain``
92125
semantics: the catalog filter already authorized access to the
@@ -96,30 +129,31 @@ def getObject(self):
96129
e.g. an internal calendar container publishing public events)
97130
raise ``Unauthorized`` on the parent even though the user is
98131
allowed to see the target.
132+
133+
Resolves the traversal root lazily via ``_traversal_root()`` so
134+
brains stay catalog-independent (cache-friendly).
99135
"""
100-
if self._catalog is None:
136+
root = _traversal_root()
137+
if root is None:
101138
return None
102139
self._maybe_prefetch()
103140
path = self.getPath().split("/")
104141
if not path:
105142
return None
106143
try:
107-
parent = (
108-
self._catalog.unrestrictedTraverse(path[:-1])
109-
if len(path) > 1
110-
else self._catalog
111-
)
144+
parent = root.unrestrictedTraverse(path[:-1]) if len(path) > 1 else root
112145
return parent.restrictedTraverse(path[-1])
113146
except (KeyError, AttributeError):
114147
return None
115148

116149
def _unrestrictedGetObject(self):
117-
"""Return the object without security checks."""
118-
if self._catalog is None:
150+
"""Return the object without security checks, or None if not found."""
151+
root = _traversal_root()
152+
if root is None:
119153
return None
120154
self._maybe_prefetch()
121155
try:
122-
return self._catalog.unrestrictedTraverse(self.getPath())
156+
return root.unrestrictedTraverse(self.getPath())
123157
except (KeyError, AttributeError):
124158
return None
125159

@@ -274,14 +308,15 @@ class CatalogSearchResults(Lazy):
274308
using the same connection (and thus the same REPEATABLE READ snapshot).
275309
"""
276310

277-
def __init__(self, brains, actual_result_count=None, conn=None):
311+
def __init__(self, brains, actual_result_count=None, conn=None, catalog=None):
278312
self._brains = list(brains)
279313
self.actual_result_count = (
280314
actual_result_count
281315
if actual_result_count is not None
282316
else len(self._brains)
283317
)
284318
self._conn = conn
319+
self._catalog = catalog # used only for ZODB prefetch (needs _p_jar)
285320
self._idx_loaded = conn is None # eager if no conn
286321
self._prefetched_ranges = set() # set of (start, end) tuples
287322
# Build index for O(1) brain → position lookup (used by prefetch)
@@ -360,11 +395,12 @@ def _maybe_prefetch_objects(self, brain):
360395
if not batch:
361396
return
362397

363-
# Get storage from the catalog reference on the first brain.
364-
catalog = batch[0]._catalog
365-
if catalog is None:
398+
# Get storage from the result-set's catalog reference (brains
399+
# are catalog-independent — the tool lives only on the
400+
# transient result set).
401+
if self._catalog is None:
366402
return
367-
jar = getattr(catalog, "_p_jar", None)
403+
jar = getattr(self._catalog, "_p_jar", None)
368404
if jar is None:
369405
return
370406
storage = getattr(jar, "_storage", None)
@@ -390,6 +426,7 @@ def __getitem__(self, index):
390426
result,
391427
self.actual_result_count,
392428
conn=self._conn,
429+
catalog=self._catalog,
393430
)
394431
# Re-wire brains to new result set if idx not yet loaded
395432
if not self._idx_loaded:

src/plone/pgcatalog/search.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,19 @@ def _unrestrictedGetObject(self):
104104

105105

106106
def _build_results(rows, actual_count, catalog, lazy_conn):
107-
"""Build CatalogSearchResults from raw rows."""
108-
brains = [PGCatalogBrain(row, catalog=catalog) for row in rows]
107+
"""Build CatalogSearchResults from raw rows.
108+
109+
Brains are catalog-independent (catalog-independent brains cache
110+
cleanly across request boundaries — see #156). The catalog ref
111+
is only kept on the transient result set, where ZODB-prefetch
112+
needs it to reach the storage jar.
113+
"""
114+
brains = [PGCatalogBrain(row) for row in rows]
109115
results = CatalogSearchResults(
110-
brains, actual_result_count=actual_count, conn=lazy_conn
116+
brains,
117+
actual_result_count=actual_count,
118+
conn=lazy_conn,
119+
catalog=catalog,
111120
)
112121
if lazy_conn is not None:
113122
for brain in brains:

0 commit comments

Comments
 (0)