cs-seq 20250103__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/seq.py +654 -0
- cs_seq-20250103.dist-info/METADATA +399 -0
- cs_seq-20250103.dist-info/RECORD +5 -0
- cs_seq-20250103.dist-info/WHEEL +5 -0
- cs_seq-20250103.dist-info/top_level.txt +1 -0
cs/seq.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
#!/usr/bin/python -tt
|
|
2
|
+
#
|
|
3
|
+
# Stuff to do with counters, sequences and iterables.
|
|
4
|
+
# - Cameron Simpson <cs@cskk.id.au> 20jul2008
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
r'''
|
|
8
|
+
Stuff to do with counters, sequences and iterables.
|
|
9
|
+
|
|
10
|
+
Note that any function accepting an iterable
|
|
11
|
+
will consume some or all of the derived iterator
|
|
12
|
+
in the course of its function.
|
|
13
|
+
'''
|
|
14
|
+
|
|
15
|
+
import heapq
|
|
16
|
+
import itertools
|
|
17
|
+
from threading import Lock, Condition, Thread
|
|
18
|
+
|
|
19
|
+
from cs.deco import decorator
|
|
20
|
+
from cs.gimmicks import warning
|
|
21
|
+
|
|
22
|
+
__version__ = '20250103'
|
|
23
|
+
|
|
24
|
+
DISTINFO = {
|
|
25
|
+
'description':
|
|
26
|
+
"Stuff to do with counters, sequences and iterables.",
|
|
27
|
+
'keywords': ["python2", "python3"],
|
|
28
|
+
'classifiers': [
|
|
29
|
+
"Programming Language :: Python",
|
|
30
|
+
"Programming Language :: Python :: 2",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
],
|
|
33
|
+
'install_requires': [
|
|
34
|
+
'cs.deco',
|
|
35
|
+
'cs.gimmicks',
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class Seq(object):
|
|
40
|
+
''' A numeric sequence implemented as a thread safe wrapper for
|
|
41
|
+
`itertools.count()`.
|
|
42
|
+
|
|
43
|
+
A `Seq` is iterable and both iterating and calling it return
|
|
44
|
+
the next number in the sequence.
|
|
45
|
+
'''
|
|
46
|
+
|
|
47
|
+
__slots__ = ('counter', '_lock')
|
|
48
|
+
|
|
49
|
+
def __init__(self, start=0, lock=None):
|
|
50
|
+
if lock is None:
|
|
51
|
+
lock = Lock()
|
|
52
|
+
self.counter = itertools.count(start)
|
|
53
|
+
self._lock = lock
|
|
54
|
+
|
|
55
|
+
def __iter__(self):
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __next__(self):
|
|
59
|
+
with self._lock:
|
|
60
|
+
return next(self.counter)
|
|
61
|
+
|
|
62
|
+
next = __next__
|
|
63
|
+
__call__ = __next__
|
|
64
|
+
|
|
65
|
+
__seq = Seq()
|
|
66
|
+
|
|
67
|
+
def seq():
|
|
68
|
+
''' Return a new sequential value.
|
|
69
|
+
'''
|
|
70
|
+
return next(__seq)
|
|
71
|
+
|
|
72
|
+
def the(iterable, context=None):
|
|
73
|
+
''' Returns the first element of an iterable, but requires there to be
|
|
74
|
+
exactly one.
|
|
75
|
+
'''
|
|
76
|
+
icontext = "expected exactly one value"
|
|
77
|
+
if context is not None:
|
|
78
|
+
icontext = icontext + " for " + context
|
|
79
|
+
is_first = True
|
|
80
|
+
for elem in iterable:
|
|
81
|
+
if is_first:
|
|
82
|
+
it = elem
|
|
83
|
+
is_first = False
|
|
84
|
+
else:
|
|
85
|
+
raise IndexError(
|
|
86
|
+
"%s: got more than one element (%s, %s, ...)" % (icontext, it, elem)
|
|
87
|
+
)
|
|
88
|
+
if is_first:
|
|
89
|
+
raise IndexError("%s: got no elements" % (icontext,))
|
|
90
|
+
|
|
91
|
+
return it
|
|
92
|
+
|
|
93
|
+
def first(iterable):
|
|
94
|
+
''' Return the first item from an iterable; raise `IndexError` on empty iterables.
|
|
95
|
+
'''
|
|
96
|
+
for item in iterable:
|
|
97
|
+
return item
|
|
98
|
+
raise IndexError("empty iterable %r" % (iterable,))
|
|
99
|
+
|
|
100
|
+
def last(iterable):
|
|
101
|
+
''' Return the last item from an iterable; raise `IndexError` on empty iterables.
|
|
102
|
+
'''
|
|
103
|
+
nothing = True
|
|
104
|
+
for item in iterable:
|
|
105
|
+
nothing = False
|
|
106
|
+
if nothing:
|
|
107
|
+
raise IndexError("no items in iterable: %r" % (iterable,))
|
|
108
|
+
return item # pylint: disable=undefined-loop-variable
|
|
109
|
+
|
|
110
|
+
def get0(iterable, default=None):
|
|
111
|
+
''' Return first element of an iterable, or the default.
|
|
112
|
+
'''
|
|
113
|
+
try:
|
|
114
|
+
i = first(iterable)
|
|
115
|
+
except IndexError:
|
|
116
|
+
return default
|
|
117
|
+
return i
|
|
118
|
+
|
|
119
|
+
def tee(iterable, *Qs):
|
|
120
|
+
''' A generator yielding the items from an iterable
|
|
121
|
+
which also copies those items to a series of queues.
|
|
122
|
+
|
|
123
|
+
Parameters:
|
|
124
|
+
* `iterable`: the iterable to copy
|
|
125
|
+
* `Qs`: the queues, objects accepting a `.put` method.
|
|
126
|
+
|
|
127
|
+
Note: the item is `.put` onto every queue
|
|
128
|
+
before being yielded from this generator.
|
|
129
|
+
'''
|
|
130
|
+
for item in iterable:
|
|
131
|
+
for Q in Qs:
|
|
132
|
+
Q.put(item)
|
|
133
|
+
yield item
|
|
134
|
+
|
|
135
|
+
def imerge(*iters, **kw):
|
|
136
|
+
''' Merge an iterable of ordered iterables in order.
|
|
137
|
+
|
|
138
|
+
Parameters:
|
|
139
|
+
* `iters`: an iterable of iterators
|
|
140
|
+
* `reverse`: keyword parameter: if true, yield items in reverse order.
|
|
141
|
+
This requires the iterables themselves to also be in
|
|
142
|
+
reversed order.
|
|
143
|
+
|
|
144
|
+
This function relies on the source iterables being ordered
|
|
145
|
+
and their elements being comparable, through slightly misordered
|
|
146
|
+
iterables (for example, as extracted from web server logs)
|
|
147
|
+
will produce only slightly misordered results, as the merging
|
|
148
|
+
is done on the basis of the front elements of each iterable.
|
|
149
|
+
'''
|
|
150
|
+
reverse = kw.get('reverse', False)
|
|
151
|
+
if kw:
|
|
152
|
+
raise ValueError("unexpected keyword arguments: %r" % (kw,))
|
|
153
|
+
if reverse:
|
|
154
|
+
# tuples that compare in reverse order
|
|
155
|
+
class _MergeHeapItem(tuple):
|
|
156
|
+
|
|
157
|
+
def __lt__(self, other):
|
|
158
|
+
return self[0] > other[0]
|
|
159
|
+
else:
|
|
160
|
+
# tuples that compare in forward order
|
|
161
|
+
class _MergeHeapItem(tuple):
|
|
162
|
+
|
|
163
|
+
def __lt__(self, other):
|
|
164
|
+
return self[0] < other[0]
|
|
165
|
+
|
|
166
|
+
# prime the list of head elements with (value, iter)
|
|
167
|
+
heap = []
|
|
168
|
+
for it in iters:
|
|
169
|
+
it = iter(it)
|
|
170
|
+
try:
|
|
171
|
+
head = next(it)
|
|
172
|
+
except StopIteration:
|
|
173
|
+
pass
|
|
174
|
+
else:
|
|
175
|
+
heapq.heappush(heap, _MergeHeapItem((head, it)))
|
|
176
|
+
while heap:
|
|
177
|
+
head, it = heapq.heappop(heap)
|
|
178
|
+
yield head
|
|
179
|
+
try:
|
|
180
|
+
head = next(it)
|
|
181
|
+
except StopIteration:
|
|
182
|
+
pass
|
|
183
|
+
else:
|
|
184
|
+
heapq.heappush(heap, _MergeHeapItem((head, it)))
|
|
185
|
+
|
|
186
|
+
def onetoone(func):
|
|
187
|
+
''' A decorator for a method of a sequence to merge the results of
|
|
188
|
+
passing every element of the sequence to the function, expecting a
|
|
189
|
+
single value back.
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
|
|
193
|
+
class X(list):
|
|
194
|
+
@onetoone
|
|
195
|
+
def lower(self, item):
|
|
196
|
+
return item.lower()
|
|
197
|
+
strs = X(['Abc', 'Def'])
|
|
198
|
+
lower_strs = X.lower()
|
|
199
|
+
'''
|
|
200
|
+
|
|
201
|
+
def gather(self, *a, **kw):
|
|
202
|
+
''' Yield the results of calling the function on each item.
|
|
203
|
+
'''
|
|
204
|
+
for item in self:
|
|
205
|
+
yield func(item, *a, **kw)
|
|
206
|
+
|
|
207
|
+
return gather
|
|
208
|
+
|
|
209
|
+
def onetomany(func):
|
|
210
|
+
''' A decorator for a method of a sequence to merge the results of
|
|
211
|
+
passing every element of the sequence to the function, expecting
|
|
212
|
+
multiple values back.
|
|
213
|
+
|
|
214
|
+
Example:
|
|
215
|
+
|
|
216
|
+
class X(list):
|
|
217
|
+
@onetomany
|
|
218
|
+
def chars(self, item):
|
|
219
|
+
return item
|
|
220
|
+
strs = X(['Abc', 'Def'])
|
|
221
|
+
all_chars = X.chars()
|
|
222
|
+
'''
|
|
223
|
+
|
|
224
|
+
def gather(self, *a, **kw):
|
|
225
|
+
''' Chain the function results together.
|
|
226
|
+
'''
|
|
227
|
+
return itertools.chain(*[func(item, *a, **kw) for item in self])
|
|
228
|
+
|
|
229
|
+
return gather
|
|
230
|
+
|
|
231
|
+
def isordered(items, reverse=False, strict=False):
|
|
232
|
+
''' Test whether an iterable is ordered.
|
|
233
|
+
Note that the iterable is iterated, so this is a destructive
|
|
234
|
+
test for nonsequences.
|
|
235
|
+
'''
|
|
236
|
+
is_first = True
|
|
237
|
+
prev = None
|
|
238
|
+
for item in items:
|
|
239
|
+
if not is_first:
|
|
240
|
+
if reverse:
|
|
241
|
+
ordered = item < prev if strict else item <= prev
|
|
242
|
+
else:
|
|
243
|
+
ordered = item > prev if strict else item >= prev
|
|
244
|
+
if not ordered:
|
|
245
|
+
return False
|
|
246
|
+
prev = item
|
|
247
|
+
is_first = False
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
def common_prefix_length(*seqs):
|
|
251
|
+
''' Return the length of the common prefix of sequences `seqs`.
|
|
252
|
+
'''
|
|
253
|
+
if not seqs:
|
|
254
|
+
return 0
|
|
255
|
+
if len(seqs) == 1:
|
|
256
|
+
return len(seqs[0])
|
|
257
|
+
for i, items in enumerate(zip(*seqs)):
|
|
258
|
+
item0 = items[0]
|
|
259
|
+
# pylint: disable=cell-var-from-loop
|
|
260
|
+
if not all(map(lambda item: item == item0, items)):
|
|
261
|
+
return i
|
|
262
|
+
# return the length of the shorted sequence
|
|
263
|
+
return len(min(*seqs, key=len))
|
|
264
|
+
|
|
265
|
+
def common_suffix_length(*seqs):
|
|
266
|
+
''' Return the length of the common suffix of sequences `seqs`.
|
|
267
|
+
'''
|
|
268
|
+
return common_prefix_length(list(map(lambda s: list(reversed(s)), *seqs)))
|
|
269
|
+
|
|
270
|
+
class TrackingCounter(object):
|
|
271
|
+
''' A wrapper for a counter which can be incremented and decremented.
|
|
272
|
+
|
|
273
|
+
A facility is provided to wait for the counter to reach a specific value.
|
|
274
|
+
The .inc and .dec methods also accept a `tag` argument to keep
|
|
275
|
+
individual counts based on the tag to aid debugging.
|
|
276
|
+
|
|
277
|
+
TODO: add `strict` option to error and abort if any counter tries
|
|
278
|
+
to go below zero.
|
|
279
|
+
'''
|
|
280
|
+
|
|
281
|
+
def __init__(self, value=0, name=None, lock=None):
|
|
282
|
+
''' Initialise the counter to `value` (default 0) with the optional `name`.
|
|
283
|
+
'''
|
|
284
|
+
if name is None:
|
|
285
|
+
name = "TrackingCounter-%d" % (seq(),)
|
|
286
|
+
if lock is None:
|
|
287
|
+
lock = Lock()
|
|
288
|
+
self.value = value
|
|
289
|
+
self.name = name
|
|
290
|
+
self._lock = lock
|
|
291
|
+
self._watched = {}
|
|
292
|
+
self._tag_up = {}
|
|
293
|
+
self._tag_down = {}
|
|
294
|
+
|
|
295
|
+
def __str__(self):
|
|
296
|
+
return "%s:%d" % (self.name, self.value)
|
|
297
|
+
|
|
298
|
+
def __repr__(self):
|
|
299
|
+
return "<TrackingCounter %r:%r>" % (str(self), self._watched)
|
|
300
|
+
|
|
301
|
+
def __nonzero__(self):
|
|
302
|
+
return self.value != 0
|
|
303
|
+
|
|
304
|
+
def __int__(self):
|
|
305
|
+
return self.value
|
|
306
|
+
|
|
307
|
+
def _notify(self):
|
|
308
|
+
''' Notify any waiters on the current counter value.
|
|
309
|
+
This should be called inside self._lock.
|
|
310
|
+
'''
|
|
311
|
+
value = self.value
|
|
312
|
+
watcher = self._watched.get(value)
|
|
313
|
+
if watcher:
|
|
314
|
+
del self._watched[value]
|
|
315
|
+
watcher.acquire()
|
|
316
|
+
watcher.notify_all()
|
|
317
|
+
watcher.release()
|
|
318
|
+
|
|
319
|
+
def inc(self, tag=None):
|
|
320
|
+
''' Increment the counter.
|
|
321
|
+
Wake up any threads waiting for its new value.
|
|
322
|
+
'''
|
|
323
|
+
with self._lock:
|
|
324
|
+
self.value += 1
|
|
325
|
+
if tag is not None:
|
|
326
|
+
tag = str(tag)
|
|
327
|
+
self._tag_up.setdefault(tag, 0)
|
|
328
|
+
self._tag_up[tag] += 1
|
|
329
|
+
self._notify()
|
|
330
|
+
|
|
331
|
+
def dec(self, tag=None):
|
|
332
|
+
''' Decrement the counter.
|
|
333
|
+
Wake up any threads waiting for its new value.
|
|
334
|
+
'''
|
|
335
|
+
with self._lock:
|
|
336
|
+
self.value -= 1
|
|
337
|
+
if tag is not None:
|
|
338
|
+
tag = str(tag)
|
|
339
|
+
self._tag_down.setdefault(tag, 0)
|
|
340
|
+
self._tag_down[tag] += 1
|
|
341
|
+
if self._tag_up.get(tag, 0) < self._tag_down[tag]:
|
|
342
|
+
warning("%s.dec: more .decs than .incs for tag %r", self, tag)
|
|
343
|
+
if self.value < 0:
|
|
344
|
+
warning("%s.dec: value < 0!", self)
|
|
345
|
+
self._notify()
|
|
346
|
+
|
|
347
|
+
def check(self):
|
|
348
|
+
''' Internal consistency check.
|
|
349
|
+
'''
|
|
350
|
+
for tag in sorted(self._tag_up.keys()):
|
|
351
|
+
ups = self._tag_up[tag]
|
|
352
|
+
downs = self._tag_down.get(tag, 0)
|
|
353
|
+
if ups != downs:
|
|
354
|
+
warning("%s: ups=%d, downs=%d: tag %r", self, ups, downs, tag)
|
|
355
|
+
|
|
356
|
+
def wait(self, value):
|
|
357
|
+
''' Wait for the counter to reach the specified `value`.
|
|
358
|
+
'''
|
|
359
|
+
with self._lock:
|
|
360
|
+
if value == self.value:
|
|
361
|
+
return
|
|
362
|
+
if value not in self._watched:
|
|
363
|
+
watcher = self._watched[value] = Condition()
|
|
364
|
+
else:
|
|
365
|
+
watcher = self._watched[value]
|
|
366
|
+
watcher.acquire()
|
|
367
|
+
watcher.wait()
|
|
368
|
+
|
|
369
|
+
class StatefulIterator(object):
|
|
370
|
+
''' A trivial iterator which wraps another iterator to expose some tracking state.
|
|
371
|
+
|
|
372
|
+
This has 2 attributes:
|
|
373
|
+
* `.it`: the internal iterator which should yield `(item,new_state)`
|
|
374
|
+
* `.state`: the last state value from the internal iterator
|
|
375
|
+
|
|
376
|
+
The originating use case is resuse of an iterator by independent
|
|
377
|
+
calls that are typically sequential, specificly the .read
|
|
378
|
+
method of file like objects. Naive sequential reads require
|
|
379
|
+
the underlying storage to locate the data on every call, even
|
|
380
|
+
though the previous call has just performed this task for the
|
|
381
|
+
previous read. Saving the iterator used from the preceeding
|
|
382
|
+
call allows the iterator to pick up directly if the file
|
|
383
|
+
offset hasn't been fiddled in the meantime.
|
|
384
|
+
'''
|
|
385
|
+
|
|
386
|
+
def __init__(self, it):
|
|
387
|
+
self.it = it
|
|
388
|
+
self.state = None
|
|
389
|
+
|
|
390
|
+
def __iter__(self):
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
def __next__(self):
|
|
394
|
+
item, new_state = next(self.it)
|
|
395
|
+
self.state = new_state
|
|
396
|
+
return item
|
|
397
|
+
|
|
398
|
+
def splitoff(sq, *sizes):
|
|
399
|
+
''' Split a sequence into (usually short) prefixes and a tail,
|
|
400
|
+
for example to construct subdirectory trees based on a UUID.
|
|
401
|
+
|
|
402
|
+
Example:
|
|
403
|
+
|
|
404
|
+
>>> from uuid import UUID
|
|
405
|
+
>>> uuid = 'd6d9c510-785c-468c-9aa4-b7bda343fb79'
|
|
406
|
+
>>> uu = UUID(uuid).hex
|
|
407
|
+
>>> uu
|
|
408
|
+
'd6d9c510785c468c9aa4b7bda343fb79'
|
|
409
|
+
>>> splitoff(uu, 2, 2)
|
|
410
|
+
['d6', 'd9', 'c510785c468c9aa4b7bda343fb79']
|
|
411
|
+
'''
|
|
412
|
+
if len(sizes) < 1:
|
|
413
|
+
raise ValueError("no sizes")
|
|
414
|
+
offset = 0
|
|
415
|
+
parts = []
|
|
416
|
+
for size in sizes:
|
|
417
|
+
if size < 1:
|
|
418
|
+
raise ValueError("size:%s < 1" % (size,))
|
|
419
|
+
end_offset = offset + size
|
|
420
|
+
if end_offset >= len(sq):
|
|
421
|
+
raise ValueError(
|
|
422
|
+
"size:%s consumes up to or beyond"
|
|
423
|
+
" the end of the sequence (length %d)" % (size, len(sq))
|
|
424
|
+
)
|
|
425
|
+
parts.append(sq[offset:end_offset])
|
|
426
|
+
offset = end_offset
|
|
427
|
+
parts.append(sq[offset:])
|
|
428
|
+
return parts
|
|
429
|
+
|
|
430
|
+
def unrepeated(it, seen=None, signature=None):
|
|
431
|
+
''' A generator yielding items from the iterable `it` with no repetitions.
|
|
432
|
+
|
|
433
|
+
Parameters:
|
|
434
|
+
* `it`: the iterable to process
|
|
435
|
+
* `seen`: an optional setlike container supporting `in` and `.add()`
|
|
436
|
+
* `signature`: an optional signature function for items from `it`
|
|
437
|
+
which produces the value to compare to recognise repeated items;
|
|
438
|
+
its values are stored in the `seen` set
|
|
439
|
+
|
|
440
|
+
The default `signature` function is equality;
|
|
441
|
+
the items are stored n `seen` and compared.
|
|
442
|
+
This requires the items to be hashable and support equality tests.
|
|
443
|
+
The same applies to whatever values the `signature` function produces.
|
|
444
|
+
|
|
445
|
+
Another common signature is identity: `id`, useful for
|
|
446
|
+
traversing a graph which may have cycles.
|
|
447
|
+
|
|
448
|
+
Since `seen` accrues all the signature values for yielded items
|
|
449
|
+
generally it will grow monotonicly as iteration proceeeds.
|
|
450
|
+
If the items are complex or large it is well worth providing a signature
|
|
451
|
+
function even if the items themselves can be used in a set.
|
|
452
|
+
'''
|
|
453
|
+
if seen is None:
|
|
454
|
+
seen = set()
|
|
455
|
+
if signature is None:
|
|
456
|
+
signature = lambda item: item
|
|
457
|
+
for item in it:
|
|
458
|
+
sig = signature(item)
|
|
459
|
+
if sig in seen:
|
|
460
|
+
continue
|
|
461
|
+
seen.add(sig)
|
|
462
|
+
yield item
|
|
463
|
+
|
|
464
|
+
@decorator
|
|
465
|
+
def _greedy_decorator(g, queue_depth=0):
|
|
466
|
+
|
|
467
|
+
def greedy_generator(*a, **kw):
|
|
468
|
+
return greedy(g(*a, **kw), queue_depth=queue_depth)
|
|
469
|
+
|
|
470
|
+
return greedy_generator
|
|
471
|
+
|
|
472
|
+
def greedy(g=None, queue_depth=0):
|
|
473
|
+
''' A decorator or function for greedy computation of iterables.
|
|
474
|
+
|
|
475
|
+
If `g` is omitted or callable
|
|
476
|
+
this is a decorator for a generator function
|
|
477
|
+
causing it to compute greedily,
|
|
478
|
+
capacity limited by `queue_depth`.
|
|
479
|
+
|
|
480
|
+
If `g` is iterable
|
|
481
|
+
this function dispatches it in a `Thread` to compute greedily,
|
|
482
|
+
capacity limited by `queue_depth`.
|
|
483
|
+
|
|
484
|
+
Example with an iterable:
|
|
485
|
+
|
|
486
|
+
for packet in greedy(parse_data_stream(stream)):
|
|
487
|
+
... process packet ...
|
|
488
|
+
|
|
489
|
+
which does some readahead of the stream.
|
|
490
|
+
|
|
491
|
+
Example as a function decorator:
|
|
492
|
+
|
|
493
|
+
@greedy
|
|
494
|
+
def g(n):
|
|
495
|
+
for item in range(n):
|
|
496
|
+
yield n
|
|
497
|
+
|
|
498
|
+
This can also be used directly on an existing iterable:
|
|
499
|
+
|
|
500
|
+
for item in greedy(range(n)):
|
|
501
|
+
yield n
|
|
502
|
+
|
|
503
|
+
Normally a generator runs on demand.
|
|
504
|
+
This function dispatches a `Thread` to run the iterable
|
|
505
|
+
(typically a generator)
|
|
506
|
+
putting yielded values to a queue
|
|
507
|
+
and returns a new generator yielding from the queue.
|
|
508
|
+
|
|
509
|
+
The `queue_depth` parameter specifies the depth of the queue
|
|
510
|
+
and therefore how many values the original generator can compute
|
|
511
|
+
before blocking at the queue's capacity.
|
|
512
|
+
|
|
513
|
+
The default `queue_depth` is `0` which creates a `Channel`
|
|
514
|
+
as the queue - a zero storage buffer - which lets the generator
|
|
515
|
+
compute only a single value ahead of time.
|
|
516
|
+
|
|
517
|
+
A larger `queue_depth` allocates a `Queue` with that much storage
|
|
518
|
+
allowing the generator to compute as many as `queue_depth+1` values
|
|
519
|
+
ahead of time.
|
|
520
|
+
|
|
521
|
+
Here's a comparison of the behaviour:
|
|
522
|
+
|
|
523
|
+
Example without `@greedy`
|
|
524
|
+
where the "yield 1" step does not occur until after the "got 0":
|
|
525
|
+
|
|
526
|
+
>>> from time import sleep
|
|
527
|
+
>>> def g():
|
|
528
|
+
... for i in range(2):
|
|
529
|
+
... print("yield", i)
|
|
530
|
+
... yield i
|
|
531
|
+
... print("g done")
|
|
532
|
+
...
|
|
533
|
+
>>> G = g(); sleep(0.1)
|
|
534
|
+
>>> for i in G:
|
|
535
|
+
... print("got", i)
|
|
536
|
+
... sleep(0.1)
|
|
537
|
+
...
|
|
538
|
+
yield 0
|
|
539
|
+
got 0
|
|
540
|
+
yield 1
|
|
541
|
+
got 1
|
|
542
|
+
g done
|
|
543
|
+
|
|
544
|
+
Example with `@greedy`
|
|
545
|
+
where the "yield 1" step computes before the "got 0":
|
|
546
|
+
|
|
547
|
+
>>> from time import sleep
|
|
548
|
+
>>> @greedy
|
|
549
|
+
... def g():
|
|
550
|
+
... for i in range(2):
|
|
551
|
+
... print("yield", i)
|
|
552
|
+
... yield i
|
|
553
|
+
... print("g done")
|
|
554
|
+
...
|
|
555
|
+
>>> G = g(); sleep(0.1)
|
|
556
|
+
yield 0
|
|
557
|
+
>>> for i in G:
|
|
558
|
+
... print("got", repr(i))
|
|
559
|
+
... sleep(0.1)
|
|
560
|
+
...
|
|
561
|
+
yield 1
|
|
562
|
+
got 0
|
|
563
|
+
g done
|
|
564
|
+
got 1
|
|
565
|
+
|
|
566
|
+
Example with `@greedy(queue_depth=1)`
|
|
567
|
+
where the "yield 1" step computes before the "got 0":
|
|
568
|
+
|
|
569
|
+
>>> from cs.x import X
|
|
570
|
+
>>> from time import sleep
|
|
571
|
+
>>> @greedy
|
|
572
|
+
... def g():
|
|
573
|
+
... for i in range(3):
|
|
574
|
+
... X("Y")
|
|
575
|
+
... print("yield", i)
|
|
576
|
+
... yield i
|
|
577
|
+
... print("g done")
|
|
578
|
+
...
|
|
579
|
+
>>> G = g(); sleep(2)
|
|
580
|
+
yield 0
|
|
581
|
+
yield 1
|
|
582
|
+
>>> for i in G:
|
|
583
|
+
... print("got", repr(i))
|
|
584
|
+
... sleep(0.1)
|
|
585
|
+
...
|
|
586
|
+
yield 2
|
|
587
|
+
got 0
|
|
588
|
+
yield 3
|
|
589
|
+
got 1
|
|
590
|
+
g done
|
|
591
|
+
got 2
|
|
592
|
+
|
|
593
|
+
'''
|
|
594
|
+
assert queue_depth >= 0
|
|
595
|
+
|
|
596
|
+
if g is None:
|
|
597
|
+
# the parameterised @greedy(queue_depth=n) form
|
|
598
|
+
# pylint: disable=no-value-for-parameter
|
|
599
|
+
return _greedy_decorator(queue_depth=queue_depth)
|
|
600
|
+
|
|
601
|
+
if callable(g):
|
|
602
|
+
# the direct @greedy form
|
|
603
|
+
return _greedy_decorator(g, queue_depth=queue_depth)
|
|
604
|
+
|
|
605
|
+
# presumably an iterator - dispatch it in a Thread
|
|
606
|
+
try:
|
|
607
|
+
it = iter(g)
|
|
608
|
+
except TypeError as e:
|
|
609
|
+
# pylint: disable=raise-missing-from
|
|
610
|
+
raise TypeError("g=%r: neither callable nor iterable: %s" % (g, e))
|
|
611
|
+
|
|
612
|
+
# pylint: disable=import-outside-toplevel
|
|
613
|
+
from cs.queues import Channel, IterableQueue
|
|
614
|
+
if queue_depth == 0:
|
|
615
|
+
q = Channel()
|
|
616
|
+
else:
|
|
617
|
+
q = IterableQueue(queue_depth)
|
|
618
|
+
|
|
619
|
+
def run_generator():
|
|
620
|
+
''' Thread body for greedy generator.
|
|
621
|
+
'''
|
|
622
|
+
try:
|
|
623
|
+
for item in it:
|
|
624
|
+
q.put(item)
|
|
625
|
+
finally:
|
|
626
|
+
q.close()
|
|
627
|
+
|
|
628
|
+
Thread(target=run_generator).start()
|
|
629
|
+
return iter(q)
|
|
630
|
+
|
|
631
|
+
def skip_map(func, *iterables, except_types, quiet=False):
|
|
632
|
+
''' A version of `map()` which will skip items where `func(item)`
|
|
633
|
+
raises an exception in `except_types`, a tuple of exception types.
|
|
634
|
+
If a skipped exception occurs a warning will be issued unless
|
|
635
|
+
`quiet` is true (default `False`).
|
|
636
|
+
'''
|
|
637
|
+
if not isinstance(except_types, tuple):
|
|
638
|
+
raise TypeError(
|
|
639
|
+
"except types must be a tuple of exception types but has type %s" %
|
|
640
|
+
(type(except_types),)
|
|
641
|
+
)
|
|
642
|
+
for iterable in iterables:
|
|
643
|
+
for item in iterable:
|
|
644
|
+
try:
|
|
645
|
+
yield func(item)
|
|
646
|
+
except except_types as e:
|
|
647
|
+
quiet or warning(
|
|
648
|
+
"skip_map(func=%s): item=%s: skip exception: %s", func, item, e
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
if __name__ == '__main__':
|
|
652
|
+
import sys
|
|
653
|
+
import cs.seq_tests
|
|
654
|
+
cs.seq_tests.selftest(sys.argv)
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: cs-seq
|
|
3
|
+
Version: 20250103
|
|
4
|
+
Summary: Stuff to do with counters, sequences and iterables.
|
|
5
|
+
Author-email: Cameron Simpson <cs@cskk.id.au>
|
|
6
|
+
License: GNU General Public License v3 or later (GPLv3+)
|
|
7
|
+
Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
|
|
8
|
+
Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
|
|
9
|
+
Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
|
|
10
|
+
Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/seq.py
|
|
11
|
+
Keywords: python2,python3
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 2
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: cs.deco>=20250103
|
|
22
|
+
Requires-Dist: cs.gimmicks>=20240316
|
|
23
|
+
|
|
24
|
+
Stuff to do with counters, sequences and iterables.
|
|
25
|
+
|
|
26
|
+
*Latest release 20250103*:
|
|
27
|
+
New skip_map(func, *iterables, except_types, quiet=False) generator function, like map() but skipping certain exceptions.
|
|
28
|
+
|
|
29
|
+
Note that any function accepting an iterable
|
|
30
|
+
will consume some or all of the derived iterator
|
|
31
|
+
in the course of its function.
|
|
32
|
+
|
|
33
|
+
## <a name="common_prefix_length"></a>`common_prefix_length(*seqs)`
|
|
34
|
+
|
|
35
|
+
Return the length of the common prefix of sequences `seqs`.
|
|
36
|
+
|
|
37
|
+
## <a name="common_suffix_length"></a>`common_suffix_length(*seqs)`
|
|
38
|
+
|
|
39
|
+
Return the length of the common suffix of sequences `seqs`.
|
|
40
|
+
|
|
41
|
+
## <a name="first"></a>`first(iterable)`
|
|
42
|
+
|
|
43
|
+
Return the first item from an iterable; raise `IndexError` on empty iterables.
|
|
44
|
+
|
|
45
|
+
## <a name="get0"></a>`get0(iterable, default=None)`
|
|
46
|
+
|
|
47
|
+
Return first element of an iterable, or the default.
|
|
48
|
+
|
|
49
|
+
## <a name="greedy"></a>`greedy(g=None, queue_depth=0)`
|
|
50
|
+
|
|
51
|
+
A decorator or function for greedy computation of iterables.
|
|
52
|
+
|
|
53
|
+
If `g` is omitted or callable
|
|
54
|
+
this is a decorator for a generator function
|
|
55
|
+
causing it to compute greedily,
|
|
56
|
+
capacity limited by `queue_depth`.
|
|
57
|
+
|
|
58
|
+
If `g` is iterable
|
|
59
|
+
this function dispatches it in a `Thread` to compute greedily,
|
|
60
|
+
capacity limited by `queue_depth`.
|
|
61
|
+
|
|
62
|
+
Example with an iterable:
|
|
63
|
+
|
|
64
|
+
for packet in greedy(parse_data_stream(stream)):
|
|
65
|
+
... process packet ...
|
|
66
|
+
|
|
67
|
+
which does some readahead of the stream.
|
|
68
|
+
|
|
69
|
+
Example as a function decorator:
|
|
70
|
+
|
|
71
|
+
@greedy
|
|
72
|
+
def g(n):
|
|
73
|
+
for item in range(n):
|
|
74
|
+
yield n
|
|
75
|
+
|
|
76
|
+
This can also be used directly on an existing iterable:
|
|
77
|
+
|
|
78
|
+
for item in greedy(range(n)):
|
|
79
|
+
yield n
|
|
80
|
+
|
|
81
|
+
Normally a generator runs on demand.
|
|
82
|
+
This function dispatches a `Thread` to run the iterable
|
|
83
|
+
(typically a generator)
|
|
84
|
+
putting yielded values to a queue
|
|
85
|
+
and returns a new generator yielding from the queue.
|
|
86
|
+
|
|
87
|
+
The `queue_depth` parameter specifies the depth of the queue
|
|
88
|
+
and therefore how many values the original generator can compute
|
|
89
|
+
before blocking at the queue's capacity.
|
|
90
|
+
|
|
91
|
+
The default `queue_depth` is `0` which creates a `Channel`
|
|
92
|
+
as the queue - a zero storage buffer - which lets the generator
|
|
93
|
+
compute only a single value ahead of time.
|
|
94
|
+
|
|
95
|
+
A larger `queue_depth` allocates a `Queue` with that much storage
|
|
96
|
+
allowing the generator to compute as many as `queue_depth+1` values
|
|
97
|
+
ahead of time.
|
|
98
|
+
|
|
99
|
+
Here's a comparison of the behaviour:
|
|
100
|
+
|
|
101
|
+
Example without `@greedy`
|
|
102
|
+
where the "yield 1" step does not occur until after the "got 0":
|
|
103
|
+
|
|
104
|
+
>>> from time import sleep
|
|
105
|
+
>>> def g():
|
|
106
|
+
... for i in range(2):
|
|
107
|
+
... print("yield", i)
|
|
108
|
+
... yield i
|
|
109
|
+
... print("g done")
|
|
110
|
+
...
|
|
111
|
+
>>> G = g(); sleep(0.1)
|
|
112
|
+
>>> for i in G:
|
|
113
|
+
... print("got", i)
|
|
114
|
+
... sleep(0.1)
|
|
115
|
+
...
|
|
116
|
+
yield 0
|
|
117
|
+
got 0
|
|
118
|
+
yield 1
|
|
119
|
+
got 1
|
|
120
|
+
g done
|
|
121
|
+
|
|
122
|
+
Example with `@greedy`
|
|
123
|
+
where the "yield 1" step computes before the "got 0":
|
|
124
|
+
|
|
125
|
+
>>> from time import sleep
|
|
126
|
+
>>> @greedy
|
|
127
|
+
... def g():
|
|
128
|
+
... for i in range(2):
|
|
129
|
+
... print("yield", i)
|
|
130
|
+
... yield i
|
|
131
|
+
... print("g done")
|
|
132
|
+
...
|
|
133
|
+
>>> G = g(); sleep(0.1)
|
|
134
|
+
yield 0
|
|
135
|
+
>>> for i in G:
|
|
136
|
+
... print("got", repr(i))
|
|
137
|
+
... sleep(0.1)
|
|
138
|
+
...
|
|
139
|
+
yield 1
|
|
140
|
+
got 0
|
|
141
|
+
g done
|
|
142
|
+
got 1
|
|
143
|
+
|
|
144
|
+
Example with `@greedy(queue_depth=1)`
|
|
145
|
+
where the "yield 1" step computes before the "got 0":
|
|
146
|
+
|
|
147
|
+
>>> from cs.x import X
|
|
148
|
+
>>> from time import sleep
|
|
149
|
+
>>> @greedy
|
|
150
|
+
... def g():
|
|
151
|
+
... for i in range(3):
|
|
152
|
+
... X("Y")
|
|
153
|
+
... print("yield", i)
|
|
154
|
+
... yield i
|
|
155
|
+
... print("g done")
|
|
156
|
+
...
|
|
157
|
+
>>> G = g(); sleep(2)
|
|
158
|
+
yield 0
|
|
159
|
+
yield 1
|
|
160
|
+
>>> for i in G:
|
|
161
|
+
... print("got", repr(i))
|
|
162
|
+
... sleep(0.1)
|
|
163
|
+
...
|
|
164
|
+
yield 2
|
|
165
|
+
got 0
|
|
166
|
+
yield 3
|
|
167
|
+
got 1
|
|
168
|
+
g done
|
|
169
|
+
got 2
|
|
170
|
+
|
|
171
|
+
## <a name="imerge"></a>`imerge(*iters, **kw)`
|
|
172
|
+
|
|
173
|
+
Merge an iterable of ordered iterables in order.
|
|
174
|
+
|
|
175
|
+
Parameters:
|
|
176
|
+
* `iters`: an iterable of iterators
|
|
177
|
+
* `reverse`: keyword parameter: if true, yield items in reverse order.
|
|
178
|
+
This requires the iterables themselves to also be in
|
|
179
|
+
reversed order.
|
|
180
|
+
|
|
181
|
+
This function relies on the source iterables being ordered
|
|
182
|
+
and their elements being comparable, through slightly misordered
|
|
183
|
+
iterables (for example, as extracted from web server logs)
|
|
184
|
+
will produce only slightly misordered results, as the merging
|
|
185
|
+
is done on the basis of the front elements of each iterable.
|
|
186
|
+
|
|
187
|
+
## <a name="isordered"></a>`isordered(items, reverse=False, strict=False)`
|
|
188
|
+
|
|
189
|
+
Test whether an iterable is ordered.
|
|
190
|
+
Note that the iterable is iterated, so this is a destructive
|
|
191
|
+
test for nonsequences.
|
|
192
|
+
|
|
193
|
+
## <a name="last"></a>`last(iterable)`
|
|
194
|
+
|
|
195
|
+
Return the last item from an iterable; raise `IndexError` on empty iterables.
|
|
196
|
+
|
|
197
|
+
## <a name="onetomany"></a>`onetomany(func)`
|
|
198
|
+
|
|
199
|
+
A decorator for a method of a sequence to merge the results of
|
|
200
|
+
passing every element of the sequence to the function, expecting
|
|
201
|
+
multiple values back.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
|
|
205
|
+
class X(list):
|
|
206
|
+
@onetomany
|
|
207
|
+
def chars(self, item):
|
|
208
|
+
return item
|
|
209
|
+
strs = X(['Abc', 'Def'])
|
|
210
|
+
all_chars = X.chars()
|
|
211
|
+
|
|
212
|
+
## <a name="onetoone"></a>`onetoone(func)`
|
|
213
|
+
|
|
214
|
+
A decorator for a method of a sequence to merge the results of
|
|
215
|
+
passing every element of the sequence to the function, expecting a
|
|
216
|
+
single value back.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
|
|
220
|
+
class X(list):
|
|
221
|
+
@onetoone
|
|
222
|
+
def lower(self, item):
|
|
223
|
+
return item.lower()
|
|
224
|
+
strs = X(['Abc', 'Def'])
|
|
225
|
+
lower_strs = X.lower()
|
|
226
|
+
|
|
227
|
+
## <a name="Seq"></a>Class `Seq`
|
|
228
|
+
|
|
229
|
+
A numeric sequence implemented as a thread safe wrapper for
|
|
230
|
+
`itertools.count()`.
|
|
231
|
+
|
|
232
|
+
A `Seq` is iterable and both iterating and calling it return
|
|
233
|
+
the next number in the sequence.
|
|
234
|
+
|
|
235
|
+
## <a name="seq"></a>`seq()`
|
|
236
|
+
|
|
237
|
+
Return a new sequential value.
|
|
238
|
+
|
|
239
|
+
## <a name="skip_map"></a>`skip_map(func, *iterables, except_types, quiet=False)`
|
|
240
|
+
|
|
241
|
+
A version of `map()` which will skip items where `func(item)`
|
|
242
|
+
raises an exception in `except_types`, a tuple of exception types.
|
|
243
|
+
If a skipped exception occurs a warning will be issued unless
|
|
244
|
+
`quiet` is true (default `False`).
|
|
245
|
+
|
|
246
|
+
## <a name="splitoff"></a>`splitoff(sq, *sizes)`
|
|
247
|
+
|
|
248
|
+
Split a sequence into (usually short) prefixes and a tail,
|
|
249
|
+
for example to construct subdirectory trees based on a UUID.
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
|
|
253
|
+
>>> from uuid import UUID
|
|
254
|
+
>>> uuid = 'd6d9c510-785c-468c-9aa4-b7bda343fb79'
|
|
255
|
+
>>> uu = UUID(uuid).hex
|
|
256
|
+
>>> uu
|
|
257
|
+
'd6d9c510785c468c9aa4b7bda343fb79'
|
|
258
|
+
>>> splitoff(uu, 2, 2)
|
|
259
|
+
['d6', 'd9', 'c510785c468c9aa4b7bda343fb79']
|
|
260
|
+
|
|
261
|
+
## <a name="StatefulIterator"></a>Class `StatefulIterator`
|
|
262
|
+
|
|
263
|
+
A trivial iterator which wraps another iterator to expose some tracking state.
|
|
264
|
+
|
|
265
|
+
This has 2 attributes:
|
|
266
|
+
* `.it`: the internal iterator which should yield `(item,new_state)`
|
|
267
|
+
* `.state`: the last state value from the internal iterator
|
|
268
|
+
|
|
269
|
+
The originating use case is resuse of an iterator by independent
|
|
270
|
+
calls that are typically sequential, specificly the .read
|
|
271
|
+
method of file like objects. Naive sequential reads require
|
|
272
|
+
the underlying storage to locate the data on every call, even
|
|
273
|
+
though the previous call has just performed this task for the
|
|
274
|
+
previous read. Saving the iterator used from the preceeding
|
|
275
|
+
call allows the iterator to pick up directly if the file
|
|
276
|
+
offset hasn't been fiddled in the meantime.
|
|
277
|
+
|
|
278
|
+
## <a name="tee"></a>`tee(iterable, *Qs)`
|
|
279
|
+
|
|
280
|
+
A generator yielding the items from an iterable
|
|
281
|
+
which also copies those items to a series of queues.
|
|
282
|
+
|
|
283
|
+
Parameters:
|
|
284
|
+
* `iterable`: the iterable to copy
|
|
285
|
+
* `Qs`: the queues, objects accepting a `.put` method.
|
|
286
|
+
|
|
287
|
+
Note: the item is `.put` onto every queue
|
|
288
|
+
before being yielded from this generator.
|
|
289
|
+
|
|
290
|
+
## <a name="the"></a>`the(iterable, context=None)`
|
|
291
|
+
|
|
292
|
+
Returns the first element of an iterable, but requires there to be
|
|
293
|
+
exactly one.
|
|
294
|
+
|
|
295
|
+
## <a name="TrackingCounter"></a>Class `TrackingCounter`
|
|
296
|
+
|
|
297
|
+
A wrapper for a counter which can be incremented and decremented.
|
|
298
|
+
|
|
299
|
+
A facility is provided to wait for the counter to reach a specific value.
|
|
300
|
+
The .inc and .dec methods also accept a `tag` argument to keep
|
|
301
|
+
individual counts based on the tag to aid debugging.
|
|
302
|
+
|
|
303
|
+
TODO: add `strict` option to error and abort if any counter tries
|
|
304
|
+
to go below zero.
|
|
305
|
+
|
|
306
|
+
*`TrackingCounter.__init__(self, value=0, name=None, lock=None)`*:
|
|
307
|
+
Initialise the counter to `value` (default 0) with the optional `name`.
|
|
308
|
+
|
|
309
|
+
*`TrackingCounter.check(self)`*:
|
|
310
|
+
Internal consistency check.
|
|
311
|
+
|
|
312
|
+
*`TrackingCounter.dec(self, tag=None)`*:
|
|
313
|
+
Decrement the counter.
|
|
314
|
+
Wake up any threads waiting for its new value.
|
|
315
|
+
|
|
316
|
+
*`TrackingCounter.inc(self, tag=None)`*:
|
|
317
|
+
Increment the counter.
|
|
318
|
+
Wake up any threads waiting for its new value.
|
|
319
|
+
|
|
320
|
+
*`TrackingCounter.wait(self, value)`*:
|
|
321
|
+
Wait for the counter to reach the specified `value`.
|
|
322
|
+
|
|
323
|
+
## <a name="unrepeated"></a>`unrepeated(it, seen=None, signature=None)`
|
|
324
|
+
|
|
325
|
+
A generator yielding items from the iterable `it` with no repetitions.
|
|
326
|
+
|
|
327
|
+
Parameters:
|
|
328
|
+
* `it`: the iterable to process
|
|
329
|
+
* `seen`: an optional setlike container supporting `in` and `.add()`
|
|
330
|
+
* `signature`: an optional signature function for items from `it`
|
|
331
|
+
which produces the value to compare to recognise repeated items;
|
|
332
|
+
its values are stored in the `seen` set
|
|
333
|
+
|
|
334
|
+
The default `signature` function is equality;
|
|
335
|
+
the items are stored n `seen` and compared.
|
|
336
|
+
This requires the items to be hashable and support equality tests.
|
|
337
|
+
The same applies to whatever values the `signature` function produces.
|
|
338
|
+
|
|
339
|
+
Another common signature is identity: `id`, useful for
|
|
340
|
+
traversing a graph which may have cycles.
|
|
341
|
+
|
|
342
|
+
Since `seen` accrues all the signature values for yielded items
|
|
343
|
+
generally it will grow monotonicly as iteration proceeeds.
|
|
344
|
+
If the items are complex or large it is well worth providing a signature
|
|
345
|
+
function even if the items themselves can be used in a set.
|
|
346
|
+
|
|
347
|
+
# Release Log
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
*Release 20250103*:
|
|
352
|
+
New skip_map(func, *iterables, except_types, quiet=False) generator function, like map() but skipping certain exceptions.
|
|
353
|
+
|
|
354
|
+
*Release 20221118*:
|
|
355
|
+
Small doc improvement.
|
|
356
|
+
|
|
357
|
+
*Release 20220530*:
|
|
358
|
+
Seq: calling a Seq is like next(seq).
|
|
359
|
+
|
|
360
|
+
*Release 20210924*:
|
|
361
|
+
New greedy(iterable) or @greedy(generator_function) to let generators precompute.
|
|
362
|
+
|
|
363
|
+
*Release 20210913*:
|
|
364
|
+
New unrepeated() generator removing duplicates from an iterable.
|
|
365
|
+
|
|
366
|
+
*Release 20201025*:
|
|
367
|
+
New splitoff() function to split a sequence into (usually short) prefixes and a tail.
|
|
368
|
+
|
|
369
|
+
*Release 20200914*:
|
|
370
|
+
New common_prefix_length and common_suffix_length for comparing prefixes and suffixes of sequences.
|
|
371
|
+
|
|
372
|
+
*Release 20190103*:
|
|
373
|
+
Documentation update.
|
|
374
|
+
|
|
375
|
+
*Release 20190101*:
|
|
376
|
+
* New and UNTESTED class StatefulIterator to associate some externally visible state with an iterator.
|
|
377
|
+
* Seq: accept optional `lock` parameter.
|
|
378
|
+
|
|
379
|
+
*Release 20171231*:
|
|
380
|
+
* Python 2 backport for imerge().
|
|
381
|
+
* New tee function to duplicate an iterable to queues.
|
|
382
|
+
* Function isordered() is now a test instead of an assertion.
|
|
383
|
+
* Drop NamedTuple, NamedTupleClassFactory (unused).
|
|
384
|
+
|
|
385
|
+
*Release 20160918*:
|
|
386
|
+
* New function isordered() to test ordering of a sequence.
|
|
387
|
+
* imerge: accept new `reverse` parameter for merging reversed iterables.
|
|
388
|
+
|
|
389
|
+
*Release 20160828*:
|
|
390
|
+
Modify DISTINFO to say "install_requires", fixes pypi requirements.
|
|
391
|
+
|
|
392
|
+
*Release 20160827*:
|
|
393
|
+
TrackingCounter: accept presupplied lock object. Python 3 exec fix.
|
|
394
|
+
|
|
395
|
+
*Release 20150118*:
|
|
396
|
+
metadata update
|
|
397
|
+
|
|
398
|
+
*Release 20150111*:
|
|
399
|
+
Initial PyPI release.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
cs/seq.py,sha256=p9k6zwoF-wRvPKfFKS7MWPygalhqc0CohCcLmC0vFrI,18384
|
|
2
|
+
cs_seq-20250103.dist-info/METADATA,sha256=C9MINzOVG0z-icxM6Iy0_hUzvn7ErbP3KJzsZy5l9aY,12134
|
|
3
|
+
cs_seq-20250103.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
4
|
+
cs_seq-20250103.dist-info/top_level.txt,sha256=MJn10B_hUOb4f5hIJP5wKj_mMYsOQ6NUWqo9vSLmwcQ,3
|
|
5
|
+
cs_seq-20250103.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cs
|