zope.locking 3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1246 @@
1
+ Metadata-Version: 2.2
2
+ Name: zope.locking
3
+ Version: 3.0
4
+ Summary: Advisory exclusive locks, shared locks, and freezes (locked to no-one).
5
+ Home-page: https://github.com/zopefoundation/zope.locking
6
+ Author: Zope Project
7
+ Author-email: zope3-dev@zope.org
8
+ License: ZPL-2.1
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Zope Public License
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
+ Classifier: Natural Language :: English
23
+ Classifier: Operating System :: OS Independent
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Framework :: Zope :: 3
26
+ Requires-Python: >=3.9
27
+ License-File: LICENSE.rst
28
+ Requires-Dist: BTrees
29
+ Requires-Dist: persistent
30
+ Requires-Dist: pytz
31
+ Requires-Dist: setuptools
32
+ Requires-Dist: zope.component
33
+ Requires-Dist: zope.event
34
+ Requires-Dist: zope.generations
35
+ Requires-Dist: zope.interface>=3.8
36
+ Requires-Dist: zope.keyreference
37
+ Requires-Dist: zope.location
38
+ Requires-Dist: zope.schema
39
+ Requires-Dist: zope.security
40
+ Provides-Extra: test
41
+ Requires-Dist: transaction; extra == "test"
42
+ Requires-Dist: zope.app.appsetup; extra == "test"
43
+ Requires-Dist: zope.testing; extra == "test"
44
+ Requires-Dist: zope.testrunner; extra == "test"
45
+ Dynamic: author
46
+ Dynamic: author-email
47
+ Dynamic: classifier
48
+ Dynamic: description
49
+ Dynamic: home-page
50
+ Dynamic: license
51
+ Dynamic: provides-extra
52
+ Dynamic: requires-dist
53
+ Dynamic: requires-python
54
+ Dynamic: summary
55
+
56
+ =======================================================================
57
+ Advisory exclusive locks, shared locks, and freezes (locked to no-one).
58
+ =======================================================================
59
+
60
+ The zope.locking package provides three main features:
61
+
62
+ - advisory exclusive locks for individual objects;
63
+
64
+ - advisory shared locks for individual objects; and
65
+
66
+ - frozen objects (locked to no one).
67
+
68
+ Locks and freezes by themselves are advisory tokens and inherently
69
+ meaningless. They must be given meaning by other software, such as a security
70
+ policy.
71
+
72
+ This package approaches these features primarily from the perspective of a
73
+ system API, largely free of policy; and then provides a set of adapters for
74
+ more common interaction with users, with some access policy. We will first
75
+ look at the system API, and then explain the policy and suggested use of the
76
+ provided adapters.
77
+
78
+
79
+ .. contents::
80
+
81
+ ==========
82
+ System API
83
+ ==========
84
+
85
+ The central approach for the package is that locks and freeze tokens must be
86
+ created and then registered by a token utility. The tokens will not work
87
+ until they have been registered. This gives the ability to definitively know,
88
+ and thus manipulate, all active tokens in a system.
89
+
90
+ The first object we'll introduce, then, is the TokenUtility: the utility that
91
+ is responsible for the registration and the retrieving of tokens.
92
+
93
+ >>> from zope import component, interface
94
+ >>> from zope.locking import interfaces, utility, tokens
95
+ >>> util = utility.TokenUtility()
96
+ >>> from zope.interface.verify import verifyObject
97
+ >>> verifyObject(interfaces.ITokenUtility, util)
98
+ True
99
+
100
+ The utility only has a few methods--`get`, `iterForPrincipalId`,
101
+ `__iter__`, and `register`--which we will look at below. It is expected to be
102
+ persistent, and the included implementation is in fact persistent.Persistent,
103
+ and expects to be installed as a local utility. The utility needs a
104
+ connection to the database before it can register persistent tokens.
105
+
106
+ >>> from zope.locking.testing import Demo
107
+ >>> lock = tokens.ExclusiveLock(Demo(), 'Fantomas')
108
+ >>> util.register(lock)
109
+ Traceback (most recent call last):
110
+ ...
111
+ AttributeError: 'NoneType' object has no attribute 'add'
112
+
113
+ >>> conn = get_connection()
114
+ >>> conn.add(util)
115
+
116
+ If the token provides IPersistent, the utility will add it to its connection.
117
+
118
+ >>> lock._p_jar is None
119
+ True
120
+
121
+ >>> lock = util.register(lock)
122
+ >>> lock._p_jar is util._p_jar
123
+ True
124
+
125
+ >>> lock.end()
126
+ >>> lock = util.register(lock)
127
+
128
+
129
+ The standard token utility can accept tokens for any object that is adaptable
130
+ to IKeyReference.
131
+
132
+ >>> import datetime
133
+ >>> import pytz
134
+ >>> before_creation = datetime.datetime.now(pytz.utc)
135
+ >>> demo = Demo()
136
+
137
+ Now, with an instance of the demo class, it is possible to register lock and
138
+ freeze tokens for demo instances with the token utility.
139
+
140
+ As mentioned above, the general pattern for making a lock or freeze token is
141
+ to create it--at which point most of its methods and attributes are
142
+ unusable--and then to register it with the token utility. After registration,
143
+ the lock is effective and in place.
144
+
145
+ The TokenUtility can actually be used with anything that implements
146
+ zope.locking.interfaces.IAbstractToken, but we'll look at the four tokens that
147
+ come with the zope.locking package: an exclusive lock, a shared lock, a
148
+ permanent freeze, and an endable freeze.
149
+
150
+ Exclusive Locks
151
+ ===============
152
+
153
+ Exclusive locks are tokens that are owned by a single principal. No principal
154
+ may be added or removed: the lock token must be ended and another started for
155
+ another principal to get the benefits of the lock (whatever they have been
156
+ configured to be).
157
+
158
+ Here's an example of creating and registering an exclusive lock: the principal
159
+ with an id of 'john' locks the demo object.
160
+
161
+ >>> lock = tokens.ExclusiveLock(demo, 'john')
162
+ >>> res = util.register(lock)
163
+ >>> res is lock
164
+ True
165
+
166
+ The lock token is now in effect. Registering the token (the lock) fired an
167
+ ITokenStartedEvent, which we'll look at now.
168
+
169
+ (Note that this example uses an events list to look at events that have fired.
170
+ This is simply a list whose `append` method has been added as a subscriber
171
+ to the zope.event.subscribers list. It's included as a global when this file
172
+ is run as a test.)
173
+
174
+ >>> from zope.component.eventtesting import events
175
+ >>> ev = events[-1]
176
+ >>> verifyObject(interfaces.ITokenStartedEvent, ev)
177
+ True
178
+ >>> ev.object is lock
179
+ True
180
+
181
+ Now that the lock token is created and registered, the token utility knows
182
+ about it. The utilities `get` method simply returns the active token for an
183
+ object or None--it never returns an ended token, and in fact none of the
184
+ utility methods do.
185
+
186
+ >>> util.get(demo) is lock
187
+ True
188
+ >>> util.get(Demo()) is None
189
+ True
190
+
191
+ Note that `get` accepts alternate defaults, like a dictionary.get:
192
+
193
+ >>> util.get(Demo(), util) is util
194
+ True
195
+
196
+ The `iterForPrincipalId` method returns an iterator of active locks for the
197
+ given principal id.
198
+
199
+ >>> list(util.iterForPrincipalId('john')) == [lock]
200
+ True
201
+ >>> list(util.iterForPrincipalId('mary')) == []
202
+ True
203
+
204
+ The util's `__iter__` method simply iterates over all active (non-ended)
205
+ tokens.
206
+
207
+ >>> list(util) == [lock]
208
+ True
209
+
210
+ The token utility disallows registration of multiple active tokens for the
211
+ same object.
212
+
213
+ >>> util.register(tokens.ExclusiveLock(demo, 'mary'))
214
+ ... # doctest: +ELLIPSIS
215
+ Traceback (most recent call last):
216
+ ...
217
+ zope.locking.interfaces.RegistrationError: ...
218
+ >>> util.register(tokens.SharedLock(demo, ('mary', 'jane')))
219
+ ... # doctest: +ELLIPSIS
220
+ Traceback (most recent call last):
221
+ ...
222
+ zope.locking.interfaces.RegistrationError: ...
223
+ >>> util.register(tokens.Freeze(demo))
224
+ ... # doctest: +ELLIPSIS
225
+ Traceback (most recent call last):
226
+ ...
227
+ zope.locking.interfaces.RegistrationError: ...
228
+
229
+ It's also worth looking at the lock token itself. The registered lock token
230
+ implements IExclusiveLock.
231
+
232
+ >>> verifyObject(interfaces.IExclusiveLock, lock)
233
+ True
234
+
235
+ It provides a number of capabilities. Arguably the most important attribute is
236
+ whether the token is in effect or not: `ended`. This token is active, so it
237
+ has not yet ended:
238
+
239
+ >>> lock.ended is None
240
+ True
241
+
242
+ When it does end, the ended attribute is a datetime in UTC of when the token
243
+ ended. We'll demonstrate that below.
244
+
245
+ Later, the `creation`, `expiration`, `duration`, and `remaining_duration` will
246
+ be important; for now we merely note their existence.
247
+
248
+ >>> before_creation <= lock.started <= datetime.datetime.now(pytz.utc)
249
+ True
250
+ >>> lock.expiration is None # == forever
251
+ True
252
+ >>> lock.duration is None # == forever
253
+ True
254
+ >>> lock.remaining_duration is None # == forever
255
+ True
256
+
257
+ The `end` method and the related ending and expiration attributes are all part
258
+ of the IEndable interface--an interface that not all tokens must implement,
259
+ as we will also discuss later.
260
+
261
+ >>> interfaces.IEndable.providedBy(lock)
262
+ True
263
+
264
+ The `context` and `__parent__` attributes point to the locked object--demo in
265
+ our case. `context` is the intended standard API for obtaining the object,
266
+ but `__parent__` is important for the Zope 3 security set up, as discussed
267
+ towards the end of this document.
268
+
269
+ >>> lock.context is demo
270
+ True
271
+ >>> lock.__parent__ is demo # important for security
272
+ True
273
+
274
+ Registering the lock with the token utility set the utility attribute and
275
+ initialized the started attribute to the datetime that the lock began. The
276
+ utility attribute should never be set by any code other than the token
277
+ utility.
278
+
279
+ >>> lock.utility is util
280
+ True
281
+
282
+ Tokens always provide a `principal_ids` attribute that provides an iterable of
283
+ the principals that are part of a token. In our case, this is an exclusive
284
+ lock for 'john', so the value is simple.
285
+
286
+ >>> sorted(lock.principal_ids)
287
+ ['john']
288
+
289
+ The only method on a basic token like the exclusive lock is `end`. Calling it
290
+ without arguments permanently and explicitly ends the life of the token.
291
+
292
+ >>> lock.end()
293
+
294
+ Like registering a token, ending a token fires an event.
295
+
296
+ >>> ev = events[-1]
297
+ >>> verifyObject(interfaces.ITokenEndedEvent, ev)
298
+ True
299
+ >>> ev.object is lock
300
+ True
301
+
302
+ It affects attributes on the token. Again, the most important of these is
303
+ ended, which is now the datetime of ending.
304
+
305
+ >>> lock.ended >= lock.started
306
+ True
307
+ >>> lock.remaining_duration == datetime.timedelta()
308
+ True
309
+
310
+ It also affects queries of the token utility.
311
+
312
+ >>> util.get(demo) is None
313
+ True
314
+ >>> list(util.iterForPrincipalId('john')) == []
315
+ True
316
+ >>> list(util) == []
317
+ True
318
+
319
+ Don't try to end an already-ended token.
320
+
321
+ >>> lock.end()
322
+ Traceback (most recent call last):
323
+ ...
324
+ zope.locking.interfaces.EndedError
325
+
326
+ The other way of ending a token is with an expiration datetime. As we'll see,
327
+ one of the most important caveats about working with timeouts is that a token
328
+ that expires because of a timeout does not fire any expiration event. It
329
+ simply starts providing the `expiration` value for the `ended` attribute.
330
+
331
+ >>> one = datetime.timedelta(hours=1)
332
+ >>> two = datetime.timedelta(hours=2)
333
+ >>> three = datetime.timedelta(hours=3)
334
+ >>> four = datetime.timedelta(hours=4)
335
+ >>> lock = util.register(tokens.ExclusiveLock(demo, 'john', three))
336
+ >>> lock.duration
337
+ datetime.timedelta(seconds=10800)
338
+ >>> three >= lock.remaining_duration >= two
339
+ True
340
+ >>> lock.ended is None
341
+ True
342
+ >>> util.get(demo) is lock
343
+ True
344
+ >>> list(util.iterForPrincipalId('john')) == [lock]
345
+ True
346
+ >>> list(util) == [lock]
347
+ True
348
+
349
+ The expiration time of an endable token is always the creation date plus the
350
+ timeout.
351
+
352
+ >>> lock.expiration == lock.started + lock.duration
353
+ True
354
+ >>> ((before_creation + three) <=
355
+ ... (lock.expiration) <= # this value is the expiration date
356
+ ... (before_creation + four))
357
+ True
358
+
359
+ Expirations can be changed while a lock is still active, using any of
360
+ the `expiration`, `remaining_duration` or `duration` attributes. All changes
361
+ fire events. First we'll change the expiration attribute.
362
+
363
+ >>> lock.expiration = lock.started + one
364
+ >>> lock.expiration == lock.started + one
365
+ True
366
+ >>> lock.duration == one
367
+ True
368
+ >>> ev = events[-1]
369
+ >>> verifyObject(interfaces.IExpirationChangedEvent, ev)
370
+ True
371
+ >>> ev.object is lock
372
+ True
373
+ >>> ev.old == lock.started + three
374
+ True
375
+
376
+ Next we'll change the duration attribute.
377
+
378
+ >>> lock.duration = four
379
+ >>> lock.duration
380
+ datetime.timedelta(seconds=14400)
381
+ >>> four >= lock.remaining_duration >= three
382
+ True
383
+ >>> ev = events[-1]
384
+ >>> verifyObject(interfaces.IExpirationChangedEvent, ev)
385
+ True
386
+ >>> ev.object is lock
387
+ True
388
+ >>> ev.old == lock.started + one
389
+ True
390
+
391
+ Now we'll hack our code to make it think that it is two hours later, and then
392
+ check and modify the remaining_duration attribute.
393
+
394
+ >>> def hackNow():
395
+ ... return (datetime.datetime.now(pytz.utc) +
396
+ ... datetime.timedelta(hours=2))
397
+ ...
398
+ >>> import zope.locking.utils
399
+ >>> oldNow = zope.locking.utils.now
400
+ >>> zope.locking.utils.now = hackNow # make code think it's 2 hours later
401
+ >>> lock.duration
402
+ datetime.timedelta(seconds=14400)
403
+ >>> two >= lock.remaining_duration >= one
404
+ True
405
+ >>> lock.remaining_duration -= one
406
+ >>> one >= lock.remaining_duration >= datetime.timedelta()
407
+ True
408
+ >>> three + datetime.timedelta(minutes=1) >= lock.duration >= three
409
+ True
410
+ >>> ev = events[-1]
411
+ >>> verifyObject(interfaces.IExpirationChangedEvent, ev)
412
+ True
413
+ >>> ev.object is lock
414
+ True
415
+ >>> ev.old == lock.started + four
416
+ True
417
+
418
+ Now, we'll hack our code to make it think that it's a day later. It is very
419
+ important to remember that a lock ending with a timeout ends silently--that
420
+ is, no event is fired.
421
+
422
+ >>> def hackNow():
423
+ ... return (
424
+ ... datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1))
425
+ ...
426
+ >>> zope.locking.utils.now = hackNow # make code think it is a day later
427
+ >>> lock.ended == lock.expiration
428
+ True
429
+ >>> util.get(demo) is None
430
+ True
431
+ >>> util.get(demo, util) is util # alternate default works
432
+ True
433
+ >>> lock.remaining_duration == datetime.timedelta()
434
+ True
435
+ >>> lock.end()
436
+ Traceback (most recent call last):
437
+ ...
438
+ zope.locking.interfaces.EndedError
439
+
440
+ Once a lock has ended, the timeout can no longer be changed.
441
+
442
+ >>> lock.duration = datetime.timedelta(days=2)
443
+ Traceback (most recent call last):
444
+ ...
445
+ zope.locking.interfaces.EndedError
446
+
447
+ We'll undo the hacks, and also end the lock (that is no longer ended once
448
+ the hack is finished).
449
+
450
+ >>> zope.locking.utils.now = oldNow # undo the hack
451
+ >>> lock.end()
452
+
453
+ Make sure to register tokens. Creating a lock but not registering it puts it
454
+ in a state that is not fully initialized.
455
+
456
+ >>> lock = tokens.ExclusiveLock(demo, 'john')
457
+ >>> lock.started # doctest: +ELLIPSIS
458
+ Traceback (most recent call last):
459
+ ...
460
+ zope.locking.interfaces.UnregisteredError: ...
461
+ >>> lock.ended # doctest: +ELLIPSIS
462
+ Traceback (most recent call last):
463
+ ...
464
+ zope.locking.interfaces.UnregisteredError: ...
465
+
466
+
467
+ Shared Locks
468
+ ============
469
+
470
+ Shared locks are very similar to exclusive locks, but take an iterable of one
471
+ or more principals at creation, and can have principals added or removed while
472
+ they are active.
473
+
474
+ In this example, also notice a convenient characteristic of the TokenUtility
475
+ `register` method: it also returns the token, so creation, registration, and
476
+ variable assignment can be chained, if desired.
477
+
478
+ >>> lock = util.register(tokens.SharedLock(demo, ('john', 'mary')))
479
+ >>> ev = events[-1]
480
+ >>> verifyObject(interfaces.ITokenStartedEvent, ev)
481
+ True
482
+ >>> ev.object is lock
483
+ True
484
+
485
+ Here, principals with ids of 'john' and 'mary' have locked the demo object.
486
+ The returned token implements ISharedLock and provides a superset of the
487
+ IExclusiveLock capabilities. These next operations should all look familiar
488
+ from the discussion of the ExclusiveLock tokens above.
489
+
490
+ >>> verifyObject(interfaces.ISharedLock, lock)
491
+ True
492
+ >>> lock.context is demo
493
+ True
494
+ >>> lock.__parent__ is demo # important for security
495
+ True
496
+ >>> lock.utility is util
497
+ True
498
+ >>> sorted(lock.principal_ids)
499
+ ['john', 'mary']
500
+ >>> lock.ended is None
501
+ True
502
+ >>> before_creation <= lock.started <= datetime.datetime.now(pytz.utc)
503
+ True
504
+ >>> lock.expiration is None
505
+ True
506
+ >>> lock.duration is None
507
+ True
508
+ >>> lock.remaining_duration is None
509
+ True
510
+ >>> lock.end()
511
+ >>> lock.ended >= lock.started
512
+ True
513
+
514
+ As mentioned, though, the SharedLock capabilities are a superset of the
515
+ ExclusiveLock ones. There are two extra methods: `add` and `remove`. These
516
+ are able to add and remove principal ids as shared owners of the lock token.
517
+
518
+ >>> lock = util.register(tokens.SharedLock(demo, ('john',)))
519
+ >>> sorted(lock.principal_ids)
520
+ ['john']
521
+ >>> lock.add(('mary',))
522
+ >>> sorted(lock.principal_ids)
523
+ ['john', 'mary']
524
+ >>> lock.add(('alice',))
525
+ >>> sorted(lock.principal_ids)
526
+ ['alice', 'john', 'mary']
527
+ >>> lock.remove(('john',))
528
+ >>> sorted(lock.principal_ids)
529
+ ['alice', 'mary']
530
+ >>> lock.remove(('mary',))
531
+ >>> sorted(lock.principal_ids)
532
+ ['alice']
533
+
534
+ Adding and removing principals fires appropriate events, as you might expect.
535
+
536
+ >>> lock.add(('mary',))
537
+ >>> sorted(lock.principal_ids)
538
+ ['alice', 'mary']
539
+ >>> ev = events[-1]
540
+ >>> verifyObject(interfaces.IPrincipalsChangedEvent, ev)
541
+ True
542
+ >>> ev.object is lock
543
+ True
544
+ >>> sorted(ev.old)
545
+ ['alice']
546
+ >>> lock.remove(('alice',))
547
+ >>> sorted(lock.principal_ids)
548
+ ['mary']
549
+ >>> ev = events[-1]
550
+ >>> verifyObject(interfaces.IPrincipalsChangedEvent, ev)
551
+ True
552
+ >>> ev.object is lock
553
+ True
554
+ >>> sorted(ev.old)
555
+ ['alice', 'mary']
556
+
557
+ Removing all participants in a lock ends the lock, making it ended.
558
+
559
+ >>> lock.remove(('mary',))
560
+ >>> sorted(lock.principal_ids)
561
+ []
562
+ >>> lock.ended >= lock.started
563
+ True
564
+ >>> ev = events[-1]
565
+ >>> verifyObject(interfaces.IPrincipalsChangedEvent, ev)
566
+ True
567
+ >>> ev.object is lock
568
+ True
569
+ >>> sorted(ev.old)
570
+ ['mary']
571
+ >>> ev = events[-2]
572
+ >>> verifyObject(interfaces.ITokenEndedEvent, ev)
573
+ True
574
+ >>> ev.object is lock
575
+ True
576
+
577
+ As you might expect, trying to add (or remove!) users from an ended lock is
578
+ an error.
579
+
580
+ >>> lock.add(('john',))
581
+ Traceback (most recent call last):
582
+ ...
583
+ zope.locking.interfaces.EndedError
584
+ >>> lock.remove(('john',))
585
+ Traceback (most recent call last):
586
+ ...
587
+ zope.locking.interfaces.EndedError
588
+
589
+ The token utility keeps track of shared lock tokens the same as exclusive lock
590
+ tokens. Here's a quick summary in code.
591
+
592
+ >>> lock = util.register(tokens.SharedLock(demo, ('john', 'mary')))
593
+ >>> util.get(demo) is lock
594
+ True
595
+ >>> list(util.iterForPrincipalId('john')) == [lock]
596
+ True
597
+ >>> list(util.iterForPrincipalId('mary')) == [lock]
598
+ True
599
+ >>> list(util) == [lock]
600
+ True
601
+ >>> util.register(tokens.ExclusiveLock(demo, 'mary'))
602
+ ... # doctest: +ELLIPSIS
603
+ Traceback (most recent call last):
604
+ ...
605
+ zope.locking.interfaces.RegistrationError: ...
606
+ >>> util.register(tokens.SharedLock(demo, ('mary', 'jane')))
607
+ ... # doctest: +ELLIPSIS
608
+ Traceback (most recent call last):
609
+ ...
610
+ zope.locking.interfaces.RegistrationError: ...
611
+ >>> util.register(tokens.Freeze(demo))
612
+ ... # doctest: +ELLIPSIS
613
+ Traceback (most recent call last):
614
+ ...
615
+ zope.locking.interfaces.RegistrationError: ...
616
+ >>> lock.end()
617
+
618
+ Timed expirations work the same as with exclusive locks. We won't repeat that
619
+ here, though look in the annoying.txt document in this package for the actual
620
+ repeated tests.
621
+
622
+
623
+ EndableFreezes
624
+ ==============
625
+
626
+ An endable freeze token is similar to a lock token except that it grants the
627
+ 'lock' to no one.
628
+
629
+ >>> token = util.register(tokens.EndableFreeze(demo))
630
+ >>> verifyObject(interfaces.IEndableFreeze, token)
631
+ True
632
+ >>> ev = events[-1]
633
+ >>> verifyObject(interfaces.ITokenStartedEvent, ev)
634
+ True
635
+ >>> ev.object is token
636
+ True
637
+ >>> sorted(token.principal_ids)
638
+ []
639
+ >>> token.end()
640
+
641
+ Endable freezes are otherwise identical to exclusive locks. See annoying.txt
642
+ for the comprehensive copy-and-paste tests duplicating the exclusive lock
643
+ tests. Notice that an EndableFreeze will never be a part of an iterable of
644
+ tokens by principal: by definition, a freeze is associated with no principals.
645
+
646
+
647
+ Freezes
648
+ =======
649
+
650
+ Freezes are similar to EndableFreezes, except they are not endable. They are
651
+ intended to be used by system level operations that should permanently disable
652
+ certain changes, such as changes to the content of an archived object version.
653
+
654
+ Creating them is the same...
655
+
656
+ >>> token = util.register(tokens.Freeze(demo))
657
+ >>> verifyObject(interfaces.IFreeze, token)
658
+ True
659
+ >>> ev = events[-1]
660
+ >>> verifyObject(interfaces.ITokenStartedEvent, ev)
661
+ True
662
+ >>> ev.object is token
663
+ True
664
+ >>> sorted(token.principal_ids)
665
+ []
666
+
667
+ But they can't go away...
668
+
669
+ >>> token.end()
670
+ Traceback (most recent call last):
671
+ ...
672
+ AttributeError: 'Freeze' object has no attribute 'end'
673
+
674
+ They also do not have expirations, duration, remaining durations, or ended
675
+ dates. They are permanent, unless you go into the database to muck with
676
+ implementation-specific data structures.
677
+
678
+ There is no API way to end a Freeze. We'll need to make a new object for the
679
+ rest of our demonstrations, and this token will exist through the
680
+ remaining examples.
681
+
682
+ >>> old_demo = demo
683
+ >>> demo = Demo()
684
+
685
+ ===============================
686
+ User API, Adapters and Security
687
+ ===============================
688
+
689
+ The API discussed so far makes few concessions to some of the common use cases
690
+ for locking. Here are some particular needs as yet unfulfilled by the
691
+ discussion so far.
692
+
693
+ - It should be possible to allow and deny per object whether users may
694
+ create and register tokens for the object.
695
+
696
+ - It should often be easier to register an endable token than a permanent
697
+ token.
698
+
699
+ - All users should be able to unlock or modify some aspects of their own
700
+ tokens, or remove their own participation in shared tokens; but it should be
701
+ possible to restrict access to ending tokens that users do not own (often
702
+ called "breaking locks").
703
+
704
+ In the context of the Zope 3 security model, the first two needs are intended
705
+ to be addressed by the ITokenBroker interface, and associated adapter; the last
706
+ need is intended to be addressed by the ITokenHandler, and associated
707
+ adapters.
708
+
709
+
710
+ TokenBrokers
711
+ ============
712
+
713
+ Token brokers adapt an object, which is the object whose tokens are
714
+ brokered, and uses this object as a security context. They provide a few
715
+ useful methods: `lock`, `lockShared`, `freeze`, and `get`. The TokenBroker
716
+ expects to be a trusted adapter.
717
+
718
+ lock
719
+ ----
720
+
721
+ The lock method creates and registers an exclusive lock. Without arguments,
722
+ it tries to create it for the user in the current interaction.
723
+
724
+ This won't work without an interaction, of course. Notice that we start the
725
+ example by registering the utility. We would normally be required to put the
726
+ utility in a site package, so that it would be persistent, but for this
727
+ demonstration we are simplifying the registration.
728
+
729
+ >>> component.provideUtility(util, provides=interfaces.ITokenUtility)
730
+
731
+ >>> import zope.interface.interfaces
732
+ >>> @interface.implementer(zope.interface.interfaces.IComponentLookup)
733
+ ... @component.adapter(interface.Interface)
734
+ ... def siteManager(obj):
735
+ ... return component.getGlobalSiteManager()
736
+ ...
737
+ >>> component.provideAdapter(siteManager)
738
+
739
+ >>> from zope.locking import adapters
740
+ >>> component.provideAdapter(adapters.TokenBroker)
741
+ >>> broker = interfaces.ITokenBroker(demo)
742
+ >>> broker.lock()
743
+ Traceback (most recent call last):
744
+ ...
745
+ ValueError
746
+ >>> broker.lock('joe')
747
+ Traceback (most recent call last):
748
+ ...
749
+ zope.locking.interfaces.ParticipationError
750
+
751
+ If we set up an interaction with one participation, the lock will have a
752
+ better chance.
753
+
754
+ >>> import zope.security.interfaces
755
+ >>> @interface.implementer(zope.security.interfaces.IPrincipal)
756
+ ... class DemoPrincipal(object):
757
+ ... def __init__(self, id, title=None, description=None):
758
+ ... self.id = id
759
+ ... self.title = title
760
+ ... self.description = description
761
+ ...
762
+ >>> joe = DemoPrincipal('joe')
763
+ >>> import zope.security.management
764
+ >>> @interface.implementer(zope.security.interfaces.IParticipation)
765
+ ... class DemoParticipation(object):
766
+ ... def __init__(self, principal):
767
+ ... self.principal = principal
768
+ ... self.interaction = None
769
+ ...
770
+ >>> zope.security.management.endInteraction()
771
+ >>> zope.security.management.newInteraction(DemoParticipation(joe))
772
+
773
+ >>> token = broker.lock()
774
+ >>> interfaces.IExclusiveLock.providedBy(token)
775
+ True
776
+ >>> token.context is demo
777
+ True
778
+ >>> token.__parent__ is demo
779
+ True
780
+ >>> sorted(token.principal_ids)
781
+ ['joe']
782
+ >>> token.started is not None
783
+ True
784
+ >>> util.get(demo) is token
785
+ True
786
+ >>> token.end()
787
+
788
+ You can only specify principals that are in the current interaction.
789
+
790
+ >>> token = broker.lock('joe')
791
+ >>> sorted(token.principal_ids)
792
+ ['joe']
793
+ >>> token.end()
794
+ >>> broker.lock('mary')
795
+ Traceback (most recent call last):
796
+ ...
797
+ zope.locking.interfaces.ParticipationError
798
+
799
+ The method can take a duration.
800
+
801
+ >>> token = broker.lock(duration=two)
802
+ >>> token.duration == two
803
+ True
804
+ >>> token.end()
805
+
806
+ If the interaction has more than one principal, a principal (in the
807
+ interaction) must be specified.
808
+
809
+ >>> mary = DemoPrincipal('mary')
810
+ >>> participation = DemoParticipation(mary)
811
+ >>> zope.security.management.getInteraction().add(participation)
812
+ >>> broker.lock()
813
+ Traceback (most recent call last):
814
+ ...
815
+ ValueError
816
+ >>> broker.lock('susan')
817
+ Traceback (most recent call last):
818
+ ...
819
+ zope.locking.interfaces.ParticipationError
820
+ >>> token = broker.lock('joe')
821
+ >>> sorted(token.principal_ids)
822
+ ['joe']
823
+ >>> token.end()
824
+ >>> token = broker.lock('mary')
825
+ >>> sorted(token.principal_ids)
826
+ ['mary']
827
+ >>> token.end()
828
+ >>> zope.security.management.endInteraction()
829
+
830
+ lockShared
831
+ ----------
832
+
833
+ The `lockShared` method has similar characteristics, except that it can handle
834
+ multiple principals.
835
+
836
+ Without an interaction, principals are either not found, or not part of the
837
+ interaction:
838
+
839
+ >>> broker.lockShared()
840
+ Traceback (most recent call last):
841
+ ...
842
+ ValueError
843
+ >>> broker.lockShared(('joe',))
844
+ Traceback (most recent call last):
845
+ ...
846
+ zope.locking.interfaces.ParticipationError
847
+
848
+ With an interaction, the principals get the lock by default.
849
+
850
+ >>> zope.security.management.newInteraction(DemoParticipation(joe))
851
+
852
+ >>> token = broker.lockShared()
853
+ >>> interfaces.ISharedLock.providedBy(token)
854
+ True
855
+ >>> token.context is demo
856
+ True
857
+ >>> token.__parent__ is demo
858
+ True
859
+ >>> sorted(token.principal_ids)
860
+ ['joe']
861
+ >>> token.started is not None
862
+ True
863
+ >>> util.get(demo) is token
864
+ True
865
+ >>> token.end()
866
+
867
+ You can only specify principals that are in the current interaction.
868
+
869
+ >>> token = broker.lockShared(('joe',))
870
+ >>> sorted(token.principal_ids)
871
+ ['joe']
872
+ >>> token.end()
873
+ >>> broker.lockShared(('mary',))
874
+ Traceback (most recent call last):
875
+ ...
876
+ zope.locking.interfaces.ParticipationError
877
+
878
+ The method can take a duration.
879
+
880
+ >>> token = broker.lockShared(duration=two)
881
+ >>> token.duration == two
882
+ True
883
+ >>> token.end()
884
+
885
+ If the interaction has more than one principal, all are included, unless some
886
+ are singled out.
887
+
888
+ >>> participation = DemoParticipation(mary)
889
+ >>> zope.security.management.getInteraction().add(participation)
890
+ >>> token = broker.lockShared()
891
+ >>> sorted(token.principal_ids)
892
+ ['joe', 'mary']
893
+ >>> token.end()
894
+ >>> token = broker.lockShared(('joe',))
895
+ >>> sorted(token.principal_ids)
896
+ ['joe']
897
+ >>> token.end()
898
+ >>> token = broker.lockShared(('mary',))
899
+ >>> sorted(token.principal_ids)
900
+ ['mary']
901
+ >>> token.end()
902
+ >>> zope.security.management.endInteraction()
903
+
904
+ freeze
905
+ ------
906
+
907
+ The `freeze` method allows users to create an endable freeze. It has no
908
+ requirements on the interaction. It should be protected carefully, from a
909
+ security perspective.
910
+
911
+ >>> token = broker.freeze()
912
+ >>> interfaces.IEndableFreeze.providedBy(token)
913
+ True
914
+ >>> token.context is demo
915
+ True
916
+ >>> token.__parent__ is demo
917
+ True
918
+ >>> sorted(token.principal_ids)
919
+ []
920
+ >>> token.started is not None
921
+ True
922
+ >>> util.get(demo) is token
923
+ True
924
+ >>> token.end()
925
+
926
+ The method can take a duration.
927
+
928
+ >>> token = broker.freeze(duration=two)
929
+ >>> token.duration == two
930
+ True
931
+ >>> token.end()
932
+
933
+ get
934
+ ---
935
+
936
+ The `get` method is exactly equivalent to the token utility's get method:
937
+ it returns the current active token for the object, or None. It is useful
938
+ for protected code, since utilities typically do not get security assertions,
939
+ and this method can get its security assertions from the object, which is
940
+ often the right place.
941
+
942
+ Again, the TokenBroker does embody some policy; if it is not good policy for
943
+ your application, build your own interfaces and adapters that do.
944
+
945
+
946
+ TokenHandlers
947
+ =============
948
+
949
+ TokenHandlers are useful for endable tokens with one or more principals--that
950
+ is, locks, but not freezes. They are intended to be protected with a lower
951
+ external security permission then the usual token methods and attributes, and
952
+ then impose their own checks on the basis of the current interaction. They are
953
+ very much policy, and other approaches may be useful. They are intended to be
954
+ registered as trusted adapters.
955
+
956
+ For exclusive locks and shared locks, then, we have token handlers.
957
+ Generally, token handlers give access to all of the same capabilities as their
958
+ corresponding tokens, with the following additional constraints and
959
+ capabilities:
960
+
961
+ - `expiration`, `duration`, and `remaining_duration` all may be set only if
962
+ all the principals in the current interaction are owners of the wrapped
963
+ token; and
964
+
965
+ - `release` removes some or all of the principals in the interaction if all
966
+ the principals in the current interaction are owners of the wrapped token.
967
+
968
+ Note that `end` is unaffected: this is effectively "break lock", while
969
+ `release` is effectively "unlock". Permissions should be set accordingly.
970
+
971
+ Shared lock handlers have two additional methods that are discussed in their
972
+ section.
973
+
974
+ ExclusiveLockHandlers
975
+ ---------------------
976
+
977
+ Given the general constraints described above, exclusive lock handlers will
978
+ generally only allow access to their special capabilities if the operation
979
+ is in an interaction with only the lock owner.
980
+
981
+ >>> zope.security.management.newInteraction(DemoParticipation(joe))
982
+ >>> component.provideAdapter(adapters.ExclusiveLockHandler)
983
+ >>> lock = broker.lock()
984
+ >>> handler = interfaces.IExclusiveLockHandler(lock)
985
+ >>> verifyObject(interfaces.IExclusiveLockHandler, handler)
986
+ True
987
+ >>> handler.__parent__ is lock
988
+ True
989
+ >>> handler.expiration is None
990
+ True
991
+ >>> handler.duration = two
992
+ >>> lock.duration == two
993
+ True
994
+ >>> handler.expiration = handler.started + three
995
+ >>> lock.expiration == handler.started + three
996
+ True
997
+ >>> handler.remaining_duration = two
998
+ >>> lock.remaining_duration <= two
999
+ True
1000
+ >>> handler.release()
1001
+ >>> handler.ended >= handler.started
1002
+ True
1003
+ >>> lock.ended >= lock.started
1004
+ True
1005
+ >>> lock = util.register(tokens.ExclusiveLock(demo, 'mary'))
1006
+ >>> handler = interfaces.ITokenHandler(lock) # for joe's interaction still
1007
+ >>> handler.duration = two # doctest: +ELLIPSIS
1008
+ Traceback (most recent call last):
1009
+ ...
1010
+ zope.locking.interfaces.ParticipationError: ...
1011
+ >>> handler.expiration = handler.started + three # doctest: +ELLIPSIS
1012
+ Traceback (most recent call last):
1013
+ ...
1014
+ zope.locking.interfaces.ParticipationError: ...
1015
+ >>> handler.remaining_duration = two # doctest: +ELLIPSIS
1016
+ Traceback (most recent call last):
1017
+ ...
1018
+ zope.locking.interfaces.ParticipationError: ...
1019
+ >>> handler.release() # doctest: +ELLIPSIS
1020
+ Traceback (most recent call last):
1021
+ ...
1022
+ zope.locking.interfaces.ParticipationError: ...
1023
+ >>> lock.end()
1024
+
1025
+ SharedLockHandlers
1026
+ ------------------
1027
+
1028
+ Shared lock handlers let anyone who is an owner of a token set the expiration,
1029
+ duration, and remaining_duration values. This is a 'get out of the way' policy
1030
+ that relies on social interactions to make sure all the participants are
1031
+ represented as they want. Other policies could be written in other adapters.
1032
+
1033
+ >>> component.provideAdapter(adapters.SharedLockHandler)
1034
+ >>> lock = util.register(tokens.SharedLock(demo, ('joe', 'mary')))
1035
+ >>> handler = interfaces.ITokenHandler(lock) # for joe's interaction still
1036
+ >>> verifyObject(interfaces.ISharedLockHandler, handler)
1037
+ True
1038
+ >>> handler.__parent__ is lock
1039
+ True
1040
+ >>> handler.expiration is None
1041
+ True
1042
+ >>> handler.duration = two
1043
+ >>> lock.duration == two
1044
+ True
1045
+ >>> handler.expiration = handler.started + three
1046
+ >>> lock.expiration == handler.started + three
1047
+ True
1048
+ >>> handler.remaining_duration = two
1049
+ >>> lock.remaining_duration <= two
1050
+ True
1051
+ >>> sorted(handler.principal_ids)
1052
+ ['joe', 'mary']
1053
+ >>> handler.release()
1054
+ >>> sorted(handler.principal_ids)
1055
+ ['mary']
1056
+ >>> handler.duration = two # doctest: +ELLIPSIS
1057
+ Traceback (most recent call last):
1058
+ ...
1059
+ zope.locking.interfaces.ParticipationError: ...
1060
+ >>> handler.expiration = handler.started + three # doctest: +ELLIPSIS
1061
+ Traceback (most recent call last):
1062
+ ...
1063
+ zope.locking.interfaces.ParticipationError: ...
1064
+ >>> handler.remaining_duration = two # doctest: +ELLIPSIS
1065
+ Traceback (most recent call last):
1066
+ ...
1067
+ zope.locking.interfaces.ParticipationError: ...
1068
+ >>> handler.release() # doctest: +ELLIPSIS
1069
+ Traceback (most recent call last):
1070
+ ...
1071
+ zope.locking.interfaces.ParticipationError: ...
1072
+
1073
+ The shared lock handler adds two additional methods to a standard handler:
1074
+ `join` and `add`. They do similar jobs, but are separate to allow separate
1075
+ security settings for each. The `join` method lets some or all of the
1076
+ principals in the current interaction join.
1077
+
1078
+ >>> handler.join()
1079
+ >>> sorted(handler.principal_ids)
1080
+ ['joe', 'mary']
1081
+ >>> handler.join(('susan',))
1082
+ Traceback (most recent call last):
1083
+ ...
1084
+ zope.locking.interfaces.ParticipationError
1085
+
1086
+ The `add` method lets any principal ids be added to the lock, but all
1087
+ principals in the current interaction must be a part of the lock.
1088
+
1089
+ >>> handler.add(('susan',))
1090
+ >>> sorted(handler.principal_ids)
1091
+ ['joe', 'mary', 'susan']
1092
+ >>> handler.release()
1093
+ >>> handler.add('jake') # doctest: +ELLIPSIS
1094
+ Traceback (most recent call last):
1095
+ ...
1096
+ zope.locking.interfaces.ParticipationError: ...
1097
+ >>> lock.end()
1098
+ >>> zope.security.management.endInteraction()
1099
+
1100
+
1101
+ Warnings
1102
+ ========
1103
+
1104
+ * The token utility will register a token for an object if it can. It does not
1105
+ check to see if it is actually the local token utility for the given object.
1106
+ This should be arranged by clients of the token utility, and verified
1107
+ externally if desired.
1108
+
1109
+ * Tokens are stored as keys in BTrees, and therefore must be orderable
1110
+ (i.e., they must implement __cmp__).
1111
+
1112
+
1113
+ Intended Security Configuration
1114
+ ===============================
1115
+
1116
+ Utilities are typically unprotected in Zope 3--or more accurately, have
1117
+ no security assertions and are used with no security proxy--and the token
1118
+ utility expects to be so. As such, the broker and handler objects are
1119
+ expected to be the objects used by view code, and so associated with security
1120
+ proxies. All should have appropriate __parent__ attribute values. The
1121
+ ability to mutate the tokens--`end`, `add` and `remove` methods, for
1122
+ instance--should be protected with an administrator-type permission such as
1123
+ 'zope.Security'. Setting the timeout properties on the token should be
1124
+ protected in the same way. Setting the handlers attributes can have a less
1125
+ restrictive setting, since they calculate security themselves on the basis of
1126
+ lock membership.
1127
+
1128
+ On the adapter, the `end` method should be protected with the same or
1129
+ similar permission. Calling methods such as lock and lockShared should be
1130
+ protected with something like 'zope.ManageContent'. Getting attributes should
1131
+ be 'zope.View' or 'zope.Public', and unlocking and setting the timeouts, since
1132
+ they are already protected to make sure the principal is a member of the lock,
1133
+ can probably be 'zope.Public'.
1134
+
1135
+ These settings can be abused relatively easily to create an insecure
1136
+ system--for instance, if a user can get an adapter to IPrincipalLockable for
1137
+ another principal--but are a reasonable start.
1138
+
1139
+ >>> broker.__parent__ is demo
1140
+ True
1141
+ >>> handler.__parent__ is lock
1142
+ True
1143
+
1144
+
1145
+ Random Thoughts
1146
+ ===============
1147
+
1148
+ As a side effect of the design, it is conceivable that multiple lock utilities
1149
+ could be in use at once, governing different aspects of an object; however,
1150
+ this may never itself be of use.
1151
+
1152
+
1153
+ =======
1154
+ Changes
1155
+ =======
1156
+
1157
+ 3.0 (2025-09-04)
1158
+ ================
1159
+
1160
+ - Add support for Python 3.9, 3.10, 3.11, 3.12, 3.13.
1161
+
1162
+ - Drop support for Python 2.7, 3.5, 3.6, 3.7, 3.8.
1163
+
1164
+
1165
+ 2.1.0 (2020-04-15)
1166
+ ==================
1167
+
1168
+ - Fix DeprecationWarnings for ObjectEvent.
1169
+
1170
+ - Add support for Python 3.7 and 3.8.
1171
+
1172
+ - Drop support for Python 3.3 and 3.4.
1173
+
1174
+
1175
+ 2.0.0 (2018-01-23)
1176
+ ==================
1177
+
1178
+ - Python 3 compatibility.
1179
+
1180
+ - Note: The browser views and related code where removed. You need to provide
1181
+ those in application-level code now.
1182
+
1183
+ - Package the zcml files.
1184
+
1185
+ - Updated dependencies.
1186
+
1187
+ - Revived from svn.zope.org
1188
+
1189
+
1190
+ 1.2.2 (2011-01-31)
1191
+ ==================
1192
+
1193
+ - Consolidate duplicate evolution code.
1194
+
1195
+ - Split generations config into its own zcml file.
1196
+
1197
+
1198
+ 1.2.1 (2010-01-20)
1199
+ ==================
1200
+
1201
+ - Bug fix: the generation added in 1.2 did not properly clean up
1202
+ expired tokens, and could leave the token utility in an inconsistent
1203
+ state.
1204
+
1205
+
1206
+ 1.2 (2009-11-23)
1207
+ ================
1208
+
1209
+ - Bug fix: tokens were stored in a manner that prevented them from
1210
+ being cleaned up properly in the utility's _principal_ids mapping.
1211
+ Make zope.locking.tokens.Token orderable to fix this, as tokens
1212
+ are stored as keys in BTrees.
1213
+
1214
+ - Add a zope.app.generations Schema Manager to clean up any lingering
1215
+ tokens due to this bug. Token utilities not accessible through the
1216
+ component registry can be cleaned up manually with
1217
+ zope.locking.generations.fix_token_utility.
1218
+
1219
+ - TokenUtility's register method will now add the token to the utility's
1220
+ database connection if the token provides IPersistent.
1221
+
1222
+ - Clean up the tests and docs and move some common code to testing.py.
1223
+
1224
+ - Fix some missing imports.
1225
+
1226
+
1227
+ 1.1
1228
+ ===
1229
+
1230
+ (series for Zope 3.4; eggs)
1231
+
1232
+ 1.1b
1233
+ ====
1234
+
1235
+ - converted to use eggs
1236
+
1237
+
1238
+ 1.0
1239
+ ===
1240
+
1241
+ (series for Zope 3.3; no dependencies on Zope eggs)
1242
+
1243
+ 1.0b
1244
+ ====
1245
+
1246
+ Initial non-dev release