cs-threads 20250306__py2.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.
cs/threads.py ADDED
@@ -0,0 +1,831 @@
1
+ #!/usr/bin/python
2
+ #
3
+ # Thread convenience facilities.
4
+ # - Cameron Simpson <cs@cskk.id.au> 18nov2007
5
+ #
6
+
7
+ ''' Thread related convenience classes and functions.
8
+ '''
9
+
10
+ from collections import defaultdict, namedtuple
11
+ from contextlib import contextmanager
12
+ from heapq import heappush, heappop
13
+ from inspect import ismethod
14
+ import sys
15
+ from threading import (
16
+ current_thread,
17
+ Lock,
18
+ Semaphore,
19
+ Thread as builtin_Thread,
20
+ local as thread_local,
21
+ )
22
+
23
+ from cs.context import (
24
+ ContextManagerMixin,
25
+ stackattrs,
26
+ stackset,
27
+ closeall,
28
+ )
29
+ from cs.deco import decorator
30
+ from cs.excutils import logexc, transmute
31
+ from cs.gimmicks import error, warning
32
+ from cs.pfx import Pfx # prefix
33
+ from cs.py.func import funcname, prop
34
+ from cs.py.stack import caller
35
+ from cs.seq import Seq
36
+
37
+ __version__ = '20250306'
38
+
39
+ DISTINFO = {
40
+ 'description':
41
+ "threading and communication/synchronisation conveniences",
42
+ 'keywords': ["python2", "python3"],
43
+ 'classifiers': [
44
+ "Programming Language :: Python",
45
+ "Programming Language :: Python :: 3",
46
+ ],
47
+ 'install_requires': [
48
+ 'cs.context',
49
+ 'cs.deco',
50
+ 'cs.excutils',
51
+ 'cs.gimmicks',
52
+ 'cs.pfx',
53
+ 'cs.py.func',
54
+ 'cs.py.stack',
55
+ 'cs.seq',
56
+ ],
57
+ }
58
+
59
+ class ThreadState(thread_local):
60
+ ''' A `Thread` local object with attributes
61
+ which can be used as a context manager to stack attribute values.
62
+
63
+ Example:
64
+
65
+ from cs.threads import ThreadState
66
+
67
+ S = ThreadState(verbose=False)
68
+
69
+ with S(verbose=True) as prev_attrs:
70
+ if S.verbose:
71
+ print("verbose! (formerly verbose=%s)" % prev_attrs['verbose'])
72
+ '''
73
+
74
+ def __init__(self, **kw):
75
+ ''' Initiate the `ThreadState`, providing the per-Thread initial values.
76
+ '''
77
+ thread_local.__init__(self)
78
+ for k, v in kw.items():
79
+ setattr(self, k, v)
80
+
81
+ def __str__(self):
82
+ return "%s(%s)" % (
83
+ type(self).__name__,
84
+ ','.join("%s=%r" % kv for kv in self.__dict__.items())
85
+ )
86
+
87
+ __repr__ = __str__
88
+
89
+ @contextmanager
90
+ def __call__(self, **kw):
91
+ ''' Calling a `ThreadState` returns a context manager which stacks some state.
92
+ The context manager yields the previous values
93
+ for the attributes which were stacked.
94
+ '''
95
+ with stackattrs(self, **kw) as prev_attrs:
96
+ yield prev_attrs
97
+
98
+ # backward compatible deprecated name
99
+ State = ThreadState
100
+
101
+ # TODO: what to do about overlapping HasThreadState usage of a particular class?
102
+ class HasThreadState(ContextManagerMixin):
103
+ ''' A mixin for classes with a `cs.threads.ThreadState` instance as `.state`
104
+ providing a context manager which pushes `current=self` onto that state
105
+ and a `default()` class method returning `cls.perthread_state.current`
106
+ as the default instance of that class.
107
+
108
+ *NOTE*: the documentation here refers to `cls.perthread_state`, but in
109
+ fact we honour the `cls.THREAD_STATE_ATTR` attribute to name
110
+ the state attribute which allows perclass state attributes,
111
+ and also use with classes which already use `.perthread_state` for
112
+ another purpose.
113
+
114
+ *NOTE*: `HasThreadState.Thread` is a _class_ method whose default
115
+ is to push state for all active `HasThreadState` subclasses.
116
+ Contrast with `HasThreadState.bg` which is an _instance_method
117
+ whose default is to push state for just that instance.
118
+ The top level `cs.threads.bg` function calls `HasThreadState.Thread`
119
+ to obtain its `Thread`.
120
+ '''
121
+
122
+ _HasThreadState_lock = Lock()
123
+ _HasThreadState_classes = set()
124
+
125
+ # the default name for the Thread state attribute
126
+ THREAD_STATE_ATTR = 'perthread_state'
127
+
128
+ @classmethod
129
+ def default(cls, *, factory=None, raise_on_None=False):
130
+ ''' The default instance of this class from `cls.perthread_state.current`.
131
+
132
+ Parameters:
133
+ * `factory`: optional callable to create an instance of `cls`
134
+ if `cls.perthread_state.current` is `None` or missing;
135
+ if `factory` is `True` then `cls` is used as the factory
136
+ * `raise_on_None`: if `cls.perthread_state.current` is `None` or missing
137
+ and `factory` is false and `raise_on_None` is true,
138
+ raise a `RuntimeError`;
139
+ this is primarily a debugging aid
140
+ '''
141
+ current = getattr(getattr(cls, cls.THREAD_STATE_ATTR), 'current', None)
142
+ if current is None:
143
+ if factory:
144
+ if factory is True:
145
+ factory = cls
146
+ return factory()
147
+ if raise_on_None:
148
+ raise RuntimeError(
149
+ "%s.default: %s.%s.current is missing/None and ifNone is None" %
150
+ (cls.__name__, cls.__name__, cls.THREAD_STATE_ATTR)
151
+ )
152
+ return current
153
+
154
+ def __enter_exit__(self):
155
+ ''' Push `self.perthread_state.current=self` as the `Thread` local current instance.
156
+
157
+ Include `self.__class__` in the set of currently active classes for the duration.
158
+ '''
159
+ cls = self.__class__
160
+ with cls._HasThreadState_lock:
161
+ stacked = stackset(
162
+ HasThreadState._HasThreadState_classes, cls, cls._HasThreadState_lock
163
+ )
164
+ with stacked:
165
+ state = getattr(cls, cls.THREAD_STATE_ATTR)
166
+ with state(current=self):
167
+ yield
168
+
169
+ @classmethod
170
+ def get_thread_states(cls, all_classes=None):
171
+ ''' Return a mapping of `class`->*current_instance*`
172
+ for use with `HasThreadState.with_thread_states`
173
+ or `HasThreadState.Thread` or `HasThreadState.bg`.
174
+
175
+ The default behaviour returns just a mapping for this class,
176
+ expecting the default instance to be responsible for what
177
+ other resources it holds.
178
+
179
+ There is also a legacy mode for `all_classes=True`
180
+ where the mapping is for all active classes,
181
+ probably best used for `Thread`s spawned outside
182
+ a `HasThreadState` context.
183
+
184
+ Parameters:
185
+ * `all_classes`: optional flag, default `False`;
186
+ if true, return a mapping of class to current instance
187
+ for all `HasThreadState` subclasses with an open instance,
188
+ otherwise just a mapping from this class to its current instance
189
+ '''
190
+ if all_classes is None:
191
+ all_classes = False
192
+ with cls._HasThreadState_lock:
193
+ if all_classes:
194
+ # the "current" instance for every HasThreadState._HasThreadState_classes
195
+ currency = {
196
+ htscls:
197
+ getattr(
198
+ getattr(htscls, htscls.THREAD_STATE_ATTR), 'current', None
199
+ )
200
+ for htscls in HasThreadState._HasThreadState_classes
201
+ }
202
+ elif cls is HasThreadState:
203
+ currency = {}
204
+ else:
205
+ # just the current instance of the calling class
206
+ currency = {
207
+ cls: getattr(getattr(cls, cls.THREAD_STATE_ATTR), 'current', None)
208
+ }
209
+ return currency
210
+
211
+ @classmethod
212
+ def Thread(
213
+ cls,
214
+ *,
215
+ name=None,
216
+ target,
217
+ enter_objects=None,
218
+ **Thread_kw,
219
+ ):
220
+ ''' Factory for a `Thread` to push the `.current` state for the
221
+ currently active classes.
222
+
223
+ The optional parameter `enter_objects` may be used to pass
224
+ an iterable of objects whose contexts should be entered
225
+ using `with obj:`.
226
+ If this is set to `True` that indicates that every "current"
227
+ `HasThreadStates` instance should be entered.
228
+ The default does not enter any object contexts.
229
+ The `HasThreadStates.bg` method defaults to passing
230
+ `enter_objects=(self,)` to enter the context for `self`.
231
+ '''
232
+ if name is None:
233
+ name = funcname(target)
234
+ if enter_objects is None:
235
+ # enter no objects
236
+ enter_tuples = ()
237
+ elif isinstance(enter_objects, bool):
238
+ # all the current objects, marked as for-with or not-for-with
239
+ for_with = enter_objects
240
+ enter_tuples = tuple(
241
+ (hts, for_with) for hts in cls.get_thread_states(True).values()
242
+ )
243
+ else:
244
+ # just the specified objects, marked as for-with
245
+ enter_tuples = ((enter_obj, True) for enter_obj in enter_objects)
246
+ enter_it = iter(enter_tuples)
247
+
248
+ def with_enter_objects():
249
+ ''' A recursive context manager to enter all the contexts
250
+ implied by `enter_it`.
251
+ For each `(enter_obj,for_with)` in `enter_it`, if `for_with`
252
+ is true, enter the object using `with enter_obj:` otherwise
253
+ enter using: `with enter_object.per_thread_state(current=enter_obj)`.
254
+ '''
255
+ try:
256
+ enter_obj, for_with = next(enter_it)
257
+ except StopIteration:
258
+ yield
259
+ else:
260
+ if enter_obj is None:
261
+ # no current object, skip to the next one
262
+ yield from with_enter_objects()
263
+ else:
264
+ if for_with:
265
+ with enter_obj:
266
+ yield from with_enter_objects()
267
+ else:
268
+ thread_state = getattr(enter_obj, enter_obj.THREAD_STATE_ATTR)
269
+ with thread_state(current=enter_obj):
270
+ yield from with_enter_objects()
271
+
272
+ def target_wrapper(*a, **kw):
273
+ ''' Wrapper for the `Thread.target` to push the current states
274
+ from `enter_it` in the new Thread before running the `target`.
275
+ '''
276
+ with contextmanager(with_enter_objects)():
277
+ return target(*a, **kw)
278
+
279
+ return builtin_Thread(name=name, target=target_wrapper, **Thread_kw)
280
+
281
+ def bg(self, func, *, enter_objects=None, **bg_kw):
282
+ ''' Get a `Thread` using `type(self).Thread` and start it.
283
+ Return the `Thread`.
284
+
285
+ The `HasThreadState.Thread` factory duplicates the current `Thread`'s
286
+ `HasThreadState` current objects as current in the new `Thread`.
287
+ Additionally it enters the contexts of various objects using
288
+ `with obj` according to the `enter_objects` parameter.
289
+
290
+ The value of the optional parameter `enter_objects` governs
291
+ which objects have their context entered using `with obj`
292
+ in the child `Thread` while running `func` as follows:
293
+ - `None`: the default, meaning `(self,)`
294
+ - `False`: no object contexts are entered
295
+ - `True`: all current `HasThreadState` object contexts will be entered
296
+ - an iterable of objects whose contexts will be entered;
297
+ pass `()` to enter no objects
298
+ '''
299
+ cls = type(self)
300
+ if enter_objects is None:
301
+ enter_objects = (self,)
302
+ # run the module level bg() function, below
303
+ return bg(
304
+ func,
305
+ thread_factory=cls.Thread,
306
+ enter_objects=enter_objects,
307
+ **bg_kw,
308
+ )
309
+
310
+ # pylint: disable=too-many-arguments
311
+ def bg(
312
+ func,
313
+ *,
314
+ daemon=None,
315
+ name=None,
316
+ no_start=False,
317
+ no_logexc=False,
318
+ args=None,
319
+ kwargs=None,
320
+ thread_factory=None,
321
+ pre_enter_objects=None,
322
+ **thread_factory_kw,
323
+ ):
324
+ ''' Dispatch the callable `func` in its own `Thread`;
325
+ return the `Thread`.
326
+
327
+ Parameters:
328
+ * `func`: a callable for the `Thread` target.
329
+ * `args`, `kwargs`: passed to the `Thread` constructor
330
+ * `kwargs`, `kwargs`: passed to the `Thread` constructor
331
+ * `daemon`: optional argument specifying the `.daemon` attribute.
332
+ * `name`: optional argument specifying the `Thread` name,
333
+ default: the name of `func`.
334
+ * `no_logexc`: if false (default `False`), wrap `func` in `@logexc`.
335
+ * `no_start`: optional argument, default `False`.
336
+ If true, do not start the `Thread`.
337
+ * `pre_enter_objects`: an optional iterable of objects which
338
+ should be entered using `with`
339
+
340
+ If `pre_enter_objects` is supplied, these objects will be
341
+ entered before the `Thread` is started and exited when the
342
+ `Thread` target function ends.
343
+ If the `Thread` is _not_ started (`no_start=True`, very
344
+ unusual) then it will be the caller's responsibility to manage
345
+ to entered objects.
346
+ '''
347
+ if name is None:
348
+ name = funcname(func)
349
+ if args is None:
350
+ args = ()
351
+ if kwargs is None:
352
+ kwargs = {}
353
+ if thread_factory is None:
354
+ thread_factory = HasThreadState.Thread
355
+ ##thread_prefix = prefix() + ': ' + name
356
+ thread_prefix = name
357
+ preopen_close = None
358
+
359
+ def thread_body():
360
+ ''' Establish a basic `Pfx` context for the target `func`.
361
+ '''
362
+ try:
363
+ with Pfx("Thread:%d:%s", current_thread().ident, thread_prefix):
364
+ return func(*args, **kwargs)
365
+ finally:
366
+ if preopen_close is not None:
367
+ # do the closes
368
+ preopen_close()
369
+
370
+ T = thread_factory(
371
+ name=thread_prefix,
372
+ target=thread_body,
373
+ **thread_factory_kw,
374
+ )
375
+ if not no_logexc:
376
+ func = logexc(func)
377
+ if daemon is not None:
378
+ T.daemon = daemon
379
+ if pre_enter_objects:
380
+ preopen_close = closeall(pre_enter_objects)
381
+ if not no_start:
382
+ T.start()
383
+ return T
384
+
385
+ def joinif(T: builtin_Thread):
386
+ ''' Call `T.join()` if `T` is not the current `Thread`.
387
+
388
+ Unlike `threading.Thread.join`, this function is a no-op if
389
+ `T` is the current `Thread.
390
+
391
+ The use case is situations such as the shutdown phase of the
392
+ `MultiOpenMixin.startup_shutdown` context manager. Because
393
+ the "initial open" startup phase is not necessarily run in
394
+ the same thread as the "final close" shutdown phase, it is
395
+ possible for example for a worker `Thread` to execute the
396
+ shutdown phase and try to join itself. Using this function
397
+ supports that scenario.
398
+ '''
399
+ if T is not current_thread():
400
+ T.join()
401
+
402
+ class AdjustableSemaphore(object):
403
+ ''' A semaphore whose value may be tuned after instantiation.
404
+ '''
405
+
406
+ def __init__(self, value=1, name="AdjustableSemaphore"):
407
+ self.limit0 = value
408
+ self.__sem = Semaphore(value)
409
+ self.__value = value
410
+ self.__name = name
411
+ self.__lock = Lock()
412
+
413
+ def __str__(self):
414
+ return "%s[%d]" % (self.__name, self.limit0)
415
+
416
+ def __enter__(self):
417
+ from cs.logutils import LogTime # pylint: disable=import-outside-toplevel
418
+ with LogTime("%s(%d).__enter__: acquire", self.__name, self.__value):
419
+ self.acquire()
420
+
421
+ def __exit__(self, exc_type, exc_value, traceback):
422
+ self.release()
423
+ return False
424
+
425
+ def release(self):
426
+ ''' Release the semaphore.
427
+ '''
428
+ self.__sem.release()
429
+
430
+ def acquire(self, blocking=True):
431
+ ''' The acquire() method calls the base acquire() method if not blocking.
432
+ If blocking is true, the base acquire() is called inside a lock to
433
+ avoid competing with a reducing adjust().
434
+ '''
435
+ if not blocking:
436
+ return self.__sem.acquire(blocking)
437
+ with self.__lock:
438
+ self.__sem.acquire(blocking) # pylint: disable=consider-using-with
439
+ return True
440
+
441
+ def adjust(self, newvalue):
442
+ ''' Set capacity to `newvalue`
443
+ by calling release() or acquire() an appropriate number of times.
444
+
445
+ If `newvalue` lowers the semaphore capacity then adjust()
446
+ may block until the overcapacity is released.
447
+ '''
448
+ if newvalue <= 0:
449
+ raise ValueError("invalid newvalue, should be > 0, got %s" % (newvalue,))
450
+ self.adjust_delta(newvalue - self.__value)
451
+
452
+ def adjust_delta(self, delta):
453
+ ''' Adjust capacity by `delta` by calling release() or acquire()
454
+ an appropriate number of times.
455
+
456
+ If `delta` lowers the semaphore capacity then adjust() may block
457
+ until the overcapacity is released.
458
+ '''
459
+ newvalue = self.__value + delta
460
+ with self.__lock:
461
+ if delta > 0:
462
+ while delta > 0:
463
+ self.__sem.release()
464
+ delta -= 1
465
+ else:
466
+ from cs.logutils import LogTime # pylint: disable=import-outside-toplevel
467
+ while delta < 0:
468
+ with LogTime("AdjustableSemaphore(%s): acquire excess capacity",
469
+ self.__name):
470
+ self.__sem.acquire(True) # pylint: disable=consider-using-with
471
+ delta += 1
472
+ self.__value = newvalue
473
+
474
+ @decorator
475
+ def locked(func, initial_timeout=10.0, lockattr='_lock'):
476
+ ''' A decorator for instance methods that must run within a lock.
477
+
478
+ Decorator keyword arguments:
479
+ * `initial_timeout`:
480
+ the initial lock attempt timeout;
481
+ if this is `>0` and exceeded a warning is issued
482
+ and then an indefinite attempt is made.
483
+ Default: `2.0`s
484
+ * `lockattr`:
485
+ the name of the attribute of `self`
486
+ which references the lock object.
487
+ Default `'_lock'`
488
+ '''
489
+ citation = "@locked(%s)" % (funcname(func),)
490
+
491
+ def lockfunc(self, *a, **kw):
492
+ ''' Obtain the lock and then call `func`.
493
+ '''
494
+ lock = getattr(self, lockattr)
495
+ if initial_timeout > 0 and lock.acquire(timeout=initial_timeout):
496
+ try:
497
+ return func(self, *a, **kw)
498
+ finally:
499
+ lock.release()
500
+ else:
501
+ if initial_timeout > 0:
502
+ warning(
503
+ "%s: timeout after %gs waiting for %s<%s>.%s, continuing to wait",
504
+ citation, initial_timeout,
505
+ type(self).__name__, self, lockattr
506
+ )
507
+ with lock:
508
+ return func(self, *a, **kw)
509
+
510
+ lockfunc.__name__ = citation
511
+ lockfunc.__doc__ = getattr(func, '__doc__', '')
512
+ return lockfunc
513
+
514
+ @decorator
515
+ def locked_property(
516
+ func, lock_name='_lock', prop_name=None, unset_object=None
517
+ ):
518
+ ''' A thread safe property whose value is cached.
519
+ The lock is taken if the value needs to computed.
520
+
521
+ The default lock attribute is `._lock`.
522
+ The default attribute for the cached value is `._`*funcname*
523
+ where *funcname* is `func.__name__`.
524
+ The default "unset" value for the cache is `None`.
525
+ '''
526
+ if prop_name is None:
527
+ prop_name = '_' + func.__name__
528
+
529
+ @transmute(exc_from=AttributeError)
530
+ def locked_property_getprop(self):
531
+ ''' Attempt lockless fetch of the property first.
532
+ Use lock if the property is unset.
533
+ '''
534
+ p = getattr(self, prop_name, unset_object)
535
+ if p is unset_object:
536
+ try:
537
+ lock = getattr(self, lock_name)
538
+ except AttributeError:
539
+ error("no %s.%s attribute", type(self).__name__, lock_name)
540
+ raise
541
+ with lock:
542
+ p = getattr(self, prop_name, unset_object)
543
+ if p is unset_object:
544
+ ##debug("compute %s...", prop_name)
545
+ p = func(self)
546
+ setattr(self, prop_name, p)
547
+ else:
548
+ ##debug("inside lock, already computed %s", prop_name)
549
+ pass
550
+ else:
551
+ ##debug("outside lock, already computed %s", prop_name)
552
+ pass
553
+ return p
554
+
555
+ return prop(locked_property_getprop)
556
+
557
+ class LockableMixin(object):
558
+ ''' Trite mixin to control access to an object via its `._lock` attribute.
559
+ Exposes the `._lock` as the property `.lock`.
560
+ Presents a context manager interface for obtaining an object's lock.
561
+ '''
562
+
563
+ def __enter__(self):
564
+ self._lock.acquire()
565
+
566
+ # pylint: disable=unused-argument
567
+ def __exit__(self, exc_type, exc_value, traceback):
568
+ self._lock.release()
569
+
570
+ @property
571
+ def lock(self):
572
+ ''' The internal lock object.
573
+ '''
574
+ return self._lock
575
+
576
+ def via(cmanager, func, *a, **kw):
577
+ ''' Return a callable that calls the supplied `func` inside a
578
+ `with` statement using the context manager `cmanager`.
579
+ This intended use case is aimed at deferred function calls.
580
+ '''
581
+
582
+ def via_func_wrapper():
583
+ with cmanager:
584
+ return func(*a, **kw)
585
+
586
+ return via_func_wrapper
587
+
588
+ class PriorityLockSubLock(namedtuple('PriorityLockSubLock',
589
+ 'name priority lock priority_lock')):
590
+ ''' The record for the per-`acquire`r `Lock` held by `PriorityLock.acquire`.
591
+ '''
592
+
593
+ def __str__(self):
594
+ return "%s(name=%r,priority=%s,lock=%s:%s,priority_lock=%r)" \
595
+ % (type(self).__name__,
596
+ self.name,
597
+ self.priority,
598
+ type(self.lock).__name__, id(self.lock),
599
+ str(self.priority_lock))
600
+
601
+ # pylint: disable=too-many-instance-attributes
602
+ class PriorityLock(object):
603
+ ''' A priority based mutex which is acquired by and released to waiters
604
+ in priority order.
605
+
606
+ The initialiser sets a default priority, itself defaulting to `0`.
607
+
608
+ The `acquire()` method accepts an optional `priority` value
609
+ which specifies the priority of the acquire request;
610
+ lower values have higher priorities.
611
+ `acquire` returns a new `PriorityLockSubLock`.
612
+
613
+ Note that internally this allocates a `threading.Lock` per acquirer.
614
+
615
+ When `acquire` is called, if the `PriorityLock` is taken
616
+ then the acquirer blocks on their personal `Lock`.
617
+
618
+ When `release()` is called the highest priority `Lock` is released.
619
+
620
+ Within a priority level `acquire`s are served in FIFO order.
621
+
622
+ Used as a context manager, the mutex is obtained at the default priority.
623
+ The `priority()` method offers a context manager
624
+ with a specified priority.
625
+ Both context managers return the `PriorityLockSubLock`
626
+ allocated by the `acquire`.
627
+ '''
628
+
629
+ _cls_seq = Seq()
630
+
631
+ def __init__(self, default_priority=0, name=None):
632
+ ''' Initialise the `PriorityLock`.
633
+
634
+ Parameters:
635
+ * `default_priority`: the default `acquire` priority,
636
+ default `0`.
637
+ * `name`: optional identifying name
638
+ '''
639
+ if name is None:
640
+ name = str(next(self._cls_seq))
641
+ self.name = name
642
+ self.default_priority = default_priority
643
+ # heap of active priorities
644
+ self._priorities = []
645
+ # queues per priority
646
+ self._blocked = defaultdict(list)
647
+ self._nlocks = 0
648
+ self._current_sublock = None
649
+ self._seq = Seq()
650
+ self._lock = Lock()
651
+
652
+ def __str__(self):
653
+ return "%s[%s]" % (type(self).__name__, self.name)
654
+
655
+ def acquire(self, priority=None):
656
+ ''' Acquire the mutex with `priority` (default from `default_priority`).
657
+ Return the new `PriorityLockSubLock`.
658
+
659
+ This blocks behind any higher priority `acquire`s
660
+ or any earlier `acquire`s of the same priority.
661
+ '''
662
+ if priority is None:
663
+ priority = self.default_priority
664
+ priorities = self._priorities
665
+ blocked_map = self._blocked
666
+ # prepare an acquired Lock at the right priority
667
+ my_lock = PriorityLockSubLock(
668
+ str(self) + '-' + str(next(self._seq)), priority, Lock(), self
669
+ )
670
+ my_lock.lock.acquire()
671
+ with self._lock:
672
+ self._nlocks += 1
673
+ if self._nlocks == 1:
674
+ # we're the only contender: return now
675
+ assert self._current_sublock is None
676
+ self._current_sublock = my_lock
677
+ return my_lock
678
+ # store my_lock in the pending locks
679
+ blocked = blocked_map[priority]
680
+ if not blocked:
681
+ # new priority
682
+ heappush(priorities, priority)
683
+ blocked.append(my_lock)
684
+ # block until someone frees my_lock
685
+ my_lock.lock.acquire()
686
+ assert self._current_sublock is None
687
+ self._current_sublock = my_lock
688
+ return my_lock
689
+
690
+ def release(self):
691
+ ''' Release the mutex.
692
+
693
+ Internally, this releases the highest priority `Lock`,
694
+ allowing that `acquire`r to go forward.
695
+ '''
696
+ # release the top Lock
697
+ priorities = self._priorities
698
+ with self._lock:
699
+ my_lock = self._current_sublock
700
+ self._current_sublock = None
701
+ my_lock.lock.release()
702
+ self._nlocks -= 1
703
+ if self._nlocks > 0:
704
+ # release to highest priority pending lock
705
+ top_priority = priorities[0]
706
+ top_blocked = self._blocked[top_priority]
707
+ top_lock = top_blocked.pop(0)
708
+ # release the lock ASAP
709
+ top_lock.lock.release()
710
+ if not top_blocked:
711
+ # no more locks of this priority, discard the queue and the priority
712
+ del self._blocked[top_priority]
713
+ heappop(priorities)
714
+
715
+ def __enter__(self):
716
+ ''' Enter the mutex as a context manager at the default priority.
717
+ Returns the new `Lock`.
718
+ '''
719
+ return self.acquire()
720
+
721
+ def __exit__(self, *_):
722
+ ''' Exit the context manager.
723
+ '''
724
+ self.release()
725
+ return False
726
+
727
+ @contextmanager
728
+ def priority(self, this_priority):
729
+ ''' A context manager with the specified `this_priority`.
730
+ Returns the new `Lock`.
731
+ '''
732
+ my_lock = self.acquire(this_priority)
733
+ try:
734
+ yield my_lock
735
+ finally:
736
+ self.release()
737
+
738
+ @decorator
739
+ def monitor(cls, attrs=None, initial_timeout=10.0, lockattr='_lock'):
740
+ ''' Turn a class into a monitor, all of whose public methods are `@locked`.
741
+
742
+ This is a simple approach which requires class instances to have a
743
+ `._lock` which is an `RLock` or compatible
744
+ because methods may naively call each other.
745
+
746
+ Parameters:
747
+ * `attrs`: optional iterable of attribute names to wrap in `@locked`.
748
+ If omitted, all names commencing with a letter are chosen.
749
+ * `initial_timeout`: optional initial lock timeout, default `10.0`s.
750
+ * `lockattr`: optional lock attribute name, default `'_lock'`.
751
+
752
+ Only attributes satifying `inspect.ismethod` are wrapped
753
+ because `@locked` requires access to the instance `._lock` attribute.
754
+ '''
755
+ if attrs is None:
756
+ attrs = filter(lambda attr: attr and attr[0].isalpha(), dir(cls))
757
+ for name in attrs:
758
+ method = getattr(cls, name)
759
+ if ismethod(method):
760
+ setattr(
761
+ cls, name,
762
+ locked(method, initial_timeout=initial_timeout, lockattr=lockattr)
763
+ )
764
+ return cls
765
+
766
+ class DeadlockError(RuntimeError):
767
+ ''' Raised by `NRLock` when a lock is attempted from the `Thread` currently holding the lock.
768
+ '''
769
+
770
+ class NRLock:
771
+ ''' A nonrecursive lock.
772
+ Attempting to take this lock when it is already held by the current `Thread`
773
+ will raise `DeadlockError`.
774
+ Otherwise this behaves like `threading.Lock`.
775
+ '''
776
+
777
+ __slots__ = ('_lock', '_lock_thread', '_locked_by', '_name')
778
+
779
+ def __init__(self, name=None):
780
+ self._lock = Lock()
781
+ self._lock_thread = None
782
+ self._locked_by = None
783
+ self._name = name
784
+
785
+ def __repr__(self):
786
+ return (
787
+ f'{self.__class__.__name__}:{self._name!r}:{self._lock}:{self._lock_thread}:{self._locked_by}'
788
+ if self.locked() else
789
+ f'{self.__class__.__name__}:{self._name!r}:{self._lock}'
790
+ )
791
+
792
+ def locked(self):
793
+ ''' Return the lock status.
794
+ '''
795
+ return self._lock.locked()
796
+
797
+ def acquire(self, *a, caller_frame=None, **kw):
798
+ ''' Acquire the lock as for `threading.Lock`.
799
+ Raises `DeadlockError` is the lock is already held by the current `Thread`.
800
+ '''
801
+ lock = self._lock
802
+ if lock.locked() and current_thread() is self._lock_thread:
803
+ raise DeadlockError(
804
+ f'lock already held by current Thread:{self._locked_by}'
805
+ )
806
+ acquired = lock.acquire(*a, **kw)
807
+ if acquired:
808
+ if caller_frame is None:
809
+ caller_frame = caller()
810
+ self._lock_thread = current_thread()
811
+ self._locked_by = caller_frame
812
+ return acquired
813
+
814
+ def release(self):
815
+ ''' Release the lock as for `threading.Lock`.
816
+ '''
817
+ self._lock.release()
818
+ self._lock_thread = None
819
+ self._locked_by = None
820
+
821
+ def __enter__(self):
822
+ acquired = self.acquire(caller_frame=caller())
823
+ assert acquired
824
+ return acquired
825
+
826
+ def __exit__(self, *_):
827
+ self.release()
828
+
829
+ if __name__ == '__main__':
830
+ import cs.threads_tests
831
+ cs.threads_tests.selftest(sys.argv)
@@ -0,0 +1,494 @@
1
+ Metadata-Version: 2.4
2
+ Name: cs-threads
3
+ Version: 20250306
4
+ Summary: threading and communication/synchronisation conveniences
5
+ Keywords: python2,python3
6
+ Author-email: Cameron Simpson <cs@cskk.id.au>
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
+ Requires-Dist: cs.context>=20250306
16
+ Requires-Dist: cs.deco>=20250306
17
+ Requires-Dist: cs.excutils>=20250306
18
+ Requires-Dist: cs.gimmicks>=20240316
19
+ Requires-Dist: cs.pfx>=20241208
20
+ Requires-Dist: cs.py.func>=20240630
21
+ Requires-Dist: cs.py.stack>=20250306
22
+ Requires-Dist: cs.seq>=20250306
23
+ Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
24
+ Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
25
+ Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
26
+ Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/threads.py
27
+
28
+ Thread related convenience classes and functions.
29
+
30
+ *Latest release 20250306*:
31
+ HasThreadState: various fixes.
32
+
33
+ ## <a name="AdjustableSemaphore"></a>Class `AdjustableSemaphore`
34
+
35
+ A semaphore whose value may be tuned after instantiation.
36
+
37
+ *`AdjustableSemaphore.acquire(self, blocking=True)`*:
38
+ The acquire() method calls the base acquire() method if not blocking.
39
+ If blocking is true, the base acquire() is called inside a lock to
40
+ avoid competing with a reducing adjust().
41
+
42
+ *`AdjustableSemaphore.adjust(self, newvalue)`*:
43
+ Set capacity to `newvalue`
44
+ by calling release() or acquire() an appropriate number of times.
45
+
46
+ If `newvalue` lowers the semaphore capacity then adjust()
47
+ may block until the overcapacity is released.
48
+
49
+ *`AdjustableSemaphore.adjust_delta(self, delta)`*:
50
+ Adjust capacity by `delta` by calling release() or acquire()
51
+ an appropriate number of times.
52
+
53
+ If `delta` lowers the semaphore capacity then adjust() may block
54
+ until the overcapacity is released.
55
+
56
+ *`AdjustableSemaphore.release(self)`*:
57
+ Release the semaphore.
58
+
59
+ ## <a name="bg"></a>`bg(func, *, daemon=None, name=None, no_start=False, no_logexc=False, args=None, kwargs=None, thread_factory=None, pre_enter_objects=None, **thread_factory_kw)`
60
+
61
+ Dispatch the callable `func` in its own `Thread`;
62
+ return the `Thread`.
63
+
64
+ Parameters:
65
+ * `func`: a callable for the `Thread` target.
66
+ * `args`, `kwargs`: passed to the `Thread` constructor
67
+ * `kwargs`, `kwargs`: passed to the `Thread` constructor
68
+ * `daemon`: optional argument specifying the `.daemon` attribute.
69
+ * `name`: optional argument specifying the `Thread` name,
70
+ default: the name of `func`.
71
+ * `no_logexc`: if false (default `False`), wrap `func` in `@logexc`.
72
+ * `no_start`: optional argument, default `False`.
73
+ If true, do not start the `Thread`.
74
+ * `pre_enter_objects`: an optional iterable of objects which
75
+ should be entered using `with`
76
+
77
+ If `pre_enter_objects` is supplied, these objects will be
78
+ entered before the `Thread` is started and exited when the
79
+ `Thread` target function ends.
80
+ If the `Thread` is _not_ started (`no_start=True`, very
81
+ unusual) then it will be the caller's responsibility to manage
82
+ to entered objects.
83
+
84
+ ## <a name="DeadlockError"></a>Class `DeadlockError(builtins.RuntimeError)`
85
+
86
+ Raised by `NRLock` when a lock is attempted from the `Thread` currently holding the lock.
87
+
88
+ ## <a name="HasThreadState"></a>Class `HasThreadState(cs.context.ContextManagerMixin)`
89
+
90
+ A mixin for classes with a `cs.threads.ThreadState` instance as `.state`
91
+ providing a context manager which pushes `current=self` onto that state
92
+ and a `default()` class method returning `cls.perthread_state.current`
93
+ as the default instance of that class.
94
+
95
+ *NOTE*: the documentation here refers to `cls.perthread_state`, but in
96
+ fact we honour the `cls.THREAD_STATE_ATTR` attribute to name
97
+ the state attribute which allows perclass state attributes,
98
+ and also use with classes which already use `.perthread_state` for
99
+ another purpose.
100
+
101
+ *NOTE*: `HasThreadState.Thread` is a _class_ method whose default
102
+ is to push state for all active `HasThreadState` subclasses.
103
+ Contrast with `HasThreadState.bg` which is an _instance_method
104
+ whose default is to push state for just that instance.
105
+ The top level `cs.threads.bg` function calls `HasThreadState.Thread`
106
+ to obtain its `Thread`.
107
+
108
+ *`HasThreadState.Thread(*, name=None, target, enter_objects=None, **Thread_kw)`*:
109
+ Factory for a `Thread` to push the `.current` state for the
110
+ currently active classes.
111
+
112
+ The optional parameter `enter_objects` may be used to pass
113
+ an iterable of objects whose contexts should be entered
114
+ using `with obj:`.
115
+ If this is set to `True` that indicates that every "current"
116
+ `HasThreadStates` instance should be entered.
117
+ The default does not enter any object contexts.
118
+ The `HasThreadStates.bg` method defaults to passing
119
+ `enter_objects=(self,)` to enter the context for `self`.
120
+
121
+ *`HasThreadState.__enter_exit__(self)`*:
122
+ Push `self.perthread_state.current=self` as the `Thread` local current instance.
123
+
124
+ Include `self.__class__` in the set of currently active classes for the duration.
125
+
126
+ *`HasThreadState.bg(self, func, *, enter_objects=None, **bg_kw)`*:
127
+ Get a `Thread` using `type(self).Thread` and start it.
128
+ Return the `Thread`.
129
+
130
+ The `HasThreadState.Thread` factory duplicates the current `Thread`'s
131
+ `HasThreadState` current objects as current in the new `Thread`.
132
+ Additionally it enters the contexts of various objects using
133
+ `with obj` according to the `enter_objects` parameter.
134
+
135
+ The value of the optional parameter `enter_objects` governs
136
+ which objects have their context entered using `with obj`
137
+ in the child `Thread` while running `func` as follows:
138
+ - `None`: the default, meaning `(self,)`
139
+ - `False`: no object contexts are entered
140
+ - `True`: all current `HasThreadState` object contexts will be entered
141
+ - an iterable of objects whose contexts will be entered;
142
+ pass `()` to enter no objects
143
+
144
+ *`HasThreadState.default(*, factory=None, raise_on_None=False)`*:
145
+ The default instance of this class from `cls.perthread_state.current`.
146
+
147
+ Parameters:
148
+ * `factory`: optional callable to create an instance of `cls`
149
+ if `cls.perthread_state.current` is `None` or missing;
150
+ if `factory` is `True` then `cls` is used as the factory
151
+ * `raise_on_None`: if `cls.perthread_state.current` is `None` or missing
152
+ and `factory` is false and `raise_on_None` is true,
153
+ raise a `RuntimeError`;
154
+ this is primarily a debugging aid
155
+
156
+ *`HasThreadState.get_thread_states(all_classes=None)`*:
157
+ Return a mapping of `class`->*current_instance*`
158
+ for use with `HasThreadState.with_thread_states`
159
+ or `HasThreadState.Thread` or `HasThreadState.bg`.
160
+
161
+ The default behaviour returns just a mapping for this class,
162
+ expecting the default instance to be responsible for what
163
+ other resources it holds.
164
+
165
+ There is also a legacy mode for `all_classes=True`
166
+ where the mapping is for all active classes,
167
+ probably best used for `Thread`s spawned outside
168
+ a `HasThreadState` context.
169
+
170
+ Parameters:
171
+ * `all_classes`: optional flag, default `False`;
172
+ if true, return a mapping of class to current instance
173
+ for all `HasThreadState` subclasses with an open instance,
174
+ otherwise just a mapping from this class to its current instance
175
+
176
+ ## <a name="joinif"></a>`joinif(T: threading.Thread)`
177
+
178
+ Call `T.join()` if `T` is not the current `Thread`.
179
+
180
+ Unlike `threading.Thread.join`, this function is a no-op if
181
+ `T` is the current `Thread.
182
+
183
+ The use case is situations such as the shutdown phase of the
184
+ `MultiOpenMixin.startup_shutdown` context manager. Because
185
+ the "initial open" startup phase is not necessarily run in
186
+ the same thread as the "final close" shutdown phase, it is
187
+ possible for example for a worker `Thread` to execute the
188
+ shutdown phase and try to join itself. Using this function
189
+ supports that scenario.
190
+
191
+ ## <a name="LockableMixin"></a>Class `LockableMixin`
192
+
193
+ Trite mixin to control access to an object via its `._lock` attribute.
194
+ Exposes the `._lock` as the property `.lock`.
195
+ Presents a context manager interface for obtaining an object's lock.
196
+
197
+ *`LockableMixin.__exit__(self, exc_type, exc_value, traceback)`*:
198
+ pylint: disable=unused-argument
199
+
200
+ *`LockableMixin.lock`*:
201
+ The internal lock object.
202
+
203
+ ## <a name="locked"></a>`locked(*da, **dkw)`
204
+
205
+ A decorator for instance methods that must run within a lock.
206
+
207
+ Decorator keyword arguments:
208
+ * `initial_timeout`:
209
+ the initial lock attempt timeout;
210
+ if this is `>0` and exceeded a warning is issued
211
+ and then an indefinite attempt is made.
212
+ Default: `2.0`s
213
+ * `lockattr`:
214
+ the name of the attribute of `self`
215
+ which references the lock object.
216
+ Default `'_lock'`
217
+
218
+ ## <a name="locked_property"></a>`locked_property(*da, **dkw)`
219
+
220
+ A thread safe property whose value is cached.
221
+ The lock is taken if the value needs to computed.
222
+
223
+ The default lock attribute is `._lock`.
224
+ The default attribute for the cached value is `._`*funcname*
225
+ where *funcname* is `func.__name__`.
226
+ The default "unset" value for the cache is `None`.
227
+
228
+ ## <a name="monitor"></a>`monitor(*da, **dkw)`
229
+
230
+ Turn a class into a monitor, all of whose public methods are `@locked`.
231
+
232
+ This is a simple approach which requires class instances to have a
233
+ `._lock` which is an `RLock` or compatible
234
+ because methods may naively call each other.
235
+
236
+ Parameters:
237
+ * `attrs`: optional iterable of attribute names to wrap in `@locked`.
238
+ If omitted, all names commencing with a letter are chosen.
239
+ * `initial_timeout`: optional initial lock timeout, default `10.0`s.
240
+ * `lockattr`: optional lock attribute name, default `'_lock'`.
241
+
242
+ Only attributes satifying `inspect.ismethod` are wrapped
243
+ because `@locked` requires access to the instance `._lock` attribute.
244
+
245
+ ## <a name="NRLock"></a>Class `NRLock`
246
+
247
+ A nonrecursive lock.
248
+ Attempting to take this lock when it is already held by the current `Thread`
249
+ will raise `DeadlockError`.
250
+ Otherwise this behaves like `threading.Lock`.
251
+
252
+ *`NRLock.acquire(self, *a, caller_frame=None, **kw)`*:
253
+ Acquire the lock as for `threading.Lock`.
254
+ Raises `DeadlockError` is the lock is already held by the current `Thread`.
255
+
256
+ *`NRLock.locked(self)`*:
257
+ Return the lock status.
258
+
259
+ *`NRLock.release(self)`*:
260
+ Release the lock as for `threading.Lock`.
261
+
262
+ ## <a name="PriorityLock"></a>Class `PriorityLock`
263
+
264
+ A priority based mutex which is acquired by and released to waiters
265
+ in priority order.
266
+
267
+ The initialiser sets a default priority, itself defaulting to `0`.
268
+
269
+ The `acquire()` method accepts an optional `priority` value
270
+ which specifies the priority of the acquire request;
271
+ lower values have higher priorities.
272
+ `acquire` returns a new `PriorityLockSubLock`.
273
+
274
+ Note that internally this allocates a `threading.Lock` per acquirer.
275
+
276
+ When `acquire` is called, if the `PriorityLock` is taken
277
+ then the acquirer blocks on their personal `Lock`.
278
+
279
+ When `release()` is called the highest priority `Lock` is released.
280
+
281
+ Within a priority level `acquire`s are served in FIFO order.
282
+
283
+ Used as a context manager, the mutex is obtained at the default priority.
284
+ The `priority()` method offers a context manager
285
+ with a specified priority.
286
+ Both context managers return the `PriorityLockSubLock`
287
+ allocated by the `acquire`.
288
+
289
+ *`PriorityLock.__init__(self, default_priority=0, name=None)`*:
290
+ Initialise the `PriorityLock`.
291
+
292
+ Parameters:
293
+ * `default_priority`: the default `acquire` priority,
294
+ default `0`.
295
+ * `name`: optional identifying name
296
+
297
+ *`PriorityLock.__enter__(self)`*:
298
+ Enter the mutex as a context manager at the default priority.
299
+ Returns the new `Lock`.
300
+
301
+ *`PriorityLock.__exit__(self, *_)`*:
302
+ Exit the context manager.
303
+
304
+ *`PriorityLock.acquire(self, priority=None)`*:
305
+ Acquire the mutex with `priority` (default from `default_priority`).
306
+ Return the new `PriorityLockSubLock`.
307
+
308
+ This blocks behind any higher priority `acquire`s
309
+ or any earlier `acquire`s of the same priority.
310
+
311
+ *`PriorityLock.priority(self, this_priority)`*:
312
+ A context manager with the specified `this_priority`.
313
+ Returns the new `Lock`.
314
+
315
+ *`PriorityLock.release(self)`*:
316
+ Release the mutex.
317
+
318
+ Internally, this releases the highest priority `Lock`,
319
+ allowing that `acquire`r to go forward.
320
+
321
+ ## <a name="PriorityLockSubLock"></a>Class `PriorityLockSubLock(PriorityLockSubLock)`
322
+
323
+ The record for the per-`acquire`r `Lock` held by `PriorityLock.acquire`.
324
+
325
+ ## <a name="State"></a>Class `State(_thread._local)`
326
+
327
+ A `Thread` local object with attributes
328
+ which can be used as a context manager to stack attribute values.
329
+
330
+ Example:
331
+
332
+ from cs.threads import ThreadState
333
+
334
+ S = ThreadState(verbose=False)
335
+
336
+ with S(verbose=True) as prev_attrs:
337
+ if S.verbose:
338
+ print("verbose! (formerly verbose=%s)" % prev_attrs['verbose'])
339
+
340
+ *`State.__init__(self, **kw)`*:
341
+ Initiate the `ThreadState`, providing the per-Thread initial values.
342
+
343
+ *`State.__call__(self, **kw)`*:
344
+ Calling a `ThreadState` returns a context manager which stacks some state.
345
+ The context manager yields the previous values
346
+ for the attributes which were stacked.
347
+
348
+ ## <a name="ThreadState"></a>Class `ThreadState(_thread._local)`
349
+
350
+ A `Thread` local object with attributes
351
+ which can be used as a context manager to stack attribute values.
352
+
353
+ Example:
354
+
355
+ from cs.threads import ThreadState
356
+
357
+ S = ThreadState(verbose=False)
358
+
359
+ with S(verbose=True) as prev_attrs:
360
+ if S.verbose:
361
+ print("verbose! (formerly verbose=%s)" % prev_attrs['verbose'])
362
+
363
+ *`ThreadState.__init__(self, **kw)`*:
364
+ Initiate the `ThreadState`, providing the per-Thread initial values.
365
+
366
+ *`ThreadState.__call__(self, **kw)`*:
367
+ Calling a `ThreadState` returns a context manager which stacks some state.
368
+ The context manager yields the previous values
369
+ for the attributes which were stacked.
370
+
371
+ ## <a name="via"></a>`via(cmanager, func, *a, **kw)`
372
+
373
+ Return a callable that calls the supplied `func` inside a
374
+ `with` statement using the context manager `cmanager`.
375
+ This intended use case is aimed at deferred function calls.
376
+
377
+ # Release Log
378
+
379
+
380
+
381
+ *Release 20250306*:
382
+ HasThreadState: various fixes.
383
+
384
+ *Release 20241005*:
385
+ Remove some debug noise.
386
+
387
+ *Release 20240630*:
388
+ * bg: use closeall instead of twostep/withall.
389
+ * HasThreadState.bg: drop pre_enter_objects (unused), gets plumbed by the **bg_kw.
390
+
391
+ *Release 20240422*:
392
+ HasThreadState.default: make factory and raise_on keyword only.
393
+
394
+ *Release 20240412*:
395
+ * New NRLock, an nonrecursive Lock and associated exception DeadlockError.
396
+ * bg: rename thread_class to thread_factory for clarity.
397
+ * HasThreadState: big refactor to separate the mapping of default instances from the previously automatic opening of a context for each.
398
+ * HasThreadState.bg: new optional pre_enter_objects to supply objects which should be opened before the Thread starts (before bg returns) and closed when the Thread exits.
399
+
400
+ *Release 20240316*:
401
+ Fixed release upload artifacts.
402
+
403
+ *Release 20240303*:
404
+ * HasThreadState: rename thread_states() to get_thread_states().
405
+ * HasThreadState.get_thread_states: some logic fixes.
406
+
407
+ *Release 20231129*:
408
+ * HasThreadState.thread_states: *policy change*: the default now makes a mapping only for this class, not for all HasThreadState subclasses, on the premise that this class can manage use of other classes if required.
409
+ * HasThreadState: new bg() class method like Thread() but also starting the Thread.
410
+
411
+ *Release 20230331*:
412
+ * HasThreadState: new thread_states() method to snapshot the current states.
413
+ * HasThreadState: new with_thread_states() context manager to apply a set of states.
414
+ * HasThreadState: rename the default state from .state to .perthread_state.
415
+ * HasThreadState.__enter_exit__: pass cls._HasThreadState_lock to stackset as the modification guard lock, prevents race in thread_states.
416
+ * Rename State to ThreadState, which how I always use it anyway, and leave a compatibility name behind.
417
+ * New joinif(Thread) method to join a Thread unless we are that Thread - this is because MultiOpenMixin.startup_shutdown stuff may run the shutdown in a differ Thread from that which ran the startup.
418
+ * @uses_runstate: use the prevailing RunState or create one.
419
+ * Drop Python 2 support.
420
+
421
+ *Release 20230212*:
422
+ * HasThreadState: maintain a set of the HasThreadState classes in use.
423
+ * New HasThreadState.Thread class factory method to create a new Thread with the current threads states at time of call instantiated in the new Thread.
424
+ * bg: new no_context=False parameter to suppress use of HasThreadState.Thread to create the new Thread.
425
+
426
+ *Release 20230125*:
427
+ New HasThreadState mixin for classes with a state=State() attribute to provide a cls.default() class method for the default instance and a context manager to push/pop self.state.current=self.
428
+
429
+ *Release 20221228*:
430
+ * Get error and warning from cs.gimmicks, breaks circular import with cs.logutils.
431
+ * Late import of cs.logutils.LogTime to avoid circular import.
432
+
433
+ *Release 20221207*:
434
+ Small bug fix.
435
+
436
+ *Release 20221118*:
437
+ REMOVE WorkerThreadPool, pulls in too many other things and was never used.
438
+
439
+ *Release 20211208*:
440
+ bg: do not pass the current Pfx prefix into the new Thread, seems to leak and grow.
441
+
442
+ *Release 20210306*:
443
+ bg: include the current Pfx prefix in the thread name and thread body Pfx, obsoletes cs.pfx.PfxThread.
444
+
445
+ *Release 20210123*:
446
+ New @monitor class decorator for simple RLock based reentrance protection.
447
+
448
+ *Release 20201025*:
449
+ * @locked: bump the default warning timeout to 10s, was firing too often.
450
+ * New State class for thread local state objects with default attribute values and a stacking __call__ context manager.
451
+
452
+ *Release 20200718*:
453
+ @locked: apply the interior __doc__ to the wrapper.
454
+
455
+ *Release 20200521*:
456
+ @locked_property: decorate with @cs.deco.decorator to support keyword arguments.
457
+
458
+ *Release 20191102*:
459
+ @locked: report slow-to-acquire locks, add initial_timeout and lockattr decorator keyword parameters.
460
+
461
+ *Release 20190923.2*:
462
+ Fix annoying docstring typo.
463
+
464
+ *Release 20190923.1*:
465
+ Docstring updates.
466
+
467
+ *Release 20190923*:
468
+ Remove dependence on cs.obj.
469
+
470
+ *Release 20190921*:
471
+ New PriorityLock class for a mutex which releases in (priority,fifo) order.
472
+
473
+ *Release 20190812*:
474
+ bg: compute default name before wrapping `func` in @logexc.
475
+
476
+ *Release 20190729*:
477
+ bg: provide default `name`, run callable inside Pfx, add optional no_logexc=False param preventing @logec wrapper if true.
478
+
479
+ *Release 20190422*:
480
+ bg(): new optional `no_start=False` keyword argument, preventing Thread.start if true
481
+
482
+ *Release 20190102*:
483
+ * Drop some unused classes.
484
+ * New LockableMixin, presenting a context manager and a .lock property.
485
+
486
+ *Release 20160828*:
487
+ Use "install_requires" instead of "requires" in DISTINFO.
488
+
489
+ *Release 20160827*:
490
+ * Replace bare "excepts" with "except BaseException".
491
+ * Doc updates. Other minor improvements.
492
+
493
+ *Release 20150115*:
494
+ First PyPI release.
@@ -0,0 +1,4 @@
1
+ cs/threads.py,sha256=g3NyPHzcB6HftETASXTB-ZZaM7P5fMThxnwgd2lJATU,26866
2
+ cs_threads-20250306.dist-info/WHEEL,sha256=BXjIu84EnBiZ4HkNUBN93Hamt5EPQMQ6VkF7-VZ_Pu0,100
3
+ cs_threads-20250306.dist-info/METADATA,sha256=n7JO0Y516bpWfVerHVVlDaAEgH_O-im2XZHClmPQNO0,18714
4
+ cs_threads-20250306.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.11.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any