1
|
|
|
import logging |
|
|
|
|
2
|
|
|
import sys |
3
|
|
|
from collections import defaultdict |
4
|
|
|
from collections.abc import ByteString, Callable, Generator, Iterable, Iterator, Mapping, Sequence |
5
|
|
|
from contextlib import contextmanager |
6
|
|
|
from typing import Any, Type, TypeVar |
|
|
|
|
7
|
|
|
|
8
|
|
|
from pocketutils.core._internal import is_lambda, look, parse_bool, parse_bool_flex |
|
|
|
|
9
|
|
|
from pocketutils.core.exceptions import ( |
|
|
|
|
10
|
|
|
MultipleMatchesError, |
11
|
|
|
RefusingRequestError, |
12
|
|
|
XKeyError, |
13
|
|
|
XTypeError, |
14
|
|
|
XValueError, |
15
|
|
|
) |
16
|
|
|
from pocketutils.core.input_output import DevNull, Writeable |
|
|
|
|
17
|
|
|
|
18
|
|
|
Y = TypeVar("Y") |
|
|
|
|
19
|
|
|
T = TypeVar("T") |
|
|
|
|
20
|
|
|
Z = TypeVar("Z") |
|
|
|
|
21
|
|
|
Q = TypeVar("Q") |
|
|
|
|
22
|
|
|
logger = logging.getLogger("pocketutils") |
23
|
|
|
|
24
|
|
|
|
25
|
|
|
class CommonTools: |
|
|
|
|
26
|
|
|
def nice_size(n_bytes: int, *, space: str = "") -> str: |
|
|
|
|
27
|
|
|
""" |
28
|
|
|
Uses IEC 1998 units, such as KiB (1024). |
29
|
|
|
n_bytes: Number of bytes |
30
|
|
|
space: Separator between digits and units |
31
|
|
|
|
32
|
|
|
Returns: |
33
|
|
|
Formatted string |
34
|
|
|
""" |
35
|
|
|
data = { |
36
|
|
|
"PiB": 1024**5, |
37
|
|
|
"TiB": 1024**4, |
38
|
|
|
"GiB": 1024**3, |
39
|
|
|
"MiB": 1024**2, |
40
|
|
|
"KiB": 1024**1, |
41
|
|
|
} |
42
|
|
|
for suffix, scale in data.items(): |
43
|
|
|
if n_bytes >= scale: |
44
|
|
|
break |
45
|
|
|
else: |
46
|
|
|
scale, suffix = 1, "B" |
47
|
|
|
return str(n_bytes // scale) + space + suffix |
48
|
|
|
|
49
|
|
|
@classmethod |
50
|
|
|
def limit(cls, items: Iterable[Q], n: int) -> Generator[Q, None, None]: |
|
|
|
|
51
|
|
|
for _i, x in zip(range(n), items): |
|
|
|
|
52
|
|
|
yield x |
53
|
|
|
|
54
|
|
|
@classmethod |
55
|
|
|
def is_float(cls, s: Any) -> bool: |
|
|
|
|
56
|
|
|
""" |
57
|
|
|
Returns whether ``float(s)`` succeeds. |
58
|
|
|
""" |
59
|
|
|
try: |
60
|
|
|
float(s) |
61
|
|
|
return True |
62
|
|
|
except ValueError: |
63
|
|
|
return False |
64
|
|
|
|
65
|
|
|
@classmethod |
66
|
|
|
def try_none( |
67
|
|
|
cls, |
|
|
|
|
68
|
|
|
function: Callable[[], T], |
|
|
|
|
69
|
|
|
fail_val: T | None = None, |
|
|
|
|
70
|
|
|
exception=Exception, |
|
|
|
|
71
|
|
|
) -> T | None: |
72
|
|
|
""" |
73
|
|
|
Returns the value of a function or None if it raised an exception. |
74
|
|
|
|
75
|
|
|
Args: |
76
|
|
|
function: Try calling this function |
77
|
|
|
fail_val: Return this value |
78
|
|
|
exception: Restrict caught exceptions to subclasses of this type |
79
|
|
|
""" |
80
|
|
|
# noinspection PyBroadException |
81
|
|
|
try: |
82
|
|
|
return function() |
83
|
|
|
except exception: |
|
|
|
|
84
|
|
|
return fail_val |
85
|
|
|
|
86
|
|
|
@classmethod |
87
|
|
|
def succeeds(cls, function: Callable[[], Any], exception=Exception) -> bool: |
|
|
|
|
88
|
|
|
"""Returns True iff ``function`` does not raise an error.""" |
89
|
|
|
return cls.try_none(function, exception=exception) is not None |
90
|
|
|
|
91
|
|
|
@classmethod |
92
|
|
|
def or_null(cls, x: Any, dtype=lambda s: s, or_else: Any = None) -> Any | None: |
|
|
|
|
93
|
|
|
""" |
94
|
|
|
Return ``None`` if the operation ``dtype`` on ``x`` failed; returns the result otherwise. |
95
|
|
|
""" |
96
|
|
|
return or_else if cls.is_null(x) else dtype(x) |
97
|
|
|
|
98
|
|
|
@classmethod |
99
|
|
|
def or_raise( |
|
|
|
|
100
|
|
|
cls, |
|
|
|
|
101
|
|
|
x: Any, |
|
|
|
|
102
|
|
|
dtype=lambda s: s, |
|
|
|
|
103
|
|
|
or_else: BaseException | type[BaseException] | None = None, |
|
|
|
|
104
|
|
|
) -> Any: |
105
|
|
|
""" |
106
|
|
|
Returns ``dtype(x)`` if ``x`` is not None, or raises ``or_else``. |
107
|
|
|
""" |
108
|
|
|
if or_else is None: |
109
|
|
|
or_else = LookupError(f"Value is {x}") |
110
|
|
|
elif isinstance(or_else, type): |
111
|
|
|
or_else = or_else(f"Value is {x}") |
112
|
|
|
if cls.is_null(x): |
113
|
|
|
raise or_else |
114
|
|
|
return dtype(x) |
115
|
|
|
|
116
|
|
|
@classmethod |
117
|
|
|
def iterator_has_elements(cls, x: Iterator[Any]) -> bool: |
|
|
|
|
118
|
|
|
""" |
119
|
|
|
Returns False iff ``next(x)`` raises a ``StopIteration``. |
120
|
|
|
WARNING: Tries to call ``next(x)``, progressing iterators. Don't use ``x`` after calling this. |
|
|
|
|
121
|
|
|
Note that calling ``iterator_has_elements([5])`` will raise a `TypeError` |
122
|
|
|
|
123
|
|
|
Args: |
124
|
|
|
x: Must be an Iterator |
125
|
|
|
""" |
126
|
|
|
return cls.succeeds(lambda: next(x), StopIteration) |
127
|
|
|
|
128
|
|
|
@classmethod |
129
|
|
|
def is_null(cls, x: Any) -> bool: |
|
|
|
|
130
|
|
|
""" |
131
|
|
|
Returns True for None, NaN, and NaT (not a time) values from Numpy, Pandas, and Python. |
132
|
|
|
Not perfect; may return false positive True for types declared outside Numpy and Pandas. |
133
|
|
|
""" |
134
|
|
|
if x is None: |
135
|
|
|
return True |
136
|
|
|
if isinstance(x, str): |
137
|
|
|
return False |
138
|
|
|
return str(x) in [ |
139
|
|
|
"nan", # float('NaN') and Numpy float NaN |
140
|
|
|
"NaN", # Pandas NaN and decimal.Decimal NaN |
141
|
|
|
"<NA>", # Pandas pd.NA |
142
|
|
|
"NaT", # Numpy datetime and timedelta NaT |
143
|
|
|
] |
144
|
|
|
|
145
|
|
|
@classmethod |
146
|
|
|
def is_empty(cls, x: Any) -> bool: |
|
|
|
|
147
|
|
|
""" |
148
|
|
|
Returns True if x is None, NaN according to Pandas, or contains 0 items. |
149
|
|
|
|
150
|
|
|
That is, if and only if: |
151
|
|
|
- :meth:`is_null` |
152
|
|
|
- x is something with 0 length |
153
|
|
|
- x is iterable and has 0 elements (will call ``__iter__``) |
154
|
|
|
|
155
|
|
|
Raises: |
156
|
|
|
RefusingRequestError If ``x`` is an Iterator. Calling this would empty the iterator, which is dangerous. |
|
|
|
|
157
|
|
|
""" |
158
|
|
|
if isinstance(x, Iterator): |
159
|
|
|
raise RefusingRequestError("Do not call is_empty on an iterator.") |
160
|
|
|
try: |
161
|
|
|
if cls.is_null(x): |
162
|
|
|
return True |
163
|
|
|
except (ValueError, TypeError): |
164
|
|
|
pass |
165
|
|
|
return ( |
166
|
|
|
hasattr(x, "__len__") |
167
|
|
|
and len(x) == 0 |
168
|
|
|
or hasattr(x, "__iter__") |
169
|
|
|
and len(list(iter(x))) == 0 |
170
|
|
|
) |
171
|
|
|
|
172
|
|
|
@classmethod |
173
|
|
|
def is_probable_null(cls, x: Any) -> bool: |
|
|
|
|
174
|
|
|
""" |
175
|
|
|
Returns True if ``x`` is None, NaN according to Pandas, 0 length, or a string representation. |
|
|
|
|
176
|
|
|
|
177
|
|
|
Specifically, returns True if and only if: |
178
|
|
|
- :meth:`is_null` |
179
|
|
|
- x is something with 0 length |
180
|
|
|
- x is iterable and has 0 elements (will call ``__iter__``) |
181
|
|
|
- a str(x) is 'nan', 'na', 'n/a', 'null', or 'none'; case-insensitive |
182
|
|
|
|
183
|
|
|
Things that are **NOT** probable nulls: |
184
|
|
|
- ``0`` |
185
|
|
|
- ``[None]`` |
186
|
|
|
|
187
|
|
|
Raises: |
188
|
|
|
TypeError If ``x`` is an Iterator. |
189
|
|
|
Calling this would empty the iterator, which is dangerous. |
190
|
|
|
""" |
191
|
|
|
return cls.is_empty(x) or str(x).lower() in ["nan", "n/a", "na", "null", "none"] |
192
|
|
|
|
193
|
|
|
@classmethod |
194
|
|
|
def unique(cls, sequence: Iterable[T]) -> Sequence[T]: |
|
|
|
|
195
|
|
|
""" |
196
|
|
|
Returns the unique items in `sequence`, in the order they appear in the iteration. |
197
|
|
|
|
198
|
|
|
Args: |
199
|
|
|
sequence: Any once-iterable sequence |
200
|
|
|
|
201
|
|
|
Returns: |
202
|
|
|
An ordered List of unique elements |
203
|
|
|
""" |
204
|
|
|
seen = set() |
205
|
|
|
return [x for x in sequence if not (x in seen or seen.add(x))] |
206
|
|
|
|
207
|
|
|
@classmethod |
208
|
|
|
def first(cls, collection: Iterable[Any], attr: str | None = None) -> Any: |
|
|
|
|
209
|
|
|
""" |
210
|
|
|
Gets the first element. |
211
|
|
|
|
212
|
|
|
.. warning:: |
213
|
|
|
Tries to call ``next(x)``, progressing iterators. |
214
|
|
|
|
215
|
|
|
Args: |
216
|
|
|
collection: Any iterable |
217
|
|
|
attr: The name of the attribute that might be defined on the elements, |
218
|
|
|
or None to indicate the elements themselves should be used |
219
|
|
|
|
220
|
|
|
Returns: |
221
|
|
|
Either ``None`` or the value, according to the rules: |
222
|
|
|
- The attribute of the first element if ``attr`` is defined on an element |
223
|
|
|
- None if the sequence is empty |
224
|
|
|
- None if the sequence has no attribute ``attr`` |
225
|
|
|
""" |
226
|
|
|
try: |
227
|
|
|
# note: calling iter on an iterator creates a view only |
228
|
|
|
x = next(iter(collection)) |
|
|
|
|
229
|
|
|
return x if attr is None else look(x, attr) |
230
|
|
|
except StopIteration: |
231
|
|
|
return None |
232
|
|
|
|
233
|
|
|
@classmethod |
234
|
|
|
def iter_rowcol(cls, n_rows: int, n_cols: int) -> Generator[tuple[int, int], None, None]: |
|
|
|
|
235
|
|
|
""" |
236
|
|
|
An iterator over (row column) pairs for a row-first grid traversal. |
237
|
|
|
|
238
|
|
|
Example: |
239
|
|
|
.. code-block:: |
240
|
|
|
it = CommonTools.iter_rowcol(5, 3) |
241
|
|
|
[next(it) for _ in range(5)] # [(0,0),(0,1),(0,2),(1,0),(1,1)] |
242
|
|
|
""" |
243
|
|
|
for i in range(n_rows * n_cols): |
244
|
|
|
yield i // n_cols, i % n_cols |
245
|
|
|
|
246
|
|
|
@classmethod |
247
|
|
|
def multidict( |
248
|
|
|
cls, |
|
|
|
|
249
|
|
|
sequence: Iterable[Z], |
|
|
|
|
250
|
|
|
key_attr: str | Iterable[str] | Callable[[Y], Z], |
|
|
|
|
251
|
|
|
skip_none: bool = False, |
|
|
|
|
252
|
|
|
) -> Mapping[Y, Sequence[Z]]: |
|
|
|
|
253
|
|
|
""" |
254
|
|
|
Builds a mapping from keys to multiple values. |
255
|
|
|
Builds a mapping of some attribute in ``sequence`` to |
256
|
|
|
the containing elements of ``sequence``. |
257
|
|
|
|
258
|
|
|
Args: |
259
|
|
|
sequence: Any iterable |
260
|
|
|
key_attr: Usually string like 'attr1.attr2'; see `look` |
261
|
|
|
skip_none: If None, raises a `KeyError` if the key is missing for any item; otherwise, skips it |
|
|
|
|
262
|
|
|
""" |
263
|
|
|
dct = defaultdict(lambda: []) |
264
|
|
|
for item in sequence: |
265
|
|
|
v = look(item, key_attr) |
|
|
|
|
266
|
|
|
if not skip_none and v is None: |
267
|
|
|
raise XKeyError(f"No {key_attr} in {item}", key=key_attr) |
268
|
|
|
if v is not None: |
269
|
|
|
dct[v].append(item) |
270
|
|
|
return dct |
271
|
|
|
|
272
|
|
|
@classmethod |
273
|
|
|
def devnull(cls): |
274
|
|
|
""" |
275
|
|
|
Yields a 'writer' that does nothing. |
276
|
|
|
|
277
|
|
|
Example: |
278
|
|
|
.. code-block:: |
279
|
|
|
|
280
|
|
|
with CommonTools.devnull() as devnull: |
281
|
|
|
devnull.write('hello') |
282
|
|
|
""" |
283
|
|
|
yield DevNull() |
284
|
|
|
|
285
|
|
|
@classmethod |
286
|
|
|
def parse_bool(cls, s: str) -> bool: |
|
|
|
|
287
|
|
|
""" |
288
|
|
|
Parses a 'true'/'false' string to a bool, ignoring case. |
289
|
|
|
|
290
|
|
|
Raises: |
291
|
|
|
ValueError: If neither true nor false |
292
|
|
|
""" |
293
|
|
|
return parse_bool(s) |
294
|
|
|
|
295
|
|
|
@classmethod |
296
|
|
|
def parse_bool_flex(cls, s: str) -> bool: |
|
|
|
|
297
|
|
|
""" |
298
|
|
|
Parses a 'true'/'false'/'yes'/'no'/... string to a bool, ignoring case. |
299
|
|
|
|
300
|
|
|
Allowed: |
301
|
|
|
- "true", "t", "yes", "y", "1" |
302
|
|
|
- "false", "f", "no", "n", "0" |
303
|
|
|
|
304
|
|
|
Raises: |
305
|
|
|
XValueError: If neither true nor false |
306
|
|
|
""" |
307
|
|
|
return parse_bool_flex(s) |
308
|
|
|
|
309
|
|
|
@classmethod |
310
|
|
|
def is_lambda(cls, function: Any) -> bool: |
311
|
|
|
""" |
312
|
|
|
Returns whether this is a lambda function. Will return False for non-callables. |
313
|
|
|
""" |
314
|
|
|
return is_lambda(function) |
315
|
|
|
|
316
|
|
|
@classmethod |
317
|
|
|
def only( |
318
|
|
|
cls, |
|
|
|
|
319
|
|
|
sequence: Iterable[Any], |
|
|
|
|
320
|
|
|
condition: str | Callable[[Any], bool] = None, |
|
|
|
|
321
|
|
|
*, |
|
|
|
|
322
|
|
|
name: str = "collection", |
|
|
|
|
323
|
|
|
) -> Any: |
324
|
|
|
""" |
325
|
|
|
Returns either the SINGLE (ONLY) UNIQUE ITEM in the sequence or raises an exception. |
326
|
|
|
Each item must have __hash__ defined on it. |
327
|
|
|
|
328
|
|
|
Args: |
329
|
|
|
sequence: A list of any items (untyped) |
330
|
|
|
condition: If nonnull, consider only those matching this condition |
331
|
|
|
name: Just a name for the collection to use in an error message |
332
|
|
|
|
333
|
|
|
Returns: |
334
|
|
|
The first item the sequence. |
335
|
|
|
|
336
|
|
|
Raises: |
337
|
|
|
LookupError If the sequence is empty |
338
|
|
|
MultipleMatchesError If there is more than one unique item. |
339
|
|
|
""" |
340
|
|
|
|
341
|
|
|
def _only(sq): |
|
|
|
|
342
|
|
|
st = set(sq) |
|
|
|
|
343
|
|
|
if len(st) > 1: |
344
|
|
|
raise MultipleMatchesError("More then 1 item in " + str(name)) |
345
|
|
|
if len(st) == 0: |
346
|
|
|
raise LookupError("Empty " + str(name)) |
347
|
|
|
return next(iter(st)) |
348
|
|
|
|
349
|
|
|
if condition and isinstance(condition, str): |
|
|
|
|
350
|
|
|
return _only( |
351
|
|
|
[ |
352
|
|
|
s |
353
|
|
|
for s in sequence |
354
|
|
|
if ( |
355
|
|
|
not getattr(s, condition[1:]) |
356
|
|
|
if condition.startswith("!") |
357
|
|
|
else getattr(s, condition) |
358
|
|
|
) |
359
|
|
|
] |
360
|
|
|
) |
361
|
|
|
elif condition: |
362
|
|
|
return _only([s for s in sequence if condition(s)]) |
363
|
|
|
else: |
364
|
|
|
return _only(sequence) |
365
|
|
|
|
366
|
|
|
@classmethod |
367
|
|
|
def forever(cls) -> Iterator[int]: |
|
|
|
|
368
|
|
|
""" |
369
|
|
|
Yields i for i in range(0, infinity). |
370
|
|
|
Useful for simplifying a i = 0; while True: i += 1 block. |
371
|
|
|
""" |
372
|
|
|
i = 0 |
373
|
|
|
while True: |
374
|
|
|
yield i |
375
|
|
|
i += 1 |
376
|
|
|
|
377
|
|
|
@classmethod |
378
|
|
|
def to_true_iterable(cls, s: Any) -> Iterable[Any]: |
|
|
|
|
379
|
|
|
""" |
380
|
|
|
See :meth:`is_true_iterable`. |
381
|
|
|
|
382
|
|
|
Examples: |
383
|
|
|
- ``to_true_iterable('abc') # ['abc']`` |
384
|
|
|
- ``to_true_iterable(['ab', 'cd')] # ['ab', 'cd']`` |
385
|
|
|
""" |
386
|
|
|
if cls.is_true_iterable(s): |
|
|
|
|
387
|
|
|
return s |
388
|
|
|
else: |
389
|
|
|
return [s] |
390
|
|
|
|
391
|
|
|
@classmethod |
392
|
|
|
def is_true_iterable(cls, s: Any) -> bool: |
|
|
|
|
393
|
|
|
""" |
394
|
|
|
Returns whether ``s`` is a probably "proper" iterable. |
395
|
|
|
In other words, iterable but not a string or bytes. |
396
|
|
|
|
397
|
|
|
.. caution:: |
398
|
|
|
This is not fully reliable. |
399
|
|
|
Types that do not define ``__iter__`` but are iterable |
400
|
|
|
via ``__getitem__`` will not be included. |
401
|
|
|
""" |
402
|
|
|
return ( |
403
|
|
|
s is not None |
404
|
|
|
and isinstance(s, Iterable) |
405
|
|
|
and not isinstance(s, str) |
406
|
|
|
and not isinstance(s, ByteString) |
407
|
|
|
) |
408
|
|
|
|
409
|
|
|
@classmethod |
410
|
|
|
@contextmanager |
411
|
|
|
def null_context(cls) -> Generator[None, None, None]: |
|
|
|
|
412
|
|
|
""" |
413
|
|
|
Returns an empty context (literally just yields). |
414
|
|
|
Useful to simplify when a generator needs to be used depending on a switch. |
415
|
|
|
Ex:: |
416
|
|
|
if verbose_flag: |
417
|
|
|
do_something() |
418
|
|
|
else: |
419
|
|
|
with Tools.silenced(): |
420
|
|
|
do_something() |
421
|
|
|
Can become:: |
422
|
|
|
with (Tools.null_context() if verbose else Tools.silenced()): |
423
|
|
|
do_something() |
424
|
|
|
""" |
425
|
|
|
yield |
426
|
|
|
|
427
|
|
|
@classmethod |
428
|
|
|
def look(cls, obj: Y, attrs: str | Iterable[str] | Callable[[Y], Z]) -> Z | None: |
|
|
|
|
429
|
|
|
""" |
430
|
|
|
Follows a dotted syntax for getting an item nested in class attributes. |
431
|
|
|
Returns the value of a chain of attributes on object ``obj``, |
432
|
|
|
or None any object in that chain is None or lacks the next attribute. |
433
|
|
|
|
434
|
|
|
Example: |
435
|
|
|
Get a kitten's breed:: |
436
|
|
|
|
437
|
|
|
BaseTools.look(kitten), 'breed.name') # either None or a string |
438
|
|
|
|
439
|
|
|
Args: |
440
|
|
|
obj: Any object |
441
|
|
|
attrs: One of: |
442
|
|
|
- A string in the form attr1.attr2, translating to ``obj.attr1`` |
443
|
|
|
- An iterable of strings of the attributes |
444
|
|
|
- A function that maps ``obj`` to its output; |
445
|
|
|
equivalent to calling `attrs(obj)` but returning None on ``AttributeError``. |
446
|
|
|
|
447
|
|
|
Returns: |
448
|
|
|
Either None or the type of the attribute |
449
|
|
|
|
450
|
|
|
Raises: |
451
|
|
|
TypeError: |
452
|
|
|
""" |
453
|
|
|
return look(obj, attrs) |
454
|
|
|
|
455
|
|
|
@classmethod |
456
|
|
|
def make_writer(cls, writer: Writeable | Callable[[str], Any]): |
|
|
|
|
457
|
|
|
if Writeable.isinstance(writer): |
|
|
|
|
458
|
|
|
return writer |
459
|
|
|
elif callable(writer): |
460
|
|
|
|
461
|
|
|
class W_(Writeable): |
|
|
|
|
462
|
|
|
def write(self, msg): |
463
|
|
|
writer(msg) |
464
|
|
|
|
465
|
|
|
def flush(self): |
466
|
|
|
pass |
467
|
|
|
|
468
|
|
|
def close(self): |
469
|
|
|
pass |
470
|
|
|
|
471
|
|
|
return W_() |
472
|
|
|
raise XTypeError(f"{type(writer)} cannot be wrapped into a Writeable") |
473
|
|
|
|
474
|
|
|
@classmethod |
475
|
|
|
def get_log_function(cls, log: str | Callable[[str], Any] | None) -> Callable[[str], None]: |
|
|
|
|
476
|
|
|
""" |
477
|
|
|
Gets a logging function from user input. |
478
|
|
|
The rules are: |
479
|
|
|
- If None, uses logger.info |
480
|
|
|
- If 'print' or 'stdout', use sys.stdout.write |
481
|
|
|
- If 'stderr', use sys.stderr.write |
482
|
|
|
- If another str or int, try using that logger level (raises an error if invalid) |
483
|
|
|
- If callable, returns it |
484
|
|
|
- If it has a callable method called 'write', uses that |
485
|
|
|
|
486
|
|
|
Returns: |
487
|
|
|
A function of the log message that returns None |
488
|
|
|
""" |
489
|
|
|
if log is None: |
|
|
|
|
490
|
|
|
return logger.info |
491
|
|
|
elif isinstance(log, str) and log.lower() in ["print", "stdout"]: |
492
|
|
|
# noinspection PyTypeChecker |
493
|
|
|
return sys.stdout.write |
494
|
|
|
elif log == "stderr": |
495
|
|
|
# noinspection PyTypeChecker |
496
|
|
|
return sys.stderr.write |
497
|
|
|
elif isinstance(log, int): |
498
|
|
|
return getattr(logger, logging.getLevelName(log).lower()) |
499
|
|
|
elif isinstance(log, str): |
500
|
|
|
return getattr(logger, log.lower()) |
501
|
|
|
elif callable(log): |
502
|
|
|
return log |
503
|
|
|
elif hasattr(log, "write"): |
504
|
|
|
return log.write |
505
|
|
|
else: |
506
|
|
|
raise XTypeError(f"Log type {type(log)} not known", actual=str(type(log))) |
507
|
|
|
|
508
|
|
|
@classmethod |
509
|
|
|
def sentinel(cls, name: str) -> Any: |
|
|
|
|
510
|
|
|
class _Sentinel: |
511
|
|
|
def __eq__(self, other): |
512
|
|
|
return self is other |
513
|
|
|
|
514
|
|
|
def __reduce__(self): |
515
|
|
|
return name # returning string is for singletons |
516
|
|
|
|
517
|
|
|
def __str__(self): |
518
|
|
|
return name |
519
|
|
|
|
520
|
|
|
def __repr__(self): |
521
|
|
|
return name |
522
|
|
|
|
523
|
|
|
return _Sentinel() |
524
|
|
|
|
525
|
|
|
def __repr__(self): |
526
|
|
|
return self.__class__.__name__ |
527
|
|
|
|
528
|
|
|
def __str__(self): |
529
|
|
|
return self.__class__.__name__ |
530
|
|
|
|
531
|
|
|
|
532
|
|
|
__all__ = ["CommonTools", "Writeable"] |
533
|
|
|
|