@@ -141,6 +141,263 @@ def test_auto_migration(self):
141141 self .assertTrue (isinstance (registry ._records , _Records ))
142142
143143
144+ class FakeRequest :
145+ """Minimal request-like object for testing request-level caching."""
146+
147+ def __init__ (self ):
148+ self .environ = {}
149+
150+
151+ class TestRequestValueCache (unittest .TestCase ):
152+ """Tests for request-level value caching in Registry."""
153+
154+ def setUp (self ):
155+ setUp (self )
156+ from plone .registry import field
157+ from plone .registry .record import Record
158+ from plone .registry .registry import Registry
159+
160+ self .registry = Registry ()
161+ self .registry .records ["test.key1" ] = Record (
162+ field .TextLine (title = "Key 1" ), "value1"
163+ )
164+ self .registry .records ["test.key2" ] = Record (
165+ field .TextLine (title = "Key 2" ), "value2"
166+ )
167+ self .registry .records ["test.none" ] = Record (
168+ field .TextLine (title = "None val" , required = False ), None
169+ )
170+ self .request = FakeRequest ()
171+
172+ def tearDown (self ):
173+ if hasattr (self .registry , "REQUEST" ):
174+ del self .registry .REQUEST
175+ testing .tearDown (self )
176+
177+ def _setRequest (self ):
178+ self .registry .REQUEST = self .request
179+
180+ # --- __getitem__ tests ---
181+
182+ def test_getitem_no_request (self ):
183+ """Without a request, values are fetched directly from OOBTree."""
184+ self .assertEqual (self .registry ["test.key1" ], "value1" )
185+ self .assertFalse (hasattr (self .request , "_plone_registry_cache" ))
186+
187+ def test_getitem_populates_cache (self ):
188+ """First read with a request populates the cache."""
189+ self ._setRequest ()
190+ self .assertEqual (self .registry ["test.key1" ], "value1" )
191+ self .assertIn ("test.key1" , self .request ._plone_registry_cache )
192+ self .assertEqual (self .request ._plone_registry_cache ["test.key1" ], "value1" )
193+
194+ def test_getitem_serves_from_cache (self ):
195+ """Subsequent reads return the cached value."""
196+ self ._setRequest ()
197+ self .request ._plone_registry_cache = {"test.key1" : "cached_value" }
198+ self .assertEqual (self .registry ["test.key1" ], "cached_value" )
199+
200+ def test_getitem_keyerror_not_cached (self ):
201+ """KeyError for missing keys propagates without caching."""
202+ self ._setRequest ()
203+ with self .assertRaises (KeyError ):
204+ self .registry ["nonexistent.key" ]
205+ self .assertNotIn ("nonexistent.key" , self .request ._plone_registry_cache )
206+
207+ def test_getitem_multiple_keys (self ):
208+ """Multiple keys are independently cached."""
209+ self ._setRequest ()
210+ self .assertEqual (self .registry ["test.key1" ], "value1" )
211+ self .assertEqual (self .registry ["test.key2" ], "value2" )
212+ cache = self .request ._plone_registry_cache
213+ self .assertEqual (cache ["test.key1" ], "value1" )
214+ self .assertEqual (cache ["test.key2" ], "value2" )
215+
216+ # --- get() tests ---
217+
218+ def test_get_no_request (self ):
219+ """Without a request, get() works normally."""
220+ self .assertEqual (self .registry .get ("test.key1" ), "value1" )
221+ self .assertEqual (self .registry .get ("nonexistent" , "default" ), "default" )
222+
223+ def test_get_populates_cache (self ):
224+ """get() populates the cache on first access."""
225+ self ._setRequest ()
226+ self .assertEqual (self .registry .get ("test.key1" ), "value1" )
227+ self .assertIn ("test.key1" , self .request ._plone_registry_cache )
228+
229+ def test_get_serves_from_cache (self ):
230+ """get() returns cached value on subsequent access."""
231+ self ._setRequest ()
232+ self .request ._plone_registry_cache = {"test.key1" : "cached" }
233+ self .assertEqual (self .registry .get ("test.key1" ), "cached" )
234+
235+ def test_get_default_not_cached (self ):
236+ """get() with missing key returns default and does not cache it."""
237+ self ._setRequest ()
238+ result = self .registry .get ("nonexistent" , "default" )
239+ self .assertEqual (result , "default" )
240+ self .assertNotIn ("nonexistent" , self .request ._plone_registry_cache )
241+
242+ def test_get_none_value_cached (self ):
243+ """None values are correctly cached (not confused with sentinel)."""
244+ self ._setRequest ()
245+ result = self .registry .get ("test.none" )
246+ self .assertIsNone (result )
247+ self .assertIn ("test.none" , self .request ._plone_registry_cache )
248+ self .assertIsNone (self .registry .get ("test.none" ))
249+
250+ def test_get_none_default (self ):
251+ """get() returns None default for missing keys without caching."""
252+ self ._setRequest ()
253+ result = self .registry .get ("nonexistent" )
254+ self .assertIsNone (result )
255+ self .assertNotIn ("nonexistent" , self .request ._plone_registry_cache )
256+
257+ # --- __setitem__ tests ---
258+
259+ def test_setitem_updates_cache (self ):
260+ """Writing a value updates the cache."""
261+ self ._setRequest ()
262+ self .assertEqual (self .registry ["test.key1" ], "value1" )
263+ self .registry ["test.key1" ] = "new_value"
264+ self .assertEqual (self .request ._plone_registry_cache ["test.key1" ], "new_value" )
265+ self .assertEqual (self .registry ["test.key1" ], "new_value" )
266+
267+ def test_setitem_no_request (self ):
268+ """Writing without a request works normally."""
269+ self .registry ["test.key1" ] = "new_value"
270+ self .assertEqual (self .registry ["test.key1" ], "new_value" )
271+
272+ # --- Cache isolation ---
273+
274+ def test_cache_isolation_between_requests (self ):
275+ """Different requests have independent caches."""
276+ self ._setRequest ()
277+ self .registry ["test.key1" ] # populate cache
278+
279+ new_request = FakeRequest ()
280+ self .registry .REQUEST = new_request
281+ self .assertFalse (hasattr (new_request , "_plone_registry_cache" ))
282+
283+ self .assertEqual (self .registry ["test.key1" ], "value1" )
284+ self .assertIn ("test.key1" , new_request ._plone_registry_cache )
285+ self .assertIn ("test.key1" , self .request ._plone_registry_cache )
286+
287+ def test_cache_lazily_created (self ):
288+ """Cache dict is only created on first registry access."""
289+ self ._setRequest ()
290+ self .assertFalse (hasattr (self .request , "_plone_registry_cache" ))
291+ self .registry ["test.key1" ]
292+ self .assertTrue (hasattr (self .request , "_plone_registry_cache" ))
293+
294+ # --- __contains__ tests ---
295+
296+ def test_contains_no_request (self ):
297+ """Without a request, __contains__ works normally."""
298+ self .assertIn ("test.key1" , self .registry )
299+ self .assertNotIn ("nonexistent" , self .registry )
300+
301+ def test_contains_uses_value_cache (self ):
302+ """__contains__ returns True if key is in the value cache."""
303+ self ._setRequest ()
304+ self .registry .get ("test.key1" )
305+ self .assertIn ("test.key1" , self .registry )
306+
307+ def test_contains_falls_through_to_oobtree (self ):
308+ """__contains__ falls through to OOBTree for uncached keys."""
309+ self ._setRequest ()
310+ self .assertIn ("test.key2" , self .registry )
311+ self .assertNotIn ("nonexistent" , self .registry )
312+
313+
314+ class TestForInterfaceCache (unittest .TestCase ):
315+ """Tests for forInterface() proxy caching."""
316+
317+ def setUp (self ):
318+ setUp (self )
319+ from plone .registry .registry import Registry
320+
321+ self .registry = Registry ()
322+ self .registry .registerInterface (IMailSettings )
323+ self .request = FakeRequest ()
324+
325+ def tearDown (self ):
326+ if hasattr (self .registry , "REQUEST" ):
327+ del self .registry .REQUEST
328+ testing .tearDown (self )
329+
330+ def _setRequest (self ):
331+ self .registry .REQUEST = self .request
332+
333+ def test_forinterface_no_request (self ):
334+ """Without a request, forInterface works normally."""
335+ proxy = self .registry .forInterface (IMailSettings )
336+ self .assertTrue (IMailSettings .providedBy (proxy ))
337+
338+ def test_forinterface_caches_proxy (self ):
339+ """With a request, forInterface caches the proxy."""
340+ self ._setRequest ()
341+ proxy1 = self .registry .forInterface (IMailSettings )
342+ proxy2 = self .registry .forInterface (IMailSettings )
343+ self .assertIs (proxy1 , proxy2 )
344+
345+ def test_forinterface_cache_hit_skips_check (self ):
346+ """Cached proxy avoids the field existence check."""
347+ self ._setRequest ()
348+ proxy1 = self .registry .forInterface (IMailSettings )
349+ prefix = IMailSettings .__identifier__ + "."
350+ key = prefix + "sender"
351+ del self .registry .records ._values [key ]
352+ del self .registry .records ._fields [key ]
353+ proxy2 = self .registry .forInterface (IMailSettings )
354+ self .assertIs (proxy1 , proxy2 )
355+
356+ def test_forinterface_same_prefix_different_interface_not_shared (self ):
357+ """Different interfaces with same prefix get separate proxies."""
358+ self ._setRequest ()
359+ proxy1 = self .registry .forInterface (
360+ IMailSettings , check = False , prefix = "shared.prefix"
361+ )
362+ proxy2 = self .registry .forInterface (
363+ IMailPreferences , check = False , prefix = "shared.prefix"
364+ )
365+ self .assertIsNot (proxy1 , proxy2 )
366+ self .assertTrue (IMailSettings .providedBy (proxy1 ))
367+ self .assertTrue (IMailPreferences .providedBy (proxy2 ))
368+
369+ def test_forinterface_different_prefix_not_shared (self ):
370+ """Different prefixes get separate cached proxies."""
371+ self ._setRequest ()
372+ self .registry .registerInterface (IMailSettings , prefix = "alt.settings" )
373+ proxy1 = self .registry .forInterface (IMailSettings )
374+ proxy2 = self .registry .forInterface (IMailSettings , prefix = "alt.settings" )
375+ self .assertIsNot (proxy1 , proxy2 )
376+
377+ def test_forinterface_different_omit_not_shared (self ):
378+ """Different omit tuples get separate cached proxies."""
379+ self ._setRequest ()
380+ proxy1 = self .registry .forInterface (IMailSettings )
381+ proxy2 = self .registry .forInterface (IMailSettings , omit = ("sender" ,))
382+ self .assertIsNot (proxy1 , proxy2 )
383+
384+ def test_forinterface_cache_isolation (self ):
385+ """Different requests have independent proxy caches."""
386+ self ._setRequest ()
387+ proxy1 = self .registry .forInterface (IMailSettings )
388+
389+ new_request = FakeRequest ()
390+ self .registry .REQUEST = new_request
391+ proxy2 = self .registry .forInterface (IMailSettings )
392+ self .assertIsNot (proxy1 , proxy2 )
393+
394+ def test_forinterface_no_request_returns_new_each_time (self ):
395+ """Without a request, each call returns a fresh proxy."""
396+ proxy1 = self .registry .forInterface (IMailSettings )
397+ proxy2 = self .registry .forInterface (IMailSettings )
398+ self .assertIsNot (proxy1 , proxy2 )
399+
400+
144401def test_suite ():
145402 return unittest .TestSuite (
146403 [
@@ -170,5 +427,7 @@ def test_suite():
170427 ),
171428 unittest .defaultTestLoader .loadTestsFromTestCase (TestBugs ),
172429 unittest .defaultTestLoader .loadTestsFromTestCase (TestMigration ),
430+ unittest .defaultTestLoader .loadTestsFromTestCase (TestRequestValueCache ),
431+ unittest .defaultTestLoader .loadTestsFromTestCase (TestForInterfaceCache ),
173432 ]
174433 )
0 commit comments