immlib 1.0.0.dev2__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.
Files changed (45) hide show
  1. immlib/__init__.py +131 -0
  2. immlib/_init.py +108 -0
  3. immlib/_version.py +235 -0
  4. immlib/doc/__init__.py +38 -0
  5. immlib/doc/_core.py +311 -0
  6. immlib/iolib/__init__.py +29 -0
  7. immlib/iolib/_core.py +720 -0
  8. immlib/pathlib/__init__.py +69 -0
  9. immlib/pathlib/_cache.py +152 -0
  10. immlib/pathlib/_core.py +869 -0
  11. immlib/pathlib/_osf.py +538 -0
  12. immlib/test/__init__.py +16 -0
  13. immlib/test/__main__.py +10 -0
  14. immlib/test/doc/__init__.py +6 -0
  15. immlib/test/doc/test_core.py +91 -0
  16. immlib/test/iolib/__init__.py +7 -0
  17. immlib/test/iolib/test_core.py +81 -0
  18. immlib/test/pathlib/__init__.py +11 -0
  19. immlib/test/pathlib/test_core.py +146 -0
  20. immlib/test/pathlib/test_osf.py +54 -0
  21. immlib/test/types/__init__.py +5 -0
  22. immlib/test/types/test_core.py +110 -0
  23. immlib/test/util/__init__.py +11 -0
  24. immlib/test/util/test_core.py +681 -0
  25. immlib/test/util/test_numeric.py +1374 -0
  26. immlib/test/util/test_quantity.py +218 -0
  27. immlib/test/util/test_url.py +51 -0
  28. immlib/test/workflow/__init__.py +9 -0
  29. immlib/test/workflow/test_core.py +418 -0
  30. immlib/test/workflow/test_plantype.py +248 -0
  31. immlib/types/__init__.py +29 -0
  32. immlib/types/_core.py +333 -0
  33. immlib/util/__init__.py +283 -0
  34. immlib/util/_core.py +2524 -0
  35. immlib/util/_numeric.py +2651 -0
  36. immlib/util/_quantity.py +523 -0
  37. immlib/util/_url.py +114 -0
  38. immlib/workflow/__init__.py +48 -0
  39. immlib/workflow/_core.py +1635 -0
  40. immlib/workflow/_plantype.py +334 -0
  41. immlib-1.0.0.dev2.dist-info/METADATA +76 -0
  42. immlib-1.0.0.dev2.dist-info/RECORD +45 -0
  43. immlib-1.0.0.dev2.dist-info/WHEEL +5 -0
  44. immlib-1.0.0.dev2.dist-info/licenses/LICENSE +21 -0
  45. immlib-1.0.0.dev2.dist-info/top_level.txt +1 -0
immlib/util/_core.py ADDED
@@ -0,0 +1,2524 @@
1
+ # -*- coding: utf-8 -*-
2
+ ###############################################################################
3
+ # immlib/util/_core.py
4
+
5
+
6
+ # Dependencies ################################################################
7
+
8
+ import operator as op
9
+ from inspect import (signature, getfullargspec)
10
+ from functools import (wraps, partial, lru_cache)
11
+ from joblib import Memory
12
+ from pathlib import Path
13
+
14
+ import pint
15
+ import numpy as np
16
+ import scipy.sparse as sps
17
+ from pcollections import holdlazy
18
+
19
+ from ..doc import docwrap
20
+
21
+
22
+ # Strings #####################################################################
23
+
24
+ @docwrap('immlib.is_str')
25
+ def is_str(obj):
26
+ """Returns ``True`` if an object is a string and ``False`` otherwise.
27
+
28
+ ``is_str(obj)`` returns ``True`` if the given object `obj` is an instance
29
+ of the ``str`` type and ``False`` otherwise.
30
+
31
+ Parameters
32
+ ----------
33
+ obj : object
34
+ The object whose quality as a string object is to be assessed.
35
+
36
+ Returns
37
+ -------
38
+ bool
39
+ `True` if `obj` is a string, otherwise `False`.
40
+ """
41
+ return isinstance(obj, str)
42
+ from unicodedata import normalize as unicodedata_normalize
43
+ @docwrap('immlib.strnorm')
44
+ def strnorm(s, /, case=False, *, unicode=True):
45
+ """Normalizes a string using the ``unicodedata`` package.
46
+
47
+ ``strnorm(s)`` returns a version of `s` that has been unicode-normalized
48
+ using the ``unicodedata.normalize(s)`` function. Case-normalization can
49
+ also be requested via the `case` option.
50
+
51
+ Parameters
52
+ ----------
53
+ s : object
54
+ The string to be normalized.
55
+ case : bool, optional
56
+ Whether to perform case-normalization (``case=True``) or not
57
+ (``case=False``, the default). If two strings are case-normalized, then
58
+ an equality comparison will reveal whether the original (unnormalized
59
+ strings) were equal up to the case of the characters. Case
60
+ normalization is performed using the ``str.casefold()`` method.
61
+ unicode : bool or str, optional
62
+ Whether to perform unicode normalization via the
63
+ ``unicodedata.normalize`` function. The default behavior
64
+ (``unicode=True``) is to perform normalization, but this can be
65
+ disabled with ``unicode=False``. Alternatively, a string may be given,
66
+ in which case it is passed to the ``unicodedata.normalize`` function as
67
+ the first argument; when `unicode` is ``True``, the string used is
68
+ ``'NFD'``.
69
+
70
+ Returns
71
+ -------
72
+ str
73
+ A normalized version of `s`.
74
+ """
75
+ if unicode is True:
76
+ unicode = 'NFD'
77
+ if unicode:
78
+ s = unicodedata_normalize(unicode, s)
79
+ if case:
80
+ s = s.casefold()
81
+ s = unicodedata_normalize(unicode, s)
82
+ elif case:
83
+ s = s.casefold()
84
+ return s
85
+ def _strbinop_prep(a, b, case=True, unicode=None, strip=False):
86
+ if not is_str(a) or not is_str(b): return None
87
+ # We do case normalization when case comparison is *not* requested
88
+ casenorm = not bool(case)
89
+ # When unicode is None, we do its normalization only when case
90
+ # normalization is being done.
91
+ if unicode is None: unicode = casenorm
92
+ # We now perform normalization if it is required.
93
+ if unicode or casenorm:
94
+ a = strnorm(a, case=casenorm, unicode=unicode)
95
+ b = strnorm(b, case=casenorm, unicode=unicode)
96
+ # If we requested stripping, do that now.
97
+ if strip is True:
98
+ a = a.strip()
99
+ b = b.strip()
100
+ elif strip is not False:
101
+ a = a.strip(strip)
102
+ b = b.strip(strip)
103
+ return (a,b)
104
+ @docwrap('immlib.strcmp')
105
+ def strcmp(a, b, /, case=True, *, unicode=None, strip=False, split=False):
106
+ """Determines if the given objects are strings and compares them if so.
107
+
108
+ ``strcmp(a, b)`` returns ``None`` if either `a` or `b` is not a string;
109
+ otherwise, it returns ``-1``, ``0``, or ``1`` if `a` is less than, equal to,
110
+ or greater than `b`, respectively, subject to the constraints of the
111
+ parameters.
112
+
113
+ Parameters
114
+ ----------
115
+ a : object
116
+ The first argument.
117
+ b : object
118
+ The second argument.
119
+ case : bool, optional
120
+ Whether to perform case-sensitive (``case=True``) or case-insensitive
121
+ (``case=False``) string comparison. The default is ``False``.
122
+ unicode : bool or None, optional
123
+ Whether to run unicode normalization on `a` and `b` prior to
124
+ comparison. By default, this is ``None``, which is interpreted as a
125
+ `True` value when `case` is ``False`` and as ``False`` value when
126
+ `case` is ``True``. In other words, unicode normalization is performed
127
+ when case-insensitive comparison is being performed but not when
128
+ standard string comparison is being performed. Unicode normalization is
129
+ always performed both before and after casefolding. Unicode
130
+ normalization is performed using the ``unicodedata`` package's
131
+ ``normalize(unicode, string)`` function. If this argument is a string,
132
+ it is instead passed to the ``normalize`` function as the first
133
+ argument. When `unicode` is not a string but normalization is
134
+ performed, them the default string is ``'NFD'``.
135
+ strip : bool, optional
136
+ If set to ``True``, then ``a.strip()`` and ``b.strip()`` are used in
137
+ place of `a` and `b`. If set to ``False`` (the default), then no
138
+ stripping is performed. If a non-boolean value is given, then it is
139
+ passed as an argument to the ``strip()`` method.
140
+ split : bool, optional
141
+ If set to ``True``, then ``a.split()`` and ``b.split()`` are used in
142
+ place of `a` and `b`. The lists of strings that result from
143
+ ``a.split()`` and ``b.split()`` are rejoined with no separator prior to
144
+ comparison. If this option is set to `False` (the default), then no
145
+ splitting is performed. If a non-boolean value is given, then it is
146
+ passed as an argument to the ``split()`` method.
147
+
148
+ Returns
149
+ -------
150
+ bool or None
151
+ ``None`` if either `a` is not a string or `b` is not a string;
152
+ otherwise, ``-1`` if `a` is lexicographically less than `b`, ``0`` if
153
+ ``a == b``, and ``1`` if `a` is lexicographically greater than `b`,
154
+ subject to the constraints of the optional parameters.
155
+
156
+ See Also
157
+ --------
158
+ strnorm, streq
159
+
160
+ """
161
+ prep = _strbinop_prep(a, b, case=case, unicode=unicode, strip=strip)
162
+ if prep is None:
163
+ return None
164
+ (a, b) = prep
165
+ # If the split argument is true-ish, then we need to eliminate spaces.
166
+ if split is not False:
167
+ if split is True:
168
+ a = a.split()
169
+ b = b.split()
170
+ else:
171
+ a = a.split(split)
172
+ b = b.split(split)
173
+ # We can do this comparison by re-joining the strings with an empty
174
+ # separator. The lexicographically smaller string (excepting the
175
+ # spacers, which are now removed) will still be lexicographically
176
+ # smaller.
177
+ a = ''.join(a)
178
+ b = ''.join(b)
179
+ return (-1 if a < b else 1 if a > b else 0)
180
+ @docwrap('immlib.streq')
181
+ def streq(a, b, /, case=True, *, unicode=None, strip=False, split=False):
182
+ """Determines if the given objects are equal strings or not.
183
+
184
+ ``streq(a, b)`` returns ``True`` if `a` and `b` are both strings and are
185
+ equal to each other, subject to the constraints of the options.
186
+
187
+ Parameters
188
+ ----------
189
+ %(immlib.strcmp.parameters)s
190
+
191
+ Returns
192
+ -------
193
+ bool or None
194
+ If `a` and `b` are both strings then ``True`` is returned if `a` equals
195
+ `b` and ``False`` is returned otherwise. If either `a` or `b` is not a
196
+ string, then ``None`` is returned.
197
+
198
+ See Also
199
+ --------
200
+ strnorm, strcmp
201
+ """
202
+ cmpval = strcmp(a, b, case=case, unicode=unicode, strip=strip, split=split)
203
+ return None if cmpval is None else (cmpval == 0)
204
+ @docwrap('immlib.strends')
205
+ def strends(a, b, /, case=True, *, unicode=None, strip=False):
206
+ """Determines whether or not the string `a` ends with the string `b`.
207
+
208
+ ``strends(a, b)`` returns ``True`` if `a` and `b` are both strings and if
209
+ `a` ends with `b`, subject to the constraints of the parameters.
210
+
211
+ Parameters
212
+ ----------
213
+ %(immlib.strcmp.parameters.case)s
214
+ %(immlib.strcmp.parameters.unicode)s
215
+ %(immlib.strcmp.parameters.strip)s
216
+
217
+ Returns
218
+ -------
219
+ bool or None
220
+ If `a` and `b` are both strings then ``True`` is returned if `a` ends
221
+ with `b` and ``False`` is returned otherwise. If either `a` or `b` is
222
+ not a string, then ``None`` is returned.
223
+ """
224
+ prep = _strbinop_prep(a, b, case=case, unicode=unicode, strip=strip)
225
+ if prep is None: return None
226
+ else: (a, b) = prep
227
+ # Check the ending
228
+ return a.endswith(b)
229
+ @docwrap('immlib.strstarts')
230
+ def strstarts(a, b, /, case=True, *, unicode=None, strip=False):
231
+ """Determines whether or not the string `a` starts with the string `b`.
232
+
233
+ ``strstarts(a, b)`` returns ``True`` if `a` and `b` are both strings and if
234
+ `a` starts with `b`, subject to the constraints of the parameters.
235
+
236
+ Parameters
237
+ ----------
238
+ %(immlib.strcmp.parameters.case)s
239
+ %(immlib.strcmp.parameters.unicode)s
240
+ %(immlib.strcmp.parameters.strip)s
241
+
242
+ Returns
243
+ -------
244
+ bool or None
245
+ ``True` if `a` and `b` are both strings and if `a` startss with `b`,
246
+ subject to the constraints of the optional parameters. If either `a` or
247
+ `b` is not a string, then ``None`` is returned.
248
+ """
249
+ prep = _strbinop_prep(a, b, case=case, unicode=unicode, strip=strip)
250
+ if prep is None: return None
251
+ else: (a, b) = prep
252
+ # Check the beginning.
253
+ return a.startswith(b)
254
+ @docwrap('immlib.strissym')
255
+ def strissym(s):
256
+ """Determines if the given string is a valid symbol (identifier).
257
+
258
+ ``strissym(s)`` returns ``True`` if `s` is both a string and a valid
259
+ identifier. Otherwise, it returns ``False`` if `s` is a string and
260
+ ``None`` if not.
261
+
262
+ See also
263
+ --------
264
+ striskey, strisvar
265
+ """
266
+ return s.isidentifier() if is_str(s) else None
267
+ from keyword import iskeyword
268
+ @docwrap('immlib.striskey')
269
+ def striskey(s):
270
+ """Determines if the given string is a valid keyword.
271
+
272
+ ``strissym(s)`` returns ``True`` if `s` is both a string and a valid keyword
273
+ (such as ``'if'`` or ``'while'``). Otherwise, it returns ``False`` if `s` is a
274
+ string and ``None`` if not.
275
+
276
+ See Also
277
+ --------
278
+ strissym, strisvar
279
+ """
280
+ return iskeyword(s) if is_str(s) else None
281
+ @docwrap('immlib.strisvar')
282
+ def strisvar(s):
283
+ """Determines if the given string is a valid variable name.
284
+
285
+ ``strissym(s)`` returns ``True`` if `s` is both a string and a valid name
286
+ (i.e., a symbol but not a keyword). Otherwise, it returns ``False`` if `s`
287
+ is a string and ``None`` if not.
288
+
289
+ See Also
290
+ --------
291
+ strissym, striskey
292
+ """
293
+ return (
294
+ None if not is_str(s) else
295
+ False if iskeyword(s) else
296
+ s.isidentifier())
297
+
298
+
299
+ # Builtin Python Abstract Types ###############################################
300
+
301
+ from collections.abc import Callable
302
+ @docwrap('immlib.is_acallable')
303
+ def is_acallable(obj):
304
+ """Returns ``True`` if an object is a callable object like a function.
305
+
306
+ ``is_acallable(obj)`` returns ``True`` if the given object `obj` is an
307
+ instance of the abstract callable type, ``collections.abc.Callable``. Note
308
+ that in general, it is more common and preferable to use the builtin
309
+ ``callable`` function; however, ``is_acallable`` is included in ``immlib``
310
+ for completeness.
311
+
312
+ Parameters
313
+ ----------
314
+ obj : object
315
+ The object whose quality as an ``Callable`` object is to be assessed.
316
+
317
+ Returns
318
+ -------
319
+ bool
320
+ ``True`` if `obj` is an instance of ``Callable``, otherwise ``False``.
321
+ """
322
+ return isinstance(obj, Callable)
323
+ from types import LambdaType
324
+ @docwrap('immlib.is_lambda')
325
+ def is_lambda(obj):
326
+ """Returns ``True`` if an object is a lambda function, otherwise ``False``.
327
+
328
+ ``is_lambda(obj)`` returns ``True`` if the given object `obj` is an
329
+ instance of the ``types.LambdaType`` type.
330
+
331
+ Parameters
332
+ ----------
333
+ obj : object
334
+ The object whose quality as a ``LambdaType`` object is to be assessed.
335
+
336
+ Returns
337
+ -------
338
+ bool
339
+ ``True`` if `obj` is an instance of ``LambdaType``, otherwise
340
+ ``False``.
341
+
342
+ """
343
+ return isinstance(obj, LambdaType)
344
+ from collections.abc import Sized
345
+ @docwrap('immlib.is_asized')
346
+ def is_asized(obj):
347
+ """Returns ``True`` if an object implements ``len()``, otherwise ``False``.
348
+
349
+ ``is_asized(obj)`` returns ``True`` if the given object `obj` is an
350
+ instance of the abstract ``collections.abc.Sized`` type.
351
+
352
+ Parameters
353
+ ----------
354
+ obj : object
355
+ The object whose quality as a ``Sized`` object is to be assessed.
356
+
357
+ Returns
358
+ -------
359
+ bool
360
+ ``True`` if `obj` is an instance of ``Sized``, otherwise ``False``.
361
+
362
+ """
363
+ return isinstance(obj, Sized)
364
+ from collections.abc import Container
365
+ @docwrap('immlib.is_acontainer')
366
+ def is_acontainer(obj):
367
+ """Returns ``True`` if an object implements ``__contains__``, otherwise
368
+ ``False``.
369
+
370
+ ``is_acontainer(obj)`` returns ``True`` if the given object `obj` is an
371
+ instance of the abstract ``collections.abc.Container`` type.
372
+
373
+ Parameters
374
+ ----------
375
+ obj : object
376
+ The object whose quality as a ``Container`` object is to be assessed.
377
+
378
+ Returns
379
+ -------
380
+ bool
381
+ ``True`` if `obj` is an instance of ``Container``, otherwise ``False``.
382
+ """
383
+ return isinstance(obj, Container)
384
+ from collections.abc import Iterable
385
+ @docwrap('immlib.is_aiterable')
386
+ def is_aiterable(obj):
387
+ """Returns ``True`` if an object implements ``__iter__``, otherwise
388
+ ``False``.
389
+
390
+ ``is_aiterable(obj)`` returns ``True`` if the given object `obj` is an
391
+ instance of the abstract ``collections.abc.Iterable`` type.
392
+
393
+ Parameters
394
+ ----------
395
+ obj : object
396
+ The object whose quality as an ``Iterable`` object is to be assessed.
397
+
398
+ Returns
399
+ -------
400
+ bool
401
+ ``True`` if `obj` is an instance of ``Iterable``, otherwise ``False``.
402
+ """
403
+ return isinstance(obj, Iterable)
404
+ from collections.abc import Iterator
405
+ @docwrap('immlib.is_aiterator')
406
+ def is_aiterator(obj):
407
+ """Returns ``True`` if an object is an instance of
408
+ ``collections.abc.Iterator``.
409
+
410
+ ``is_aiterable(obj)`` returns ``True`` if the given object `obj` is an
411
+ instance of the abstract ``collections.abc.Iterator`` type.
412
+
413
+ Parameters
414
+ ----------
415
+ obj : object
416
+ The object whose quality as an ``Iterator`` object is to be assessed.
417
+
418
+ Returns
419
+ -------
420
+ bool
421
+ ``True`` if `obj` is an instance of ``Iterator``, otherwise ``False``.
422
+ """
423
+ return isinstance(obj, Iterator)
424
+ from collections.abc import Reversible
425
+ @docwrap('immlib.is_areversible')
426
+ def is_areversible(obj):
427
+ """Returns ``True`` if an object is an instance of ``Reversible``.
428
+
429
+ ``is_areversible(obj)`` returns ``True`` if the given object `obj` is an
430
+ instance of the abstract ``collections.abc.Reversible`` type.
431
+
432
+ Parameters
433
+ ----------
434
+ obj : object
435
+ The object whose quality as an ``Reversible`` object is to be assessed.
436
+
437
+ Returns
438
+ -------
439
+ bool
440
+ ``True`` if `obj` is an instance of ``Reversible``, otherwise
441
+ ``False``.
442
+ """
443
+ return isinstance(obj, Reversible)
444
+ from collections.abc import Collection
445
+ @docwrap('immlib.is_acoll')
446
+ def is_acoll(obj):
447
+ """Returns ``True`` if an object is a collection (a sized iterable
448
+ container).
449
+
450
+ ``is_acoll(obj)`` returns ``True`` if the given object `obj` is an instance
451
+ of the abstract ``collections.abc.Collection`` type.
452
+
453
+ Parameters
454
+ ----------
455
+ obj : object
456
+ The object whose quality as an ``Collection`` object is to be assessed.
457
+
458
+ Returns
459
+ -------
460
+ bool
461
+ ``True`` if `obj` is an instance of ``Collection``, otherwise
462
+ ``False``.
463
+ """
464
+ return isinstance(obj, Collection)
465
+ from collections.abc import Sequence
466
+ @docwrap('immlib.is_aseq')
467
+ def is_aseq(obj):
468
+ """Returns ``True`` if an object is a sequence, otherwise ``False``.
469
+
470
+ ``is_aseq(obj)`` returns ``True`` if the given object `obj` is an instance
471
+ of the abstract ``collections.abc.Sequence`` type.
472
+
473
+ Parameters
474
+ ----------
475
+ obj : object
476
+ The object whose quality as an ``Sequence`` object is to be assessed.
477
+
478
+ Returns
479
+ -------
480
+ bool
481
+ ``True`` if `obj` is an instance of ``Sequence``, otherwise ``False``.
482
+ """
483
+ return isinstance(obj, Sequence)
484
+ from collections.abc import MutableSequence
485
+ @docwrap('immlib.is_amseq')
486
+ def is_amseq(obj):
487
+ """Returns ``True`` if an object is a mutable sequence, otherwise
488
+ ``False``.
489
+
490
+ ``is_amseq(obj)`` returns ``True`` if the given object `obj` is an instance
491
+ of the abstract ``collections.abc.MutableSequence`` type.
492
+
493
+ Parameters
494
+ ----------
495
+ obj : object
496
+ The object whose quality as an ``MutableSequence`` object is to be
497
+ assessed.
498
+
499
+ Returns
500
+ -------
501
+ bool
502
+ ``True`` if `obj` is an instance of ``MutableSequence``, otherwise
503
+ ``False``.
504
+ """
505
+ return isinstance(obj, MutableSequence)
506
+ from pcollections.abc import PersistentSequence
507
+ @docwrap('immlib.is_apseq')
508
+ def is_apseq(obj):
509
+ """Returns ``True`` if an object is a persistent sequence, otherwise
510
+ ``False``.
511
+
512
+ ``is_apseq(obj)`` returns ``True`` if the given object `obj` is an instance
513
+ of the abstract ``pcollections.abc.PersistentSequence`` type.
514
+
515
+ Parameters
516
+ ----------
517
+ obj : object
518
+ The object whose quality as an ``PersistentSequence`` object is to be
519
+ assessed.
520
+
521
+ Returns
522
+ -------
523
+ bool
524
+ ``True`` if `obj` is an instance of ``PersistentSequence``, otherwise
525
+ ``False``.
526
+ """
527
+ return isinstance(obj, PersistentSequence)
528
+ from collections.abc import ByteString
529
+ @docwrap('immlib.is_abytes')
530
+ def is_abytes(obj):
531
+ """Returns ``True`` if an object is a byte-string, otherwise ``False``.
532
+
533
+ ``is_abytes(obj)`` returns ``True`` if the given object `obj` is an
534
+ instance of the abstract ``collections.abc.ByteString`` type.
535
+
536
+ Parameters
537
+ ----------
538
+ obj : object
539
+ The object whose quality as an ``ByteString`` object is to be assessed.
540
+
541
+ Returns
542
+ -------
543
+ bool
544
+ ``True`` if `obj` is an instance of ``ByteString``, otherwise
545
+ ``False``.
546
+ """
547
+ return isinstance(obj, ByteString)
548
+ @docwrap('immlib.is_bytes')
549
+ def is_bytes(obj):
550
+ """Returns ``True`` if an object is a ``bytes`` object, otherwise
551
+ ``False``.
552
+
553
+ ``is_bytes(obj)`` returns ``True`` if the given object `obj` is an instance
554
+ of the ``bytes`` type and returns ``False`` otherwise.
555
+
556
+ Parameters
557
+ ----------
558
+ obj : object
559
+ The object whose quality as an ``bytes`` object is to be assessed.
560
+
561
+ Returns
562
+ -------
563
+ bool
564
+ ``True`` if `obj` is an instance of ``bytes``, otherwise ``False``.
565
+ """
566
+ return isinstance(obj, bytes)
567
+ from collections.abc import Set
568
+ @docwrap('immlib.is_aset')
569
+ def is_aset(obj):
570
+ """Returns ``True`` if an object is a set type, otherwise ``False``.
571
+
572
+ ``is_aset(obj)`` returns ``True`` if the given object `obj` is an instance
573
+ of the abstract ``collections.abc.Set`` type.
574
+
575
+ Parameters
576
+ ----------
577
+ obj : object
578
+ The object whose quality as an ``Set`` object is to be assessed.
579
+
580
+ Returns
581
+ -------
582
+ bool
583
+ ``True`` if `obj` is an instance of ``Set``, otherwise ``False``.
584
+ """
585
+ return isinstance(obj, Set)
586
+ from collections.abc import MutableSet
587
+ @docwrap('immlib.is_amset')
588
+ def is_amset(obj):
589
+ """Returns ``True`` if an object is a mutable set, otherwise ``False``.
590
+
591
+ ``is_amset(obj)`` returns ``True`` if the given object `obj` is an instance
592
+ of the abstract ``collections.abc.MutableSet`` type.
593
+
594
+ Parameters
595
+ ----------
596
+ obj : object
597
+ The object whose quality as an ``MutableSet`` object is to be assessed.
598
+
599
+ Returns
600
+ -------
601
+ bool
602
+ ``True`` if `obj` is an instance of ``MutableSet``, otherwise
603
+ ``False``.
604
+ """
605
+ return isinstance(obj, MutableSet)
606
+ from pcollections.abc import PersistentSet
607
+ @docwrap('immlib.is_apset')
608
+ def is_apset(obj):
609
+ """Returns ``True`` if an object is a persistent set, otherwise ``False``.
610
+
611
+ ``is_apset(obj)`` returns ``True`` if the given object `obj` is an instance
612
+ of the abstract ``pcollections.abc.PersistentSet`` type.
613
+
614
+ Parameters
615
+ ----------
616
+ obj : object
617
+ The object whose quality as an ``PersistentSet`` object is to be
618
+ assessed.
619
+
620
+ Returns
621
+ -------
622
+ bool
623
+ ``True`` if `obj` is an instance of ``PersistentSet``, otherwise
624
+ ``False``.
625
+ """
626
+ return isinstance(obj, PersistentSet)
627
+ from collections.abc import Mapping
628
+ @docwrap('immlib.is_amap')
629
+ def is_amap(obj):
630
+ """Returns ``True`` if an object is an abstract mapping, otherwise
631
+ ``False``.
632
+
633
+ ``is_amap(obj)`` returns ``True`` if the given object `obj` is an instance
634
+ of the abstract ``collections.abc.Mapping`` type.
635
+
636
+ Parameters
637
+ ----------
638
+ obj : object
639
+ The object whose quality as an ``Mapping`` object is to be assessed.
640
+
641
+ Returns
642
+ -------
643
+ bool
644
+ ``True`` if `obj` is an instance of ``Mapping``, otherwise ``False``.
645
+ """
646
+ return isinstance(obj, Mapping)
647
+ from collections.abc import MutableMapping
648
+ @docwrap('immlib.is_ammap')
649
+ def is_ammap(obj):
650
+ """Returns ``True`` if an object is a mutable mapping, otherwise ``False``.
651
+
652
+ ``is_ammap(obj)`` returns ``True`` if the given object ``obj`` is an
653
+ instance of the abstract ``collections.abc.MutableMapping`` type.
654
+
655
+ Parameters
656
+ ----------
657
+ obj : object
658
+ The object whose quality as an ``MutableMapping`` object is to be
659
+ assessed.
660
+
661
+ Returns
662
+ -------
663
+ bool
664
+ ``True`` if `obj` is an instance of ``MutableMapping``, otherwise
665
+ ``False``.
666
+ """
667
+ return isinstance(obj, MutableMapping)
668
+ from pcollections.abc import PersistentMapping
669
+ @docwrap('immlib.is_apmap')
670
+ def is_apmap(obj):
671
+ """Returns ``True`` if an object is a persistent mapping, otherwise
672
+ ``False``.
673
+
674
+ ``is_apmap(obj)`` returns ``True`` if the given object `obj` is an instance
675
+ of the abstract ``pcollections.abc.PersistentMapping`` type.
676
+
677
+ Parameters
678
+ ----------
679
+ obj : object
680
+ The object whose quality as an ``PersistentMapping`` object is to be
681
+ assessed.
682
+
683
+ Returns
684
+ -------
685
+ bool
686
+ ``True`` if `obj` is an instance of ``PersistentMapping``, otherwise
687
+ ``False``.
688
+ """
689
+ return isinstance(obj, PersistentMapping)
690
+ from collections.abc import Hashable
691
+ @docwrap('immlib.is_ahashable')
692
+ def is_ahashable(obj):
693
+ """Returns ``True`` if an object is a hashable object, otherwise ``False``.
694
+
695
+ ``is_ahashable(obj)`` returns ``True`` if the given object `obj` is an
696
+ instance of the abstract ``collections.abc.Hashable`` type. This differs
697
+ from the ``can_hash`` function, which checks whehter calling ``hash`` on an
698
+ object raises an exception.
699
+
700
+ Parameters
701
+ ----------
702
+ obj : object
703
+ The object whose quality as an ``Hashable`` object is to be assessed.
704
+
705
+ Returns
706
+ -------
707
+ boolean
708
+ ``True`` if `obj` is an instance of ``Hashable``, otherwise ``False``.
709
+
710
+ See Also
711
+ --------
712
+ can_hash
713
+ """
714
+ return isinstance(obj, Hashable)
715
+
716
+
717
+ # Builtin Python Concrete Types ###############################################
718
+
719
+ @docwrap('immlib.is_list')
720
+ def is_list(obj):
721
+ """Returns ``True`` if an object is a ``list`` object.
722
+
723
+ ``is_list(obj)`` returns ``True`` if the given object `obj` is an instance
724
+ of the ``list`` type.
725
+
726
+ Parameters
727
+ ----------
728
+ obj : object
729
+ The object whose quality as an ``list`` object is to be assessed.
730
+
731
+ Returns
732
+ -------
733
+ bool
734
+ ``True`` if `obj` is an instance of ``list``, otherwise ``False``.
735
+ """
736
+ return isinstance(obj, list)
737
+ @docwrap('immlib.is_tuple')
738
+ def is_tuple(obj):
739
+ """Returns ``True`` if an object is a ``tuple`` object.
740
+
741
+ ``is_tuple(obj)`` returns ``True`` if the given object `obj` is an instance
742
+ of the ``tuple`` type.
743
+
744
+ Parameters
745
+ ----------
746
+ obj : object
747
+ The object whose quality as an ``tuple`` object is to be assessed.
748
+
749
+ Returns
750
+ -------
751
+ bool
752
+ ``True`` if `obj` is an instance of ``tuple``, otherwise ``False``.
753
+ """
754
+ return isinstance(obj, tuple)
755
+ from pcollections import plist
756
+ @docwrap('immlib.is_plist')
757
+ def is_plist(obj):
758
+ """Returns ``True`` if an object is a persistent list object.
759
+
760
+ ``is_plist(obj)`` returns ``True`` if the given object `obj` is an instance
761
+ of the ``pcollections.plist`` type and ``False`` otherwise.
762
+
763
+ Parameters
764
+ ----------
765
+ obj : object
766
+ The object whose quality as a ``plist`` object is to be assessed.
767
+
768
+ Returns
769
+ -------
770
+ bool
771
+ ``True`` if `obj` is an instance of ``plist``, otherwise ``False``.
772
+ """
773
+ return isinstance(obj, plist)
774
+ from pcollections import tlist
775
+ @docwrap('immlib.is_tlist')
776
+ def is_tlist(obj):
777
+ """Returns ``True`` if an object is a transient list object.
778
+
779
+ ``is_tlist(obj)`` returns ``True`` if the given object `obj` is an instance
780
+ of the ``pcollections.tlist`` type and ``False`` otherwise.
781
+
782
+ Parameters
783
+ ----------
784
+ obj : object
785
+ The object whose quality as a ``tlist`` object is to be assessed.
786
+
787
+ Returns
788
+ -------
789
+ bool
790
+ ``True`` if `obj` is an instance of ``tlist``, otherwise ``False``.
791
+ """
792
+ return isinstance(obj, tlist)
793
+ from pcollections import llist
794
+ @docwrap('immlib.is_llist')
795
+ def is_llist(obj):
796
+ """Returns ``True`` if an object is a persistent lazy list object.
797
+
798
+ ``is_llist(obj)`` returns ``True`` if the given object `obj` is an instance
799
+ of the ``pcollections.llist`` type and ``False`` otherwise.
800
+
801
+ Parameters
802
+ ----------
803
+ obj : object
804
+ The object whose quality as a ``llist`` object is to be assessed.
805
+
806
+ Returns
807
+ -------
808
+ bool
809
+ ``True`` if `obj` is an instance of ``llist``, otherwise ``False``.
810
+ """
811
+ return isinstance(obj, llist)
812
+ @docwrap('immlib.is_set')
813
+ def is_set(obj):
814
+ """Returns ``True`` if an object is a ``set`` object.
815
+
816
+ ``is_set(obj)`` returns ``True`` if the given object `obj` is an instance
817
+ of the ``set`` type. Note that this is not the same as ``is_aset`` which
818
+ determines whether the object is of the ``collections.abc.Set`` abstract
819
+ type.
820
+
821
+ Parameters
822
+ ----------
823
+ obj : object
824
+ The object whose quality as an ``set`` object is to be assessed.
825
+
826
+ Returns
827
+ -------
828
+ bool
829
+ ``True`` if `obj` is an instance of ``set``, otherwise ``False``.
830
+
831
+ See Also
832
+ --------
833
+ is_aset, is_amset, is_apset, is_pset, is_tset, is_frozenset
834
+ """
835
+ return isinstance(obj, set)
836
+ @docwrap('immlib.is_frozenset')
837
+ def is_frozenset(obj):
838
+ """Returns ``True`` if an object is a ``frozenset`` object.
839
+
840
+ ``is_frozenset(obj)`` returns ``True`` if the given object `obj` is an
841
+ instance of the ``frozenset`` type.
842
+
843
+ Parameters
844
+ ----------
845
+ obj : object
846
+ The object whose quality as an ``frozenset`` object is to be assessed.
847
+
848
+ Returns
849
+ -------
850
+ bool
851
+ ``True`` if `obj` is an instance of ``frozenset``, otherwise ``False``.
852
+
853
+ See Also
854
+ --------
855
+ is_set, is_aset, is_amset, is_apset, is_pset, is_tset
856
+ """
857
+ return isinstance(obj, frozenset)
858
+ from pcollections import pset
859
+ @docwrap('immlib.is_pset')
860
+ def is_pset(obj):
861
+ """Returns ``True`` if an object is a persistent set object.
862
+
863
+ ``is_pset(obj)`` returns ``True`` if the given object `obj` is an instance
864
+ of the ``pcollections.pset`` type and ``False`` otherwise.
865
+
866
+ Parameters
867
+ ----------
868
+ obj : object
869
+ The object whose quality as a ``pset`` object is to be assessed.
870
+
871
+ Returns
872
+ -------
873
+ bool
874
+ ``True`` if `obj` is an instance of ``pset``, otherwise ``False``.
875
+
876
+ See Also
877
+ --------
878
+ is_set, is_aset, is_amset, is_apset, is_tset, is_frozenset
879
+
880
+ """
881
+ return isinstance(obj, pset)
882
+ from pcollections import tset
883
+ @docwrap('immlib.is_tset')
884
+ def is_tset(obj):
885
+ """Returns ``True`` if an object is a transient set object.
886
+
887
+ ``is_tset(obj)`` returns ``True`` if the given object `obj` is an instance
888
+ of the ``pcollections.tset`` type and ``False`` otherwise.
889
+
890
+ Parameters
891
+ ----------
892
+ obj : object
893
+ The object whose quality as a ``tset`` object is to be assessed.
894
+
895
+ Returns
896
+ -------
897
+ bool
898
+ ``True`` if `obj` is an instance of ``tset``, otherwise ``False``.
899
+ """
900
+ return isinstance(obj, tset)
901
+ @docwrap('immlib.is_dict')
902
+ def is_dict(obj):
903
+ """Returns ``True`` if an object is a ``dict`` object.
904
+
905
+ ``is_dict(obj)`` returns ``True`` if the given object `obj` is an instance
906
+ of the ``dict`` type.
907
+
908
+ Parameters
909
+ ----------
910
+ obj : object
911
+ The object whose quality as an ``dict`` object is to be assessed.
912
+
913
+ Returns
914
+ -------
915
+ bool
916
+ ``True`` if `obj` is an instance of ``dict``, otherwise ``False``.
917
+ """
918
+ return isinstance(obj, dict)
919
+ from collections import OrderedDict
920
+ @docwrap('immlib.is_odict')
921
+ def is_odict(obj):
922
+ """Returns ``True`` if an object is an ``OrderedDict`` object.
923
+
924
+ ``is_odict(obj)`` returns ``True`` if the given object `obj` is an instance
925
+ of the ``collections.OrderedDict`` type.
926
+
927
+ Parameters
928
+ ----------
929
+ obj : object
930
+ The object whose quality as an ``OrderedDict`` object is to be
931
+ assessed.
932
+
933
+ Returns
934
+ -------
935
+ bool
936
+ ``True`` if `obj` is an instance of ``OrderedDict``, otherwise
937
+ ``False``.
938
+ """
939
+ return isinstance(obj, OrderedDict)
940
+ from collections import defaultdict
941
+ @docwrap('immlib.is_ddict')
942
+ def is_ddict(obj):
943
+ """Returns ``True`` if an object is a ``defaultdict`` object.
944
+
945
+ ``is_ddict(obj)`` returns ``True`` if the given object `obj` is an instance
946
+ of the ``collections.defaultdict`` type.
947
+
948
+ Parameters
949
+ ----------
950
+ obj : object
951
+ The object whose quality as a ``defaultdict`` object is to be assessed.
952
+
953
+ Returns
954
+ -------
955
+ bool
956
+ ``True`` if `obj` is an instance of ``defaultdict``, otherwise
957
+ ``False``.
958
+ """
959
+ return isinstance(obj, defaultdict)
960
+ from pcollections import pdict
961
+ @docwrap('immlib.is_pdict')
962
+ def is_pdict(obj):
963
+ """Returns ``True`` if an object is a persistent dictionary object.
964
+
965
+ ``is_pdict(obj)`` returns ``True`` if the given object `obj` is an instance
966
+ of the ``pcollections.pdict`` type and ``False`` otherwise.
967
+
968
+ .. Note:: The ``ldict`` type is a subtype of ``pdict``, so for
969
+ ``is_pdict(ldict())`` returns ``True``.
970
+
971
+ Parameters
972
+ ----------
973
+ obj : object
974
+ The object whose quality as a ``pdict`` object is to be assessed.
975
+
976
+ Returns
977
+ -------
978
+ bool
979
+ ``True`` if `obj` is an instance of ``pdict``, otherwise ``False``.
980
+ """
981
+ return isinstance(obj, pdict)
982
+ from pcollections import tdict, tldict
983
+ @docwrap('immlib.is_tdict')
984
+ def is_tdict(obj):
985
+ """Returns ``True`` if an object is a transient dictionary object.
986
+
987
+ ``is_tdict(obj)`` returns ``True`` if the given object `obj` is an instance
988
+ of the ``pcollections.tdict`` type and ``False`` otherwise.
989
+
990
+ Parameters
991
+ ----------
992
+ obj : object
993
+ The object whose quality as a ``tdict`` object is to be assessed.
994
+
995
+ Returns
996
+ -------
997
+ bool
998
+ ``True`` if `obj` is an instance of ``tdict``, otherwise ``False``.
999
+ """
1000
+ return isinstance(obj, tdict)
1001
+ from pcollections import ldict
1002
+ @docwrap('immlib.is_ldict')
1003
+ def is_ldict(obj):
1004
+ """Returns ``True`` if an object is a persistent lazy dictionary object.
1005
+
1006
+ ``is_ldict(obj)`` returns ``True`` if the given object `obj` is an instance
1007
+ of the ``pcollections.ldict`` type and ``False`` otherwise.
1008
+
1009
+ Parameters
1010
+ ----------
1011
+ obj : object
1012
+ The object whose quality as a ``ldict`` object is to be assessed.
1013
+
1014
+ Returns
1015
+ -------
1016
+ bool
1017
+ ``True`` if `obj` is an instance of ``ldict``, otherwise ``False``.
1018
+ """
1019
+ return isinstance(obj, ldict)
1020
+ @docwrap('immlib.hashsafe')
1021
+ def hashsafe(obj):
1022
+ """Returns ``hash(obj)`` if `obj` is hashable, otherwise returns ``None``.
1023
+
1024
+ This function attempts to hash an object and returns ``None`` when doing so
1025
+ raises a ``TypeError``.
1026
+
1027
+ .. Note:: A fairly reliable test of whether an object is immutable or not
1028
+ in Python is whether it can be hashed.
1029
+
1030
+ Parameters
1031
+ ----------
1032
+ obj : object
1033
+ The object to be hashed.
1034
+
1035
+ Returns
1036
+ -------
1037
+ int or None
1038
+ If the object is hashable, returns the hashcode; otherwise, returns
1039
+ ``None``.
1040
+
1041
+ See Also
1042
+ --------
1043
+ can_hash, is_ahashable
1044
+ """
1045
+ try:
1046
+ return hash(obj)
1047
+ except TypeError:
1048
+ return None
1049
+ @docwrap('immlib.can_hash')
1050
+ def can_hash(obj):
1051
+ """Returns ``True`` if `obj` is safe to hash and ``False`` otherwise.
1052
+
1053
+ ``can_hash(obj)`` is equivalent to ``hashsafe(obj) is not None``. This
1054
+ differs from ``is_ahashable(obj)`` in that ``is_ahashable`` only checks
1055
+ whether `obj` is an instance of ``Hashable`` while ``hashsafe(obj)``
1056
+ attempts to hash `obj` and returns ``None`` when a ``TypeError`` is raised.
1057
+
1058
+ .. Note:: A fairly reliable test of whether an object is immutable or not
1059
+ in Python is whether it can be hashed.
1060
+
1061
+ See Also
1062
+ --------
1063
+ hashsafe, is_ahashable
1064
+ """
1065
+ return hashsafe(obj) is not None
1066
+ @docwrap('immlib.itersafe')
1067
+ def itersafe(obj):
1068
+ """Returns an iterator of the given object or ``None`` if it is not
1069
+ iterable.
1070
+
1071
+ ``itersafe(obj)`` is equivalent to ``iter(obj)`` with the exception that,
1072
+ if `obj` is not iterable, it returns ``None`` instead of raising an
1073
+ exception.
1074
+
1075
+ Parameters
1076
+ ----------
1077
+ obj : object
1078
+ The object to be iterated.
1079
+
1080
+ Returns
1081
+ -------
1082
+ iterator or None
1083
+ If `obj` is iterable, returns ``iter(obj)``; otherwise, returns
1084
+ ``None``.
1085
+
1086
+ See Also
1087
+ --------
1088
+ can_iter, is_aiterable
1089
+ """
1090
+ try:
1091
+ return iter(obj)
1092
+ except TypeError:
1093
+ return None
1094
+ @docwrap('immlib.can_iter')
1095
+ def can_iter(obj):
1096
+ """Returns ``True`` if `obj` is safe to iterate and ``False`` otherwise.
1097
+
1098
+ ``can_iter(obj)`` is equivalent to ``itersafe(obj) is not None``. This
1099
+ differs from ``is_aiterable(obj)`` in that ``is_aiterable`` only checks
1100
+ whether `obj` is an instance of ``Iterable``; ``itersafe`` tries to run
1101
+ ``iter(obj)`` and returns ``None`` when a ``TypeError`` is raised.
1102
+
1103
+ See Also
1104
+ --------
1105
+ itersafe, is_aiterable
1106
+ """
1107
+ return itersafe(obj) is not None
1108
+ @docwrap('immlib.is_pcoll')
1109
+ def is_pcoll(obj):
1110
+ """Detects if an object is a ``plist``, ``pset``, ``pdict``, ``llist`` or
1111
+ ``ldict``.
1112
+
1113
+ ``is_pcoll(obj)`` returns ``True`` if the given object `obj` is an instance
1114
+ of the persistent collection types ``plist``, ``pset``, ``pdict``,
1115
+ ``llist``, or ``ldict``. Otherwise, ``False`` is returned.
1116
+
1117
+ Note that this function tests against a specific set of concrete types;
1118
+ instances of objects whose types are subclasses of these types will be
1119
+ treated as instances of the base types; however other immutable types not
1120
+ defined in ``immlib`` or ``pcollections`` will not be recognized by this
1121
+ function.
1122
+
1123
+ Parameters
1124
+ ----------
1125
+ obj : object
1126
+ The object whose quality as a persistent collection is to be assessed.
1127
+
1128
+ Returns
1129
+ -------
1130
+ bool
1131
+ ``True`` if `obj` is a persistent collection and ``False`` otherwise.
1132
+ """
1133
+ return isinstance(obj, is_pcoll.types)
1134
+ is_pcoll.types = (plist, pset, pdict, llist, ldict)
1135
+ @docwrap('immlib.is_tcoll')
1136
+ def is_tcoll(obj):
1137
+ """Returns ``True`` if an object is a transient ``tlist``, ``tset``, or
1138
+ ``tdict``.
1139
+
1140
+ ``is_tcoll(obj)`` returns ``True`` if the given object `obj` is an instance
1141
+ of the ``tdict``, ``tset``, or ``tlist`` types, all of which are transient
1142
+ collections. Otherwise, ``False`` is returned.
1143
+
1144
+ Parameters
1145
+ ----------
1146
+ obj : object
1147
+ The object whose quality as a transient collection is to be assessed.
1148
+
1149
+ Returns
1150
+ -------
1151
+ bool
1152
+ ``True`` if `obj` is a ``tlist``, ``tset``, or ``tdict`` and ``False``
1153
+ otherwise.
1154
+ """
1155
+ return isinstance(obj, is_tcoll.types)
1156
+ is_tcoll.types = (tlist, tset, tdict)
1157
+ @docwrap('immlib.is_mcoll')
1158
+ def is_mcoll(obj):
1159
+ """Returns ``True`` if an object is a mutable ``list``, ``set``, or
1160
+ ``dict``.
1161
+
1162
+ ``is_mcoll(obj)`` returns ``True`` if the given object `obj` is an instance
1163
+ of the ``dict``, ``set``, or ``list`` types, all of which are mutable
1164
+ collections. Otherwise, ``False`` is returned.
1165
+
1166
+ Parameters
1167
+ ----------
1168
+ obj : object
1169
+ The object whose quality as a mutable collection is to be assessed.
1170
+
1171
+ Returns
1172
+ -------
1173
+ bool
1174
+ ``True`` if `obj` is a ``list``, ``set``, or ``dict`` and ``False``
1175
+ otherwise.
1176
+ """
1177
+ return isinstance(obj, is_mcoll.types)
1178
+ is_mcoll.types = (list, set, dict)
1179
+ @docwrap('immlib.to_pcoll')
1180
+ def to_pcoll(obj):
1181
+ """Returns a persistent copy of `obj`.
1182
+
1183
+ ``to_pcoll(obj)`` returns `obj` itself if `obj` is a persistent collection;
1184
+ otherwise, it returns a persistent copy of `obj`. If `obj` is not a
1185
+ collection that can be converted into a persistent collection, then an
1186
+ error is raised.
1187
+
1188
+ Parameters
1189
+ ----------
1190
+ obj : collection
1191
+ An object that is to be converted into a persistent collection.
1192
+
1193
+ Returns
1194
+ -------
1195
+ object
1196
+ A persistent version of `obj`.
1197
+
1198
+ Raises
1199
+ ------
1200
+ TypeError
1201
+ If `obj` cannot be converted into a persistent collection.
1202
+ """
1203
+ if is_pcoll(obj):
1204
+ return obj
1205
+ if isinstance(obj, Sequence):
1206
+ return plist(obj)
1207
+ elif isinstance(obj, Set):
1208
+ return pset(obj)
1209
+ elif isinstance(obj, Mapping):
1210
+ return pdict(obj)
1211
+ else:
1212
+ raise TypeError(f"argument is not a recognized collection")
1213
+ from pcollections.abc import (
1214
+ TransientSequence, TransientSet, TransientMapping)
1215
+ @docwrap('immlib.to_tcoll')
1216
+ def to_tcoll(obj, /, copy=True):
1217
+ """Returns a transient copy of `obj`.
1218
+
1219
+ ``to_tcoll(obj)`` returns a copy of `obj` as a transient collection. If
1220
+ `obj` is not a collection that can be converted into a transient
1221
+ collection, then an error is raised.
1222
+
1223
+ .. Note:: If `obj` is already a transient collection, then a copy of `obj`
1224
+ is returned.
1225
+
1226
+ .. Note:: If ``to_tcoll`` is given a lazy dict (``pcollections.ldict``) or
1227
+ a lazi list (``pcollections.llist``), the resulting transient
1228
+ dictionary is made using ``obj.transient()`` and so respects the
1229
+ laziness of the elements.
1230
+
1231
+ Parameters
1232
+ ----------
1233
+ obj : collection
1234
+ An object that is to be converted into a persistent collection.
1235
+ copy : boolean, optional
1236
+ If `obj` is already a transient collection, then a copy is made if and
1237
+ only if ``copy`` is ``True``; otherwise, `obj` is returned as-is when
1238
+ it is already a transient type. The default is ``True``.
1239
+
1240
+ Returns
1241
+ -------
1242
+ object
1243
+ A transient version of `obj`. The returned value is always either a
1244
+ ``tlist``, ``tset``, or ``tdict`` object.
1245
+
1246
+ Raises
1247
+ ------
1248
+ TypeError
1249
+ If `obj` cannot be converted into a transient collection.
1250
+ """
1251
+ if not copy and is_tcoll(obj):
1252
+ return obj
1253
+ if isinstance(obj, (PersistentSequence, PersistentSet, PersistentMapping)):
1254
+ return obj.transient()
1255
+ elif isinstance(obj, (TransientSequence, TransientSet, TransientMapping)):
1256
+ # This makes a duplicate but prevents tldicts from becoming tdicts and
1257
+ # tllists from becoming tlists.
1258
+ return obj.persistent().transient()
1259
+ elif isinstance(obj, Sequence):
1260
+ return tlist(obj)
1261
+ elif isinstance(obj, Set):
1262
+ return tset(obj)
1263
+ elif isinstance(obj, Mapping):
1264
+ return tdict(obj)
1265
+ else:
1266
+ raise TypeError(f"argument is not a recognized collection")
1267
+ @docwrap('immlib.to_mcoll')
1268
+ def to_mcoll(obj, /, copy=True):
1269
+ """Returns a mutable copy of `obj`.
1270
+
1271
+ ``to_mcoll(obj)`` returns a mutable copy of the given collection `obj`. If
1272
+ `obj` is already a mutable collection, then a duplicate is returned. If
1273
+ `obj` is not a collection that can be converted into a mutable collection,
1274
+ then an error is raised.
1275
+
1276
+ A mutable collection, according to this function, is a Python ``list``,
1277
+ ``set``, or ``dict``, depending on the type of `obj`. When `obj` is a
1278
+ ``Sequence``, the result is a ``list``; when `obj` is a ``Set``, the result
1279
+ is a ``set``; and when `obj` is a ``Mapping``, the result is a ``dict``.
1280
+
1281
+ Parameters
1282
+ ----------
1283
+ obj : collection
1284
+ An object that is to be converted into a persistent collection.
1285
+ copy : boolean, optional
1286
+ If `obj` is already a mutable collection, then a copy is made if and
1287
+ only if ``copy`` is ``True``; otherwise, `obj` is returned as-is when
1288
+ it is already a mutable type. The default is ``True``.
1289
+
1290
+ Returns
1291
+ -------
1292
+ object
1293
+ A mutable version of `obj`; the return value's type will always be one
1294
+ of ``list``, ``set``, or ``dict``.
1295
+
1296
+ Raises
1297
+ ------
1298
+ TypeError
1299
+ If `obj` cannot be converted into a mutable collection.
1300
+ """
1301
+ if not copy and is_mcoll(obj):
1302
+ return obj
1303
+ if isinstance(obj, Sequence):
1304
+ return list(obj)
1305
+ elif isinstance(obj, Set):
1306
+ return set(obj)
1307
+ elif isinstance(obj, Mapping):
1308
+ return dict(obj)
1309
+ else:
1310
+ raise TypeError(f"argument is not a collection")
1311
+ @docwrap('immlib.freezearray')
1312
+ def freezearray(arr):
1313
+ """Freezes a NumPy array or SciPy sparse array in-place.
1314
+
1315
+ ``freezearray(x)`` sets the ``'WRITEABLE'`` bit on the numpy array ``x`` or
1316
+ on ``x.data`` if ``x`` is a SciPy sparse array. If ``x`` is neither a NumPy
1317
+ array nor a SciPy sparse array, then a ``TypeError`` is raised. No value is
1318
+ returned.
1319
+
1320
+ ``freezearray(q)`` is equivalent to ``freezearray(q.m)`` if ``q`` is a
1321
+ ``pint.Quantity`` object.
1322
+
1323
+ .. Warning:: This function mutates its argument in-place.
1324
+
1325
+ See Also
1326
+ --------
1327
+ frozenarray
1328
+ """
1329
+ if isinstance(arr, pint.Quantity):
1330
+ arr = arr.m
1331
+ if isinstance(arr, np.ndarray):
1332
+ arr.setflags(write=False)
1333
+ elif sps.issparse(arr):
1334
+ arr.data.setflags(write=False)
1335
+ else:
1336
+ raise TypeError(
1337
+ f"freezearray requires a numpy array or scipy sparse array,"
1338
+ f" but type {type(arr)} was given")
1339
+ @docwrap('immlib.frozenarray')
1340
+ def frozenarray(obj, /, dtype=None, *, copy=False, **kwargs):
1341
+ """Roughly equivalent to ``numpy.array`` but returns read-only arrays.
1342
+
1343
+ ``frozenarray(obj)`` is equivalent to ``numpy.array(obj)`` with a small
1344
+ number of exceptions:
1345
+
1346
+ - Primarily, the returned object is always a frozen array (i.e., an array
1347
+ with the ``'WRITEABLE'`` flag set to ``False``).
1348
+ - The default value of the ``copy`` option is ``False``, meaning that a
1349
+ copy of the array will only be made if required by the other parameters
1350
+ or if the array is not already read-only. If you wish to make an array
1351
+ read-only rather than obtaining a read-only copy of it, use the
1352
+ ``freezearray()`` function.
1353
+ - SciPy sparse arrays are also handled by setting the write flag on the
1354
+ ``obj.data`` member.
1355
+ - If ``obj`` is a ``pint.Quantity`` object, then an equivalent quantity
1356
+ with the magnitude made read-only is returned.
1357
+
1358
+ If a PyTorch tensor is passed to ``frozenarray``, it will be converted into
1359
+ a frozen NumPy array; PyTorch tensors themselves cannot be frozen, however.
1360
+
1361
+ See Also
1362
+ --------
1363
+ numpy.array : Create an array that is not frozen.
1364
+ freezearray : Convert an argument to a frozen array in-place.
1365
+ """
1366
+ if isinstance(obj, pint.Quantity):
1367
+ arr = frozenarray(obj.m, dtype=dtype, copy=copy, **kwargs)
1368
+ return obj if not copy and arr is obj.m else type(obj)(arr, obj.u)
1369
+ elif sps.issparse(obj):
1370
+ arr = frozenarray(obj.data, dtype=dtype, copy=copy, **kwargs)
1371
+ if not copy and obj.data is arr:
1372
+ return obj
1373
+ obj = obj.copy()
1374
+ obj.data = arr
1375
+ return obj
1376
+ elif isinstance(obj, np.ndarray):
1377
+ if obj.flags['WRITEABLE']:
1378
+ copy = True
1379
+ arr = np.array(obj, dtype=dtype, copy=copy, **kwargs)
1380
+ arr.setflags(write=False)
1381
+ return arr
1382
+ else:
1383
+ # PyTorch tensors might be sparse, so have to be treated specially.
1384
+ from ._numeric import torch, to_array
1385
+ if torch.is_tensor(obj):
1386
+ return frozenarray(to_array(obj), dtype=dtype, **kwargs)
1387
+ arr = np.array(obj, dtype=dtype, **kwargs)
1388
+ arr.setflags(write=False)
1389
+ return arr
1390
+
1391
+
1392
+ # Mapping/Sequence Utilities ##################################################
1393
+
1394
+ @docwrap('immlib.get')
1395
+ def get(d, k, /, *args, **kwargs):
1396
+ """Returns a value from either a mapping or a sequence.
1397
+
1398
+ The ``get`` function is essentially a function version of the ``get``
1399
+ method that works for both ``Mapping`` and ``Sequence`` types (e.g.,
1400
+ ``dict``, ``list``, ``tuple``, and related types that implement their
1401
+ abstract bases).
1402
+
1403
+ ``get(d, k)`` extracts element `k` from object `d` and returns it. If `k`
1404
+ is not a valid index of `d` (i.e., `k` is not a key of `d`, if `d` is a
1405
+ mapping, or is not an integer index of `d` if `d` is a sequence), then the
1406
+ optional value ``default`` is returned. If ``detault`` is not explicitly
1407
+ provided, then an error is raised. Note that if a non-integer key is
1408
+ provided for a sequence, this is treated as a missing index.
1409
+
1410
+ .. Note:: The default value may be expressed as either a third positional
1411
+ argument or a named argument (``default``).
1412
+
1413
+ Parameters
1414
+ ----------
1415
+ d : object
1416
+ The dict-like or list-like object from which an element is being
1417
+ extracted.
1418
+ k : object
1419
+ The key or index into `d` whose value is to be extracted.
1420
+ args
1421
+ The default value to be returned if an item is not found. The default
1422
+ value may be specified as a third positional argument.
1423
+ kwargs
1424
+ The default value to be returned if an item is not found. The default
1425
+ value may be specified as a named argument with the name ``"default"``.
1426
+
1427
+ Returns
1428
+ -------
1429
+ object
1430
+ The object ``d[k]``, if the key `k` is found in the collection `d`.
1431
+ Otherwise, ``default`` is returned.
1432
+
1433
+ Raises
1434
+ ------
1435
+ KeyError
1436
+ If the key or index `k` is not found in the collection `d` and no
1437
+ ``default`` option is given.
1438
+ """
1439
+ nargs = len(args)
1440
+ nkw = len(kwargs)
1441
+ if nargs + nkw > 1:
1442
+ raise TypeError(f"get takes 2 or 3 arguments; got {2 + nargs + nkw}")
1443
+ if nargs == 1:
1444
+ error = False
1445
+ default = args[0]
1446
+ elif nkw == 1:
1447
+ if "default" in kwargs:
1448
+ error = False
1449
+ default = kwargs.pop("default")
1450
+ else:
1451
+ k = repr(next(iter(kwargs.keys())))
1452
+ raise TypeError(f"get() got an unexpected keyword argument {k}")
1453
+ else:
1454
+ error = True
1455
+ default = None
1456
+ if isinstance(d, (Mapping, Sequence)):
1457
+ try:
1458
+ return d[k]
1459
+ except IndexError:
1460
+ pass
1461
+ except TypeError:
1462
+ pass
1463
+ except KeyError:
1464
+ pass
1465
+ else:
1466
+ raise TypeError(f"cannot get item from type {type(d)}")
1467
+ # If we reach this point, we failed to find the key.
1468
+ if error:
1469
+ raise KeyError(k)
1470
+ else:
1471
+ return default
1472
+ @docwrap('immlib.nestget')
1473
+ def nestget(d, /, *args, **kwargs):
1474
+ """Returns a value from a data structure of nested mappings and sequences.
1475
+
1476
+ The ``nestget`` function is essentially a nested version of the ``get``
1477
+ function that works for both ``Mapping`` and ``Sequence`` types (e.g.,
1478
+ ``dict``, ``list``, ``tuple``, and related types that implement their
1479
+ abstract bases).
1480
+
1481
+ ``nestget(data, k1, k2, k3...)`` extracts element ``k1`` from ``data`` then
1482
+ element ``k2`` from that value, then element ``k3`` from that value, etc.,
1483
+ until there are no more keys; the final value is returned. If any of the
1484
+ values are missing, then the optional value ``default`` is returned if it
1485
+ is provided and an error is raised if it is not. Note that the provided
1486
+ keys may be integer indices for list-like objects that may be included in
1487
+ the nesting. If a string key is given for a list-like container, then this
1488
+ is treated as a missing key, not an error.
1489
+
1490
+ This function raises a ``KeyError` when a key or index is not found in the
1491
+ relevant container, but this behavior can be changed by passing the
1492
+ optional named parameter ``default``. If ``default`` is provided, then this
1493
+ value is returned if any keys are missing.
1494
+
1495
+ Parameters
1496
+ ----------
1497
+ d : object
1498
+ The dict-like or list-like object from which an element is being
1499
+ extracted.
1500
+ args
1501
+ The list of keys and indices to be extracted.
1502
+ kwargs
1503
+ The default value to be returned if an item is not found can be
1504
+ specified using the named option ``default``. If this option is not
1505
+ provided, then an error is raised should the item not be found.
1506
+
1507
+ Returns
1508
+ -------
1509
+ object
1510
+ The object found at the given nested position in the data structure
1511
+ `d`. If one of the provided keys does not exist in the associated
1512
+ sub-collection of `d`, then the ``default`` option is returned.
1513
+
1514
+ Raises
1515
+ ------
1516
+ KeyError
1517
+ If the given sequence of keys cannot be found in the nested data
1518
+ structure and no ``default`` option is provided.
1519
+ """
1520
+ if "default" in kwargs:
1521
+ error = False
1522
+ default = kwargs.pop("default")
1523
+ else:
1524
+ error = True
1525
+ default = None
1526
+ if len(kwargs) > 0:
1527
+ k = repr(next(iter(kwargs.keys())))
1528
+ raise TypeError(f"nestget() got an unexpected keyword argument {k}")
1529
+ for k in args:
1530
+ if isinstance(d, Mapping):
1531
+ if k in d:
1532
+ d = d[k]
1533
+ continue
1534
+ elif isinstance(d, Sequence):
1535
+ try:
1536
+ d = d[k]
1537
+ continue
1538
+ except IndexError:
1539
+ pass
1540
+ except TypeError:
1541
+ pass
1542
+ else:
1543
+ raise TypeError(f"cannot get item from type {type(d)}")
1544
+ # If we reach this point, we failed to find the key.
1545
+ if error:
1546
+ raise KeyError(k)
1547
+ else:
1548
+ return default
1549
+ return d
1550
+ from pcollections import lazy
1551
+ def _lazyvalmap_extract(f, d, k, *args, **kw):
1552
+ return f(d[k], *args, **kw)
1553
+ @docwrap('immlib.lazyvalmap')
1554
+ def lazyvalmap(f, d, /, *args, **kwargs):
1555
+ """Returns a dict object whose values are transformed by a function.
1556
+
1557
+ ``lazyvalmap(f, d)`` returns a dict whose keys are the same as those of the
1558
+ given dict object and whose values, for each key ``k`` are ``f(d[k])``. All
1559
+ values are created lazily.
1560
+
1561
+ ``lazyvalmap(f, d, *args, **kw)`` additionally passes the given arguments
1562
+ to the function `f`, such that in the resulting map, each key ``k`` is
1563
+ mapped to ``f(d[k], *args, **kw)``.
1564
+
1565
+ Parameters
1566
+ ----------
1567
+ f : function
1568
+ The function used to create the values in the new dictionary.
1569
+ d : collections.abc.Mapping
1570
+ A mapping whose keys are to be preserved and remapped to a function of
1571
+ their values.
1572
+ args
1573
+ Additional positional arguments to pass to `f`.
1574
+ kwargs
1575
+ Additional named arguments to pass to `f`.
1576
+
1577
+ Returns
1578
+ -------
1579
+ pcollections.ldict
1580
+ This function always returns a lazy dictionary object of type
1581
+ ``pcollections.ldict``.
1582
+ """
1583
+ t = tldict()
1584
+ if is_ammap(d):
1585
+ # For mutable maps, we do not try to respect laziness; they may change
1586
+ # so we cannot rely on them.
1587
+ for (k,v) in d.items():
1588
+ t[k] = lazy(f, v, *args, **kwargs)
1589
+ else:
1590
+ # Anything else we assume is immutable, so we respect any possible lazy
1591
+ # implementation.
1592
+ for k in d.keys():
1593
+ t[k] = lazy(_lazyvalmap_extract, f, d, k, *args, **kwargs)
1594
+ return t.persistent()
1595
+ @docwrap('immlib.valmap')
1596
+ def valmap(f, d, /, *args, **kwargs):
1597
+ """Returns a dictionary object whose values are transformed by a function.
1598
+
1599
+ ``valmap(f, d)`` returns a dict whose keys are the same as those of the
1600
+ given dict object and whose values, for each key ``k`` are ``f(d[k])``.
1601
+
1602
+ ``valmap(f, d, *args, **kw)`` additionally passes the given arguments to
1603
+ the function `f`, such that in the resulting map, each key ``k`` is
1604
+ mapped to ``f(d[k], *args, **kw)``.
1605
+
1606
+ Unlike ``lazyvalmap``, this function returns either a ``dict``, a
1607
+ ``pdict``, or an ``ldict`` depending on the input argument `d`. If `d` is a
1608
+ (lazy) ``ldict``, then an ``ldict`` is returned; if `d` is a ``pdict``, a
1609
+ ``pdict`` is returned, and otherwise, a ``dict`` is returnd.
1610
+
1611
+ Parameters
1612
+ ----------
1613
+ f : function
1614
+ The function used to create the values in the new dictionary.
1615
+ d : collections.abc.Mapping
1616
+ A mapping whose keys are to be preserved and remapped to a function of
1617
+ their values.
1618
+ args
1619
+ Additional positional arguments to pass to `f`.
1620
+ kwargs
1621
+ Additional named arguments to pass to `f`.
1622
+
1623
+ Returns
1624
+ -------
1625
+ collections.abc.Mapping object
1626
+ This function always returns a dictionary object whose type is either
1627
+ ``pcollections.pdict``, ``pcollections.ldict``, or ``dict``, depending
1628
+ on the type of `d`.
1629
+ """
1630
+ if is_ldict(d):
1631
+ return lazyvalmap(f, d, *args, **kwargs)
1632
+ elif is_pdict(d):
1633
+ t = tdict()
1634
+ for (k,v) in d.items():
1635
+ t[k] = f(v, *args, **kwargs)
1636
+ return t.persistent()
1637
+ else:
1638
+ return {k: f(v, *args, **kwargs) for (k,v) in d.items()}
1639
+ @docwrap('immlib.lazykeymap')
1640
+ def lazykeymap(f, d, /, *args, **kwargs):
1641
+ """Returns a object of type ``pcollections.ldict`` whose values are a
1642
+ function of the keys of the mapping `d`.
1643
+
1644
+ ``keymap(f, d)`` returns a dict whose keys are the same as those of the
1645
+ given dict object and whose values, for each key ``k`` are ``f(k)``. If `d`
1646
+ is a sequence or iterable, then it is treated as a sequence of keys.
1647
+
1648
+ ``keymap(f, d, *args, **kw)`` additionally passes the given arguments to
1649
+ the function `f`, such that in the resulting map, each key ``k`` is
1650
+ mapped to ``f(k, *args, **kw)``.
1651
+
1652
+ Parameters
1653
+ ----------
1654
+ f : function
1655
+ The function used to create the values in the new dictionary.
1656
+ d : collections.abc.Mapping or iterable
1657
+ A mapping whose keys are to be preserved and remapped to a function of
1658
+ themselves. Alternatively, this may be an iterable of the keys instead
1659
+ of a dict with matching keys.
1660
+ args
1661
+ Additional positional arguments to pass to `f`.
1662
+ kwargs
1663
+ Additional named arguments to pass to `f`.
1664
+
1665
+ Returns
1666
+ -------
1667
+ pcollections.ldict
1668
+ This function always returns an object of type ``ldict`` whose values
1669
+ are lazy.
1670
+ """
1671
+ if is_amap(d):
1672
+ keys = d.keys()
1673
+ else:
1674
+ keys = d
1675
+ t = tldict()
1676
+ for k in keys:
1677
+ t[k] = lazy(f, k, *args, **kwargs)
1678
+ return t.persistent()
1679
+ @docwrap('immlib.keymap')
1680
+ def keymap(f, d, /, *args, **kwargs):
1681
+ """Returns a dict object whose values are a function of a dict's keys.
1682
+
1683
+ ``keymap(f, d)`` returns a dict whose keys are the same as those of the
1684
+ given dict object and whose values, for each key ``k`` are ``f(k)``.
1685
+
1686
+ ``keymap(f, d, *args, **kw)`` additionally passes the given arguments to
1687
+ the function `f`, such that in the resulting map, each key ``k`` is mapped
1688
+ to ``f(k, *args, **kw)``.
1689
+
1690
+ This function returns either a ``dict`` or a ``pdict``. If ``d`` is a
1691
+ ``pdict``, a ``pdict`` is returned, and otherwise, a ``dict`` is
1692
+ returnd. Unlike the ``valmap`` function, an ``ldict`` is never returned
1693
+ because the lazy values of such a dictionary are not accessed by
1694
+ ``keymap``; if a lazy dictionary is required, then the function
1695
+ ``lazykeymap`` should be used instead.
1696
+
1697
+ Parameters
1698
+ ----------
1699
+ f : function
1700
+ The function used to create the values in the new dictionary.
1701
+ d : collections.abc.Mapping or iterable
1702
+ A mapping whose keys are to be preserved and remapped to a function of
1703
+ themselves. Alternatively, this may be an iterable of the keys instead
1704
+ of a dict with matching keys.
1705
+ args
1706
+ Additional positional arguments to pass to `f`.
1707
+ kwargs
1708
+ Additional named arguments to pass to `f`.
1709
+
1710
+ Returns
1711
+ -------
1712
+ collections.abc.Mapping object
1713
+ This function always returns a dictionary object whose type is either
1714
+ ``pcollections.pdict`` or ``dict``, depending on the type of `d`.
1715
+ """
1716
+ if is_pdict(d):
1717
+ t = tdict()
1718
+ for k in d.keys():
1719
+ t[k] = f(k, *args, **kwargs)
1720
+ return t.persistent()
1721
+ elif is_amap(d):
1722
+ keys = d.keys()
1723
+ else:
1724
+ keys = d
1725
+ return {k: f(k, *args, **kwargs) for k in keys}
1726
+ def _lazyitemmap_extract(f, d, k, *args, **kw):
1727
+ return f(k, d[k], *args, **kw)
1728
+ @docwrap('immlib.lazyitemmap')
1729
+ def lazyitemmap(f, d, /, *args, **kwargs):
1730
+ """Returns an ``ldict`` object whose values are a function of a dict's
1731
+ items.
1732
+
1733
+ ``lazyitemmap(f, d)`` yields an ``ldict`` whose keys are the same as those
1734
+ of the given dict object and whose values, for each key ``k``, are lazily
1735
+ computed as ``f(k, d[k])``.
1736
+
1737
+ ``itemmap(f, d, *args, **kw)`` additionally passes the given arguments to
1738
+ the function `f`, such that in the resulting map, each key ``k`` is
1739
+ mapped to ``f(k, d[k], *args, **kw)``.
1740
+
1741
+ Parameters
1742
+ ----------
1743
+ f : function
1744
+ The function used to create the values in the new dictionary; it must
1745
+ accept two arguments (``f(k, d[k])``) plus any additional arguments
1746
+ provided in ``*args`` and ``**kwargs``.
1747
+ d : collections.abc.Mapping
1748
+ A mapping whose items are to be preserved and remapped to a function
1749
+ of their keys and values.
1750
+ args
1751
+ Additional positional arguments to pass to `f`.
1752
+ kwargs
1753
+ Additional named arguments to pass to `f`.
1754
+
1755
+ Returns
1756
+ -------
1757
+ pcollections.ldict
1758
+ This function always returns a lazy dictionary object of type
1759
+ ``pcollections.ldict``.
1760
+ """
1761
+ t = tldict()
1762
+ if is_ammap(d):
1763
+ # For mutable maps, we do not try to respect laziness; they may change
1764
+ # so we cannot rely on them.
1765
+ for (k,v) in d.items():
1766
+ t[k] = lazy(f, k, v, *args, **kwargs)
1767
+ else:
1768
+ # Otherwise, we assume that it's an immutable map, and we respect any
1769
+ # possible laziness that could be implemented.
1770
+ for k in d.keys():
1771
+ t[k] = lazy(_lazyitemmap_extract, f, d, k, *args, **kwargs)
1772
+ return t.persistent()
1773
+ @docwrap('immlib.itemmap')
1774
+ def itemmap(f, d, /, *args, **kwargs):
1775
+ """Returns a dictionary object whose values are a function of a given
1776
+ dictionary's items.
1777
+
1778
+ ``itemmap(f, d)`` returns a dict whose keys are the same as those of the
1779
+ given mapping object `d` and whose values, for each key ``k`` are ``f(k,
1780
+ d[k])``.
1781
+
1782
+ ``itemmap(f, d, *args, **kw)`` additionally passes the given arguments to
1783
+ the function `f`, such that in the resulting map, each key ``k`` is
1784
+ mapped to ``f(k, d[k], *args, **kw)``.
1785
+
1786
+ Unlike ``lazyitemmap``, this function returns either a ``dict``, a
1787
+ ``pdict``, or an ``ldict`` depending on the input argument `d`. If `d`
1788
+ is an ``ldict``, then an ``ldict`` is returned; if `d` is a ``pdict``, a
1789
+ ``pdict`` is returned, and otherwise, a ``dict`` is returnd.
1790
+
1791
+ Parameters
1792
+ ----------
1793
+ f : function
1794
+ The function used to create the values in the new dictionary; it must
1795
+ accept two arguments (``f(k, d[k])``) plus any additional arguments
1796
+ provided in ``*args`` and ``**kwargs``.
1797
+ d : collections.abc.Mapping
1798
+ A mapping whose keys are to be preserved and remapped to a function of
1799
+ their values.
1800
+ args
1801
+ Additional positional arguments to pass to `f`.
1802
+ kwargs
1803
+ Additional named arguments to pass to `f`.
1804
+
1805
+ Returns
1806
+ -------
1807
+ pcollections.pdict or pcollections.ldict or dict
1808
+ This function always returns a dictionary object whose type is either
1809
+ ``pcollections.pdict``, ``pcollections.ldict``, or ``dict``, depending
1810
+ on the type of `d`.
1811
+ """
1812
+ if is_ldict(d):
1813
+ return lazyitemmap(f, d, *args, **kwargs)
1814
+ elif is_pdict(d):
1815
+ t = tdict()
1816
+ for (k,v) in d.items():
1817
+ t[k] = f(k, v, *args, **kwargs)
1818
+ return t.persistent()
1819
+ else:
1820
+ return {k: f(k, v, *args, **kwargs) for (k,v) in d.items()}
1821
+ @docwrap('immlib.dictmap')
1822
+ def dictmap(f, keys, /, *args, **kw):
1823
+ """Returns a dict with the given keys and the values ``map(f, keys)``.
1824
+
1825
+ ``dictmap(f, keys)`` returns a dict object whose keys are the elements of
1826
+ ``iter(keys)`` and whose values are the elements of ``map(f, keys)``.
1827
+
1828
+ ``dictmap(f, keys, *args, **kw)`` returns a dict object whose keys are the
1829
+ elements of ``iter(keys)`` and whose values are the elements of
1830
+ ``[f(k, *args, **kw) for k in iter(keys)]``.
1831
+
1832
+ Parameters
1833
+ ----------
1834
+ f : function
1835
+ The function used to create the values in the new dictionary; it must
1836
+ accept one arguments (``f(k)``) plus any additional arguments provided
1837
+ in ``*args`` and ``**kwargs``.
1838
+ keys : iterable
1839
+ An iterable object whose values are to become the keys of the new
1840
+ dictionary.
1841
+ args
1842
+ Additional positional arguments to pass to `f`.
1843
+ kwargs
1844
+ Additional named arguments to pass to `f`.
1845
+
1846
+ Returns
1847
+ -------
1848
+ dict
1849
+ A dictionary of the given `keys` with each key ``k`` mapped to
1850
+ ``f(k)``.
1851
+ """
1852
+ return {k: f(k, *args, **kw) for k in keys}
1853
+ @docwrap('immlib.pdictmap')
1854
+ def pdictmap(f, keys, /, *args, **kw):
1855
+ """Returns a ``pdict`` with the given keys and the values ``map(f, keys)``.
1856
+
1857
+ ``pdictmap(f, keys)`` returns a ``pdict`` object whose keys are the
1858
+ elements of ``iter(keys)`` and whose values are the elements of
1859
+ ``map(f, keys)``.
1860
+
1861
+ ``pdictmap(f, keys, *args, **kw)`` returns a dict object whose keys are
1862
+ the elements of ``iter(keys)`` and whose values are the elements of
1863
+ ``[f(k, *args, **kw) for k in iter(keys)]``.
1864
+
1865
+ Parameters
1866
+ ----------
1867
+ f : function
1868
+ The function used to create the values in the new dictionary; it must
1869
+ accept one arguments (``f(k)``) plus any additional arguments provided
1870
+ in ``*args`` and ``**kwargs``.
1871
+ keys : iterable
1872
+ An iterable object whose values are to become the keys of the new
1873
+ dictionary.
1874
+ args
1875
+ Additional positional arguments to pass to `f`.
1876
+ kwargs
1877
+ Additional named arguments to pass to `f`.
1878
+
1879
+ Returns
1880
+ -------
1881
+ pcollections.pdict
1882
+ A persistent dictionary of the given `keys` with each key ``k`` mapped
1883
+ to ``f(k)``.
1884
+ """
1885
+ t = tdict()
1886
+ for k in keys:
1887
+ t[k] = f(k, *args, **kw)
1888
+ return t.persistent()
1889
+ @docwrap('immlib.ldictmap')
1890
+ def ldictmap(f, keys, *args, **kw):
1891
+ """Returns a lazy dictionary with the given keys and the values
1892
+ ``map(f, keys)``.
1893
+
1894
+ ``lazydictmap(f, keys)`` returns a ``pcollections.ldict`` object whose keys
1895
+ are the elements of ``iter(keys)`` and whose values are the elements of
1896
+ ``map(f, keys)``. All values are lazy.
1897
+
1898
+ ``lazydictmap(f, keys, *args, **kw)`` returns a `pcollections.ldict` object
1899
+ whose keys are the elements of ``iter(keys)`` and whose values are the
1900
+ elements of ``[f(k, *args, **kw) for k in iter(keys)]``, lazily calculated.
1901
+
1902
+ Parameters
1903
+ ----------
1904
+ f : function
1905
+ The function used to create the values in the new dictionary; it must
1906
+ accept one arguments (``f(k)``) plus any additional arguments provided
1907
+ in ``*args`` and ``**kwargs``.
1908
+ keys : iterable
1909
+ An iterable object whose values are to become the keys of the new
1910
+ dictionary.
1911
+ args
1912
+ Additional positional arguments to pass to `f`.
1913
+ kwargs
1914
+ Additional named arguments to pass to `f`.
1915
+
1916
+ Returns
1917
+ -------
1918
+ pcollections.ldict
1919
+ A persistent lazy dictionary of the given `keys` with each key ``k``
1920
+ mapped to ``f(k)``.
1921
+ """
1922
+ t = tldict()
1923
+ for k in keys:
1924
+ t[k] = lazy(f, k, *args, **kw)
1925
+ return t.persistent()
1926
+ @docwrap('immlib.merge')
1927
+ def merge(*args, **kwargs):
1928
+ '''Merges dict-like objects left-to-right. See also ``rmerge``.
1929
+
1930
+ ``merge(...)`` collapses all arguments, which must be ``Mapping`` objects
1931
+ of some kind (``dict``, ``pdict``, ``ldict``, or a similar type), into a
1932
+ single mapping from left-to-right (i.e., with values in dictionaries to the
1933
+ right in the argument list overwriting values to the left in the argument
1934
+ list). The mapping that is returned depends on the inputs: if any of the
1935
+ input mappings are ``ldict`` objects, then an ``ldict`` is returned (and
1936
+ the laziness of arguments is respected); otherwise, a ``pdict`` object is
1937
+ retuend.
1938
+
1939
+ Named arguments may be passed after the dictionaries; these are
1940
+ collectively considered equivalent to one additional dictionary argument to
1941
+ the right of the positional mapping arguments.
1942
+
1943
+ Parameters
1944
+ ----------
1945
+ args
1946
+ A sequence of ``collections.abc.Mapping`` objects such as ``dict``
1947
+ objects.
1948
+ kwargs
1949
+ Additional key-value pairs that are merged into the result last.
1950
+
1951
+ Returns
1952
+ -------
1953
+ pcollections.pdict or pcollections.ldict
1954
+ A dictionary that represents the merger of all given dictionaries and
1955
+ key-value pairs. If any of the arguments are lazy dictionaries
1956
+ (``pcollections.ldict``) then the return value is also lazy in order to
1957
+ respect the laziness of the arguments.
1958
+
1959
+ See Also
1960
+ --------
1961
+ rmerge : Merges dictionaries from right to left.
1962
+ '''
1963
+ if len(args) == 0:
1964
+ return pdict(kwargs)
1965
+ # Make the initial dictionary.
1966
+ res = args[0]
1967
+ lazy = is_ldict(res)
1968
+ res = tdict(holdlazy(res) if lazy else res)
1969
+ for d in args[1:]:
1970
+ if is_ldict(d):
1971
+ lazy = True
1972
+ res.update(holdlazy(d))
1973
+ else:
1974
+ res.update(d)
1975
+ res.update(kwargs)
1976
+ if not lazy:
1977
+ from pcollections import lazy as _lazy
1978
+ lazy = any(isinstance(u, _lazy) for u in kwargs.values())
1979
+ return ldict(res) if lazy else pdict(res)
1980
+ def rmerge(*args, **kwargs):
1981
+ '''Merges dictionary objects right-to-left. See also ``merge``.
1982
+
1983
+ ``rmerge(...)`` collapses all arguments, which must be python ``Mapping``
1984
+ objects of some kind, into a single mapping from right-to-left. The mapping
1985
+ that is returned depends on the inputs: if any of the input mappings are
1986
+ lazydict objects, then a lazydict is returned (and the laziness of
1987
+ arguments is respected); otherwise, a frozendict object is retuend.
1988
+
1989
+ Named arguments may be passed after the dictionaries; these are
1990
+ collectively considered equivalent to one additional dictionary argument to
1991
+ the right of the positional mapping arguments.
1992
+
1993
+ .. Note:: The ``rmerge`` function is identical to the ``merge`` function
1994
+ but with reversed arguments. In other words, ``merge(*args, **kw)`` is
1995
+ equivalent to ``rmerge(kw, **reversed(args))``.
1996
+
1997
+ Parameters
1998
+ ----------
1999
+ args
2000
+ A sequence of ``collections.abc.Mapping`` objects such as ``dict``
2001
+ objectss.
2002
+ kwargs
2003
+ Additional key-value pairs that are merged into the result first.
2004
+
2005
+ Returns
2006
+ -------
2007
+ pcollections.pdict or pcollections.ldict
2008
+ A dictionary that represents the merger of all given dictionaries and
2009
+ key-value pairs. If any of the arguments are lazy dictionaries
2010
+ (``pcollections.ldict``) then the return value is also lazy in order to
2011
+ respect the laziness of the arguments.
2012
+
2013
+ See Also
2014
+ --------
2015
+ merge : Merges dictionaries from left to right.
2016
+ '''
2017
+ from pcollections import lazy as _lazy
2018
+ if len(args) == 0:
2019
+ return pdict(kwargs)
2020
+ # Make the initial dictionary.
2021
+ res = tdict(kwargs)
2022
+ lazy = any(isinstance(v, _lazy) for v in kwargs.values())
2023
+ for d in reversed(args):
2024
+ if is_ldict(d):
2025
+ lazy = True
2026
+ res.update(holdlazy(d))
2027
+ else:
2028
+ res.update(d)
2029
+ return ldict(res) if lazy else pdict(res)
2030
+ @docwrap('immlib.assoc')
2031
+ def assoc(d, /, *args, **kwargs):
2032
+ """Returns a copy of the given dictionary with additional key-value pairs.
2033
+
2034
+ ``assoc(d, key, val)`` returns a copy of the dictionary `d` with the given
2035
+ key-value pair associated in the new copy. The return value is always the
2036
+ same type as the argument `d` but is always an updated copy. The argument
2037
+ `d` is never mutated.
2038
+
2039
+ ``assoc(d, key1, val1, key2, val2 ...)`` associates all the given keys to
2040
+ the given values in the returned copy.
2041
+
2042
+ ``assoc(d, key1=val1, key2=val2 ...)`` uses the keyword arguments as the
2043
+ arguments that are to be associated. These may be mixed with positional
2044
+ key-value pairs.
2045
+
2046
+ ``assoc(d)`` returns a copy of `d`.
2047
+
2048
+ Parameters
2049
+ ----------
2050
+ d : dict-like
2051
+ A dictionary that is to be copied and updated with the following
2052
+ arguments.
2053
+ args
2054
+ Sequential pairs of keys and values (i.e., ``len(args)`` must be even)
2055
+ that should be updated in the returned dictionary.
2056
+ kwargs
2057
+ Additional key-value pairs to be updated in the returned dictionary.
2058
+
2059
+ Returns
2060
+ -------
2061
+ dict-like
2062
+ A copy of `d` with updated keys and values.
2063
+ """
2064
+ if len(args) % 2 != 0:
2065
+ raise ValueError("assoc requires matched key-value arguments")
2066
+ ks = args[0::2]
2067
+ vs = args[1::2]
2068
+ if is_ammap(d):
2069
+ # This is a mutable mapping, so we copy it.
2070
+ d = d.copy()
2071
+ for (k,v) in zip(ks,vs):
2072
+ d[k] = v
2073
+ for (k,v) in kwargs.items():
2074
+ d[k] = v
2075
+ elif is_apmap(d):
2076
+ nels = len(ks) + len(kwargs)
2077
+ if nels > 1:
2078
+ d = d.transient()
2079
+ for (k,v) in zip(ks,vs):
2080
+ d[k] = v
2081
+ for (k,v) in kwargs.items():
2082
+ d[k] = v
2083
+ d = d.persistent()
2084
+ else:
2085
+ for (k,v) in zip(ks,vs):
2086
+ d = d.set(k, v)
2087
+ for (k,v) in kwargs.items():
2088
+ d = d.set(k, v)
2089
+ else:
2090
+ raise TypeError(f"cannot assoc to type {type(d)}")
2091
+ return d
2092
+ @docwrap('immlib.dissoc')
2093
+ def dissoc(d, /, *args):
2094
+ """Returns a copy of the given dictionary with certain keys removed.
2095
+
2096
+ ``dissoc(d, key)`` returns a copy of the dictionary `d` with the given
2097
+ ``key`` disssociated in the new copy. The return value is always the same
2098
+ type as the argument `d`.
2099
+
2100
+ ``dissoc(d, key1, key2 ...)`` dissociates all the given keys from their
2101
+ values in the returned copy.
2102
+
2103
+ ``dissoc(d)`` returns a copy of `d`.
2104
+
2105
+ Parameters
2106
+ ----------
2107
+ d : dict-like
2108
+ A dictionary that is to be copied and updated according to the
2109
+ following arguments.
2110
+ args
2111
+ Keys that should be removed from the copy of `d` that is returned.
2112
+
2113
+ Returns
2114
+ -------
2115
+ dict-like
2116
+ A copy of `d` with the given keys removed.
2117
+ """
2118
+ if is_ammap(d):
2119
+ # This is a mutable mapping, so we copy it.
2120
+ d = d.copy()
2121
+ for k in args:
2122
+ if k in d:
2123
+ del d[k]
2124
+ return d
2125
+ elif is_pdict(d):
2126
+ if len(args) == 1:
2127
+ return d.delete(args[0])
2128
+ else:
2129
+ d = d.transient()
2130
+ for k in args:
2131
+ del d[k]
2132
+ return d.persistent()
2133
+ else:
2134
+ raise TypeError(f"cannot dissoc from type {type(d)}")
2135
+ from pcollections import unlazy
2136
+ def _lambdadict_call(data, fn):
2137
+ spec = getfullargspec(fn)
2138
+ dflts = spec.defaults or pdict()
2139
+ args = []
2140
+ kwargs = {}
2141
+ pos = True
2142
+ for k in spec.args:
2143
+ if k in data:
2144
+ v = unlazy(data[k])
2145
+ if pos:
2146
+ args.append(v)
2147
+ else:
2148
+ kwargs[k] = v
2149
+ else:
2150
+ pos = False
2151
+ if k in dflts:
2152
+ kwargs[k] = dflts[k]
2153
+ for k in spec.kwonlyargs:
2154
+ if k in data:
2155
+ kwargs[k] = unlazy(data[k])
2156
+ else:
2157
+ if k in dflts:
2158
+ kwargs[k] = dflts[k]
2159
+ return fn(*args, **kwargs)
2160
+ def lambdadict(*args, **kwargs):
2161
+ """Builds and returns a ``ldict`` with lambda functions calculated lazily.
2162
+
2163
+ ``lambdadict(args...)`` is equivalent to ``merge(args...)`` except that
2164
+ always returns an object of type ``pcollections.ldict`` and that any lambda
2165
+ function in the values provided by the merged arguments is made into a lazy
2166
+ partial function whose inputs come from the lambda-function variable names
2167
+ in the same resulting ``ldict``.
2168
+
2169
+ .. Warning:: This function will gladly return an ``ldict`` that
2170
+ encapsulates an infinite loop if you are not careful. For example, the
2171
+ following lambdadict will infinitely loop when either key is requested:
2172
+ ``ld = lambdadict(a=lambda b:b, b=lambda a:a)``.
2173
+
2174
+ Examples
2175
+ --------
2176
+ >>> d = lambdadict(a=1, b=2, c=lambda a,b: a + b)
2177
+ >>> d.is_lazy('c')
2178
+ True
2179
+
2180
+ >>> d.is_ready('c')
2181
+ False
2182
+
2183
+ >>> d['c']
2184
+ 3
2185
+
2186
+ >>> d
2187
+ {|'a': 1, 'b': 2, 'c': 3|}
2188
+ """
2189
+ d = merge(*args, **kwargs)
2190
+ finals = d.transient()
2191
+ if isinstance(d, ldict):
2192
+ d = d.as_pdict()
2193
+ for (k,v) in d.items():
2194
+ if isinstance(v, LambdaType):
2195
+ finals[k] = lazy(_lambdadict_call, finals, v)
2196
+ else:
2197
+ finals[k] = v
2198
+ return ldict(finals)
2199
+
2200
+
2201
+ # Argument Utilities ##########################################################
2202
+
2203
+ from collections import namedtuple
2204
+ argstuple = namedtuple('argstuple', ('args', 'kwargs'))
2205
+ class args(argstuple):
2206
+ """An object type that represents a set of function arguments.
2207
+
2208
+ ``args(x1, x2 ... k1=v1, k2=v2 ...)`` yields an ``args`` object that
2209
+ represents the positional arguments ``x1, x2 ...`` and the named arguments
2210
+ ``k1=v1``, ``k2=v2``, etc.
2211
+
2212
+ If ``a`` is an instance of ``args`` and ``f`` is a function, then the
2213
+ arguments in ``a`` can be applied to ``f`` using either of the following
2214
+ methods:
2215
+
2216
+ - ``f @ a``
2217
+ - ``a.passto(f)``
2218
+
2219
+ Note that if ``f`` is an object that defines the ``__matmul__`` method,
2220
+ then the former syntax will call that method instead of the ``__rmatmul__``
2221
+ method of the ``args`` object ``a`` and thus won't work.
2222
+ """
2223
+ def __new__(cls, *args, **kwargs):
2224
+ return argstuple.__new__(cls, args, kwargs)
2225
+ def __rmatmul__(self, fn):
2226
+ return fn(*self.args, **self.kwargs)
2227
+ def passto(self, fn):
2228
+ return fn(*self.args, **self.kwargs)
2229
+ def copy(self, args=None, kwargs=None):
2230
+ """Returns a copy of the current ``args``, potentially with updates."""
2231
+ if args is None:
2232
+ args = self.args
2233
+ if kwargs is None:
2234
+ kwargs = self.kwargs
2235
+ if args is self.args and kwargs is self.kwargs:
2236
+ return self
2237
+ return argstuple.__new__(type(self), args, kwargs)
2238
+ @docwrap('immlib.argfilter')
2239
+ def argfilter(fn=None, /, **kwargs):
2240
+ """A decorator that creates decorators that filter function arguments.
2241
+
2242
+ A function decorated with ``@argfilter`` is turned into a an argument
2243
+ filter function, which itself can be used to decorate functions whose
2244
+ arguments need to be filtered.
2245
+
2246
+ In the definition of the filter function, the names of arguments must match
2247
+ those of the arguments they will be filtering on other functions. The
2248
+ filter function must return a tuple of the filtered values in the order
2249
+ they are defined in the function's argument list. The arguments may be
2250
+ given in any order, but a ``*`` in the arguments list indicates that any of
2251
+ the arguments following the ``*`` are not themselves being filtered and
2252
+ thus will not be returned from the filter function.
2253
+
2254
+ When a filter function is used to decorate another function, the decorator
2255
+ can optionally be given named arguments where the name corresponds to one
2256
+ of the arguments to the origional filter definition and the value
2257
+ corresponds to the name that is used for this parameter in the decorated
2258
+ functions. In this way, the parameter names don't have to match exactly
2259
+ those of the filter function and can instead be specified in the
2260
+ decoration.
2261
+
2262
+ Examples
2263
+ --------
2264
+ >>> @argfilter
2265
+ ... def fix_angle(angle, *, unit):
2266
+ ... angle = np.asarray(angle)
2267
+ ... if unit == 'degrees':
2268
+ ... angle = np.pi / 180 * angle
2269
+ ... elif unit != 'radians':
2270
+ ... raise ValueError(f'unrecognized unit: {unit}')
2271
+ ... return (angle,)
2272
+ ... @fix_angle
2273
+ ... def cos_halfangle(angle, unit='radians'):
2274
+ ... return np.cos(angle / 2)
2275
+
2276
+ >>> cos_halfangle([0, 360], 'degrees')
2277
+ array([1., -1.])
2278
+
2279
+ >>> @fix_angle(angle='theta')
2280
+ ... def sin_halfangle(theta, unit='radians'):
2281
+ ... return np.sin(theta / 2)
2282
+
2283
+ >>> sin_halfangle([0, 360], 'degrees')
2284
+ array([0., 0.])
2285
+ """
2286
+ sig = signature(fn)
2287
+ pos_args = {}
2288
+ pok_args = {}
2289
+ for (k,u) in sig.parameters.items():
2290
+ if u.kind == u.POSITIONAL_ONLY or u.kind == u.POSITIONAL_OR_KEYWORD:
2291
+ pos_args[k] = u
2292
+ elif u.kind == u.KEYWORD_ONLY:
2293
+ pok_args[k] = u
2294
+ else:
2295
+ raise ValueError("variadic filter arguments are not supported")
2296
+ # We make a function that uses these when it is used as a decorator. There
2297
+ # are two ways to call this decorator: one as @filter_func and the other is
2298
+ # @filter_func(origname1=newname1, origname2=newname2...) in order to
2299
+ # indicate that some of the arguments need to be renamed. These are written
2300
+ # as the private functions below and are then wrapped up into a partial.
2301
+ def fn_decr(f=None, /, **kwargs):
2302
+ return _argfilter_decr(pos_args, pok_args, fn, f, **kwargs)
2303
+ return wraps(fn)(fn_decr)
2304
+ def _argfilter_decr(pos_args, pok_args, filter_fn, fn=None, /, **kwargs):
2305
+ if len(kwargs) == 0:
2306
+ if fn is None:
2307
+ raise ValueError(
2308
+ "argfilter decorator requires either a function or options")
2309
+ else:
2310
+ return _argfilter_init(pos_args, pok_args, filter_fn, {}, fn)
2311
+ elif fn is None:
2312
+ return partial(
2313
+ _argfilter_init,
2314
+ pos_args, pok_args, filter_fn,
2315
+ kwargs)
2316
+ else:
2317
+ raise ValueError(
2318
+ "argfilter decorator requires a function or options, but not both")
2319
+ def _argfilter_init(pos_args, pok_args, filter_fn, tr, f):
2320
+ sig = signature(f)
2321
+ params = sig.parameters
2322
+ # Prep some data structures so that we can quickly extract the filtered
2323
+ # arguments when the function is called.
2324
+ keys = []
2325
+ argdat = []
2326
+ kwdat = {}
2327
+ for (args, dat) in ((pos_args,argdat), (pok_args,kwdat)):
2328
+ for (k0,arg) in args.items():
2329
+ k = tr.get(k0, k0)
2330
+ dflt = arg.default
2331
+ if k in params:
2332
+ p = params[k]
2333
+ if p.default is not p.empty:
2334
+ dflt = p.default
2335
+ else:
2336
+ if dflt is arg.empty:
2337
+ if k0 != k:
2338
+ k = f'{k0} ({k})'
2339
+ raise ValueError(f"filtered parameter {k} not found")
2340
+ app = (k, dflt)
2341
+ if isinstance(dat, list):
2342
+ dat.append(app)
2343
+ else:
2344
+ dat[k0] = app
2345
+ # Now pass these along to the argilter initialization function.
2346
+ keys = tuple(u[0] for u in argdat)
2347
+ fn = partial(_argfilter_dispatch, filter_fn, f, sig, argdat, kwdat, keys)
2348
+ return wraps(f)(fn)
2349
+ def _argfilter_dispatch(filter_fn, f, fsig,
2350
+ filt_argdat, filt_kwdat, filt_keys,
2351
+ *args, **kwargs):
2352
+ b = fsig.bind(*args, **kwargs)
2353
+ argmap = b.arguments
2354
+ filtered_vals = filter_fn(
2355
+ *(argmap.get(k, d) for (k,d) in filt_argdat),
2356
+ **{k0: argmap.get(k, d) for (k0,(k,d)) in filt_kwdat.items()})
2357
+ # The number of filtered items must be equal to the number we expect.
2358
+ if len(filtered_vals) != len(filt_keys):
2359
+ raise ValueError(
2360
+ f"filter on function {f.__name__} returned {len(filtered_vals)}"
2361
+ f" items but expected {len(filt_keys)}")
2362
+ argmap.update(zip(filt_keys, filtered_vals))
2363
+ return f(*b.args, **b.kwargs)
2364
+
2365
+
2366
+ # unitregistry ################################################################
2367
+
2368
+ # We put the unitregistry here and not in the quantity namespace because we
2369
+ # need it both for quantity and numeric and it causes a circular import if
2370
+ # placed in the quantity file.
2371
+ @docwrap('immlib.unitregistry')
2372
+ def unitregistry(obj, /, *args):
2373
+ """Returns the ``pint.UnitRegistry`` object for the given unit or quantity.
2374
+
2375
+ ``unitregistry(u)`` for a ``pint.Unit`` object ``u`` returns the
2376
+ ``pint.UnitRegistry`` object in which ``u`` is registered.
2377
+
2378
+ ``unitregistry(q)`` for a ``pint.Quantity`` object ``q`` returns the
2379
+ ``pint.UnitRegistry`` object in which ``q`` is registered.
2380
+
2381
+ ``unitregistry(ureg)`` returns ``ureg`` is ``ureg`` is itself a
2382
+ ``pint.UnitRegistry`` object.
2383
+
2384
+ ``unitregistry(Ellipsis)`` returns ``immlib.units``, the default unit
2385
+ registry for ``immlib``.
2386
+
2387
+ ``unitregistry(x)`` raises a ``TypeError`` for any other type of object.
2388
+
2389
+ ``unitregistry(x, default)`` returns ``unitregistry(x)`` unless a
2390
+ ``TypeError`` would be raised, in which case it returns the given
2391
+ ``default`` value. If ``default`` is ``Ellipsis``, then ``immlib.units`` is
2392
+ used as the default.
2393
+
2394
+ """
2395
+ nargs = len(args)
2396
+ if nargs > 1:
2397
+ raise TypeError(
2398
+ f"unitregistry() takes from 1 to 2 positional arguments but"
2399
+ f" {1+len(args)} were given")
2400
+ elif isinstance(obj, (pint.Unit, pint.Quantity)):
2401
+ return obj._REGISTRY
2402
+ elif isinstance(obj, pint.UnitRegistry):
2403
+ return obj
2404
+ elif obj is Ellipsis:
2405
+ from immlib import units
2406
+ return units
2407
+ elif len(args) == 0:
2408
+ raise TypeError(
2409
+ f"unitregistry() cannot convert object of type {type(obj)} to a"
2410
+ f" pint.UnitRegistry")
2411
+ else:
2412
+ default = args[0]
2413
+ if default is Ellipsis:
2414
+ from immlib import units as default
2415
+ return default
2416
+
2417
+
2418
+ # Caching #####################################################################
2419
+
2420
+ @docwrap('immlib.util.to_pathcache')
2421
+ def to_pathcache(obj):
2422
+ """Returns a ``joblib.Memory`` object that corresponds to the given path
2423
+ object.
2424
+
2425
+ ``to_pathcache(obj)`` converts the given object `obj` into a
2426
+ ``joblib.Memory`` cache manager. The object may be any of the following:
2427
+
2428
+ - a ``joblib.Memory`` object;
2429
+ - a filename or pathlib object pointing to a directory; or
2430
+ - a tuple containing a filename or pathlib object followed by a dict-like
2431
+ object of options to ``joblib.Memory``.
2432
+
2433
+ If the `obj` is ``None``, then ``None`` is returned. However, a
2434
+ ``joblib.Memory`` object whose location parameter is ``None`` can be
2435
+ created by using the object ``(None, opts)`` where ``opts`` may be ``None``
2436
+ or an empty dictionary.
2437
+
2438
+ The ``joblib.Memory`` constructor takes certain arguments; this function
2439
+ makes one change to those arguments: the ``verbose`` option is by default 0
2440
+ when filtered through this function, meaning that no output will be printed
2441
+ unless a ``verbose`` argument of greater than 0 is explicitly given.
2442
+
2443
+ See Also
2444
+ --------
2445
+ joblib.Memory, to_lrucache
2446
+ """
2447
+ # If we have been given a Memory object, just return it; otherwise, we
2448
+ # check to parse the object into path or path + options.
2449
+ if isinstance(obj, Memory):
2450
+ return obj
2451
+ elif is_tuple(obj):
2452
+ n = len(obj)
2453
+ if n == 1:
2454
+ (obj,opts) = (obj[0], {})
2455
+ elif n == 2:
2456
+ (obj,opts) = obj
2457
+ else:
2458
+ raise ValueError("only 1- or 2-tuples can become pathcaches")
2459
+ if opts is None:
2460
+ opts = {}
2461
+ elif isinstance(obj, args):
2462
+ (obj,opts) = (obj.args, obj.kwargs)
2463
+ if len(obj) == 1:
2464
+ obj = obj[0]
2465
+ else:
2466
+ raise TypeError(
2467
+ f"to_pathcache() takes exactly one argument ({len(obj)} given")
2468
+ else:
2469
+ opts = {}
2470
+ # We change the default argument of verbose into 0 in this function because
2471
+ # we don't want unintentional logging.
2472
+ if 'verbose' not in opts:
2473
+ opts['verbose'] = 0
2474
+ # Whether there were or were not any options, then we now have either a
2475
+ # string or pathlib path that we want to pass to the memory constructor.
2476
+ if isinstance(obj, Path) or isinstance(obj, str) or obj is None:
2477
+ return Memory(obj, **opts)
2478
+ else:
2479
+ raise TypeError(
2480
+ f"to_pathcache: arg must be path, str, or None; not {type(obj)}")
2481
+ @docwrap('immlib.util.to_lrucache')
2482
+ def to_lrucache(obj):
2483
+ """Returns an ``lru_cache`` function appropriate for the given object.
2484
+
2485
+ ``to_lrucache(obj)`` converts the given object `obj` into either
2486
+ ``None``, the ``lru_cache`` function, or a function returned by
2487
+ ``lru_cache``. The object may be any of the following:
2488
+
2489
+ - ``lru_cache`` itself, in which case it is just returned;
2490
+ - ``None`` or 0, indicating that no caching should be used (``None`` is
2491
+ returned in these cases);
2492
+ - ``inf``, indicating that an infinite cache should be returned; or
2493
+ - a positive integer indicating the number of most recently used items
2494
+ to keep in the cache.
2495
+
2496
+ See Also
2497
+ --------
2498
+ functools.lru_cache
2499
+ """
2500
+ from ._numeric import (is_number, is_integer)
2501
+ if obj is lru_cache:
2502
+ return obj
2503
+ elif obj is None:
2504
+ return None
2505
+ elif is_number(obj):
2506
+ if obj == 0:
2507
+ return None
2508
+ elif obj == np.inf:
2509
+ return lru_cache(maxsize=None)
2510
+ elif not is_integer(obj):
2511
+ raise TypeError("to_lrucache size must be an int")
2512
+ elif obj < 1:
2513
+ raise ValueError("to_lrucache size must be > 0")
2514
+ else:
2515
+ return lru_cache(maxsize=int(obj))
2516
+ else:
2517
+ raise TypeError(f"bad type for to_lrucache: {type(obj)}")
2518
+
2519
+
2520
+ # Other #######################################################################
2521
+
2522
+ def identfn(x):
2523
+ "The identify function; ``identfn(x)`` returns `x`."
2524
+ return x