File: tl.py 1 #!/usr/bin/python 2 3 # The MIT License (MIT) 4 # 5 # Copyright (c) 2026 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the "Software"), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 info = ''' 27 tl [options...] [python expression] [filepaths/URIs...] 28 29 30 Transform Lines runs a Python expression on each line of plain-text data: 31 each expression given emits its result as its own line. Each input line is 32 available to the expression as either `line`, or `l`. Lines are always 33 stripped of any trailing end-of-line bytes/sequences. 34 35 When the expression results in non-string iterable values, a sort of input 36 `amplification` happens for the current input-line, where each item from 37 the result is emitted on its own output line. Dictionaries emit their data 38 as a single JSON line. 39 40 When a formula's result is the None value, it emits no output line, which 41 filters-out the current line, the same way empty-iterable results do. 42 43 When in `all` mode, all input lines are read first into a list of strings, 44 whose items are all stripped of any end-of-line sequences, and kept in the 45 `lines` global variable: the expression given is then run only once. 46 47 Similarly, if the argument before the expression is a single equals sign 48 (a `=`, but without the quotes), no data are read/loaded: the expression is 49 then run only once, effectively acting as a `pure` plain-text generator. 50 51 Current-input names, depending on mode: 52 53 names mode evaluation 54 55 l, line each-line (default) for each input line 56 lines all-lines once, after last input line 57 b, block, p, par block/paragraph for each paragraph of lines 58 v, value jsonl for each input line 59 (no name) no-input once, without an input value 60 61 Modes `each-line` (the default) and `block/paragraph` also define `i` as an 62 integer which starts as 0, and which is incremented after each evaluation. 63 64 Options, where leading double-dashes are also allowed, except for alias `=`: 65 66 -a read all lines at once into a string-list called `lines` 67 -all same as -a 68 -lines same as -a 69 70 -b read uninterrupted blocks/groups of lines as paragraphs 71 -blocks same as -b 72 -g same as -b 73 -groups same as -b 74 -p same as -b 75 -par same as -b 76 -para same as -b 77 -paragraphs same as -b 78 79 -h show this help message 80 -help same as -h 81 82 -jsonl transform JSON Lines into proper JSON 83 84 -nil don't read any input, and run the expression only once 85 -no-input same as -nil 86 -noinput same as -nil 87 -none same as -nil 88 -null same as -nil 89 -null-input same as -nil 90 -nullinput same as -nil 91 = same as -nil 92 93 -p show a performance/time-profile of the full `task` run 94 -prof same as -p 95 -profile same as -p 96 97 -s read each input as a whole string 98 -str same as -s 99 -string same as -s 100 -w same as -s 101 -whole same as -s 102 103 -t show a full traceback of this script for exceptions 104 -trace same as -t 105 -traceback same as -t 106 107 108 Extra Functions 109 110 blue(s) color strings blue, using surrounding ANSI-style sequences 111 gray(s) color strings gray, using surrounding ANSI-style sequences 112 green(s) color strings green, using surrounding ANSI-style sequences 113 highlight(s) highlight strings, using surrounding ANSI-style sequences 114 hilite(s) same as func highlight 115 orange(s) color strings orange, using surrounding ANSI-style sequences 116 purple(s) color strings purple, using surrounding ANSI-style sequences 117 red(s) color strings red, using surrounding ANSI-style sequences 118 119 realign(x, gap=2) pad items across lines, so that all "columns" align 120 121 after(x, y) ignore items until the one given; for strings and sequences 122 afterfinal(x, y) backward counterpart of func after 123 afterlast(x, y) same as func afterfinal 124 arrayish(x) check if value is a list, a tuple, or a generator 125 basename(s) get the final/file part of a pathname 126 before(x, y) ignore items since the one given; for strings and sequences 127 beforefinal(x, y) backward counterpart of func before 128 beforelast(x, y) same as func beforefinal 129 chunk(x, size) split/resequence items into chunks of the length given 130 chunked(x, size) same as func chunk 131 compose(*args) make a func which chain-calls all funcs given 132 composed(*args) same as func compose 133 cond(*args) expression-friendly fully-evaluated if-else chain 134 debase64(s) decode base64 strings, including data-URIs 135 dedup(x) ignore later (re)occurrences of values in a sequence 136 dejson(x, f=None) safe parse JSON from strings 137 denan(x, y) turn a floating-point NaN values into the fallback given 138 denil(*args) return the first non-null/none value among those given 139 denone(*args) same as func denil 140 denull(*args) same as func denil 141 dirname(s) get the folder/directory/parent part of a pathname 142 dive(x, f) transform value in depth-first-recursive fashion 143 divebin(x, y, f) binary (2-input) version of recursive-transform func dive 144 drop(x, *what) ignore keys or substrings; for strings, dicts, dict-lists 145 dropped(x, *v) same as func drop 146 each(x, f) generalization of built-in func map 147 endict(x) turn non-dictionary values into dicts with string keys 148 enfloat(x, f=nan) turn values into floats, offering a fallback on failure 149 enint(x, f=None) turn values into ints, offering a fallback on failure 150 enlist(x) turn non-list values into lists 151 entuple(x) turn non-tuple values into tuples 152 ext(s) return the file-extension part of a pathname, if available 153 fields(s) split fields AWK-style from the string given 154 filtered(x, f) same as func keep 155 flat(*args) flatten everything into an unnested sequence 156 fromto(x, y, ?f) sequence integers, end-value included 157 group(x, ?by) group values into dicts of lists; optional transform func 158 grouped(x, ?by) same as func group 159 harden(f, v) make funcs which return values instead of exceptions 160 hardened(f, v) same as func harden 161 countif(x, f) count how many values make the func given true-like 162 idiota(x, ?f) dict-counterpart of func iota 163 ints(x, y, ?f) make sequences of increasing integers, which include the end 164 iota(x, ?f) make an integer sequence from 1 up to the number given 165 join(x, y) join values into a string; make a dict from keys and values 166 json0(x) turn a value into its smallest JSON-string representation 167 json2(x) turn a value into a 2-space-indented multi-line JSON string 168 jsonl(x) turn a value into a sequence of single-line (JSONL) strings 169 keep(x, pred) generalization of built-in func filter 170 kept(x, pred) same as func keep 171 links(x) auto-detect all hyperlink-like (HTTP/HTTPS) substrings 172 mapped(x, f) same as func each 173 number(x) try to parse as an int, on failure try to parse as a float 174 numbers(x) auto-detect all numbers in the value given 175 numstats(x) calculate various `single-pass` numeric stats 176 once(x, y=None) avoid returning the same value more than once; stateful func 177 pick(x, *what) keep only the keys given; works on dicts, or dict-sequences 178 picked(x, *what) same a func pick 179 plain(s) ignore ANSI-style sequences in strings 180 quoted(s, q='"') surround a string with the (optional) quoting-symbol given 181 recover(*args) recover from exceptions with a fallback value 182 reject(x, pred) generalization of built-in func filter, with opposite logic 183 since(x, y) ignore items before the one given; for strings and sequences 184 sincefinal(x, y) backward counterpart of func since 185 sincelast(x, y) same as func sincefinal 186 split(x, y) split string by separator; split sequence into several ones 187 squeeze(s) strip/trim a string, squishing inner runs of spaces 188 stround(x, d=6) format numbers into decimal-number strings 189 tally(x, ?by) count/tally values, using an optional transformation func 190 tallied(x, ?by) same as func tally 191 trap(x, f=None) try running a func, handing exceptions to a fallback func 192 trycall(*args) same as func recover 193 unique(x) same as func dedup 194 uniqued(x) same as func dedup 195 unjson(x, f=None) same as func dejson 196 unquoted(s) ignore surrounding quotes, if present 197 until(x, y) ignore items after the one given; for strings and sequences 198 untilfinal(x, y) backward counterpart of func until 199 untillast(x, y) same as func untilfinal 200 wait(seconds, x) wait the given number of seconds, before returning a value 201 wat(*args) What Are These (wat) shows help/doc messages for funcs 202 203 204 Examples 205 206 # numbers from 0 to 5, each on its own output line; no input is read/used 207 tl = 'range(6)' 208 209 # all powers up to the 4th, using each input line auto-parsed into a `float` 210 tl = 'range(1, 6)' | tl '(float(l)**p for p in range(1, 4+1))' 211 212 # separate input lines with an empty line between each; global var `empty` 213 # can be used to avoid bothering with nested shell-quoting 214 tl = 'range(6)' | tl '["", l] if i > 0 else l' 215 216 # keep only the last 2 lines from the input 217 tl = 'range(1, 6)' | tl -all 'lines[-2:]' 218 219 # join input lines into tab-separated lines of up to 3 items each; global 220 # var named `tab` can be used to avoid bothering with nested shell-quoting 221 tl = 'range(1, 8)' | tl -all '("\\t".join(c) for c in chunk(lines, 3))' 222 223 # ignore all lines before the first one with just a '5' in it 224 tl = 'range(8)' | tl -all 'since(lines, "5")' 225 226 # ignore errors/exceptions, in favor of the original lines/values 227 tl = '("abc", "123")' | tl 'safe(lambda: 2 * float(line), line)' 228 229 # ignore errors/exceptions, calling a fallback func with the exception 230 tl = '("abc", "123")' | tl 'safe(lambda: 2 * float(line), lambda e: str(e))' 231 232 # filtering lines out via None values 233 head -c 1024 /dev/urandom | strings | tl 'l if len(l) < 20 else None' 234 235 # boolean-valued results are concise ways to filter lines out 236 head -c 1024 /dev/urandom | strings | tl 'len(l) < 20' 237 238 # function/callable results are automatically called on the current line 239 head -c 1024 /dev/urandom | strings | tl len 240 ''' 241 242 243 from sys import argv, exit, stderr, stdin, stdout 244 245 246 if __name__ != '__main__': 247 print('don\'t import this script, run it directly instead', file=stderr) 248 exit(1) 249 250 # no args or a leading help-option arg means show the help message and quit 251 if len(argv) < 2 or argv[1] in ('-h', '--h', '-help', '--help'): 252 from sys import exit, stderr 253 print(info.strip(), file=stderr) 254 exit(0) 255 256 257 from io import StringIO, TextIOWrapper 258 259 from typing import \ 260 AbstractSet, Annotated, Any, AnyStr, \ 261 AsyncContextManager, AsyncGenerator, AsyncIterable, AsyncIterator, \ 262 Awaitable, BinaryIO, ByteString, Callable, cast, \ 263 ClassVar, Collection, Container, \ 264 ContextManager, Coroutine, Deque, Dict, Final, \ 265 final, ForwardRef, FrozenSet, Generator, Generic, get_args, get_origin, \ 266 get_type_hints, Hashable, IO, ItemsView, \ 267 Iterable, Iterator, KeysView, List, Literal, Mapping, \ 268 MappingView, Match, MutableMapping, MutableSequence, MutableSet, \ 269 NamedTuple, NewType, no_type_check, no_type_check_decorator, \ 270 NoReturn, Optional, overload, \ 271 Protocol, Reversible, \ 272 runtime_checkable, Sequence, Set, Sized, SupportsAbs, \ 273 SupportsBytes, SupportsComplex, SupportsFloat, SupportsIndex, \ 274 SupportsInt, SupportsRound, Text, TextIO, Tuple, Type, \ 275 TypedDict, TypeVar, \ 276 TYPE_CHECKING, Union, ValuesView 277 try: 278 from typing import \ 279 assert_never, assert_type, clear_overloads, Concatenate, \ 280 dataclass_transform, get_overloads, is_typeddict, LiteralString, \ 281 Never, NotRequired, ParamSpec, ParamSpecArgs, ParamSpecKwargs, \ 282 Required, reveal_type, Self, TypeAlias, TypeGuard, TypeVarTuple, \ 283 Unpack 284 from typing import \ 285 AwaitableGenerator, override, TypeAliasType, type_check_only 286 except Exception: 287 pass 288 289 290 def conforms(x: Any) -> bool: 291 ''' 292 Check if a value is JSON-compatible, which includes checking values 293 recursively, in case of composite/nestable values. 294 ''' 295 296 if x is None or isinstance(x, (bool, int, str)): 297 return True 298 if isinstance(x, float): 299 return not (isnan(x) or isinf(x)) 300 if isinstance(x, (list, tuple)): 301 return all(conforms(e) for e in x) 302 if isinstance(x, dict): 303 return all(conforms(k) and conforms(v) for k, v in x.items()) 304 return False 305 306 307 def seems_url(s: str) -> bool: 308 protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') 309 return any(s.startswith(p) for p in protocols) 310 311 312 def disabled_exec(*args, **kwargs) -> None: 313 _ = args 314 _ = kwargs 315 raise Exception('built-in func `exec` is disabled') 316 317 318 def fix_value(x: Any, default: Any) -> Any: 319 'Adapt a value so it can be output.' 320 321 # true shows the current line as the current output; presumably 322 # this is the result of calling a `condition-like` expression 323 if x is True: 324 return default 325 326 # null and false show no output for the current input line 327 if x is False: 328 return None 329 330 if x is type: 331 return type(default).__name__ 332 333 # if expression results in a func, auto-call it with the original data 334 if callable(x) and not isinstance(x, Iterable): 335 c = required_arg_count(x) 336 if c == 1: 337 x = x(default) 338 else: 339 m = f'func auto-call only works with 1-arg funcs (func wanted {c})' 340 raise Exception(m) 341 342 if x is None or isinstance(x, (bool, int, float, str)): 343 return x 344 345 rec = fix_value 346 347 if isinstance(x, dict): 348 return { 349 rec(k, default): rec(v, default) for k, v in x.items() if not 350 (isinstance(k, Skip) or isinstance(v, Skip)) 351 } 352 if isinstance(x, Iterable): 353 return tuple(rec(e, default) for e in x if not isinstance(e, Skip)) 354 355 if isinstance(x, Dottable): 356 return rec(x.__dict__, default) 357 if isinstance(x, DotCallable): 358 return rec(x.value, default) 359 360 if isinstance(x, Exception): 361 raise x 362 363 return None if isinstance(x, Skip) else str(x) 364 365 366 def show_value(w, x: Any) -> None: 367 'Helper func used by func show_result.' 368 369 # null shows no output for the current input line 370 if x is None or isinstance(x, Skip): 371 return 372 373 if isinstance(x, dict): 374 dump(x, w, separators=(', ', ': '), allow_nan=False, indent=None) 375 w.write('\n') 376 w.flush() 377 elif isinstance(x, (bytes, bytearray)): 378 w.write(x) 379 w.flush() 380 elif isinstance(x, Iterable) and not isinstance(x, str): 381 dump(x, w, separators=(', ', ': '), allow_nan=False, indent=None) 382 w.write('\n') 383 w.flush() 384 elif isinstance(x, DotCallable): 385 print(x.value, file=w, flush=True) 386 else: 387 print(x, file=w, flush=True) 388 389 390 def show_result(w, x: Any) -> None: 391 if isinstance(x, (dict, str)): 392 show_value(w, x) 393 elif isinstance(x, Iterable): 394 for e in x: 395 if isinstance(e, Exception): 396 raise e 397 show_value(w, e) 398 else: 399 show_value(w, x) 400 401 402 def make_open_utf8(open: Callable) -> Callable: 403 'Restrict the file-open func to a read-only utf-8 file-open func.' 404 def open_utf8_readonly(name: str): 405 'A UTF-8 read-only file-open func overriding the built-in open func.' 406 return open(name, encoding='utf-8') 407 return open_utf8_readonly 408 409 open_utf8 = make_open_utf8(open) 410 open = open_utf8 411 412 413 def loop_lines_inputs(r, inputs: List[str], doing: Callable) -> None: 414 ''' 415 Act on multiple named inputs line-by-line, via the func given; when 416 not given any named inputs, the default reader given is used instead. 417 ''' 418 419 main_input: List[str] = [] 420 got_main_input = False 421 dashes = inputs.count('-') 422 423 if any(seems_url(e) for e in inputs): 424 from urllib.request import urlopen 425 426 def _adapt_lines(src) -> Iterable[str]: 427 for j, line in enumerate(src): 428 if j == 0: 429 line = line.lstrip('\xef\xbb\xbf') 430 yield line.rstrip('\r\n').rstrip('\n') 431 432 def _hold_adapt_lines(src) -> Iterable[str]: 433 for e in _adapt_lines(src): 434 main_input.append(e) 435 yield e 436 437 for path in inputs: 438 if path == '-': 439 if dashes == 1: 440 doing(_adapt_lines(r)) 441 continue 442 443 if not got_main_input: 444 doing(_hold_adapt_lines(r)) 445 got_main_input = True 446 else: 447 doing(_adapt_lines(main_input)) 448 continue 449 450 if seems_url(path): 451 with urlopen(path) as inp: 452 with TextIOWrapper(inp, encoding='utf-8') as txt: 453 doing(_adapt_lines(txt)) 454 continue 455 456 with open_utf8(path) as inp: 457 doing(_adapt_lines(inp)) 458 459 if len(inputs) == 0: 460 doing(_adapt_lines(r)) 461 462 463 def loop_whole_inputs(r, inputs: List[str], doing: Callable) -> None: 464 ''' 465 Act on multiple named inputs, read as whole strings, via the func given; 466 when not given any named inputs, the default reader given is used instead. 467 ''' 468 469 main_input: List[str] = [] 470 got_main_input = False 471 dashes = inputs.count('-') 472 473 if any(seems_url(e) for e in inputs): 474 from urllib.request import urlopen 475 476 for path in inputs: 477 if path == '-': 478 if dashes == 1: 479 doing(r.read()) 480 continue 481 482 if not got_main_input: 483 main_input = r.read() 484 got_main_input = True 485 doing(main_input) 486 continue 487 488 if seems_url(path): 489 with urlopen(path) as inp: 490 with TextIOWrapper(inp, encoding='utf-8') as txt: 491 doing(txt.read()) 492 continue 493 494 with open_utf8(path) as inp: 495 doing(inp.read()) 496 497 if len(inputs) == 0: 498 doing(r.read()) 499 500 501 def main_whole_strings(out, r, expression, inputs) -> None: 502 def _each_string(out, src, expression: Any) -> None: 503 # `comprehension` expressions seem to ignore local variables: even 504 # lambda-based workarounds fail 505 global s, t, text, v, value, w, whole, _ 506 507 s = t = text = v = value = w = whole = src 508 res = eval(expression) 509 res = fix_value(res, src) 510 show_result(out, res) 511 _ = res 512 513 loop_whole_inputs(r, inputs, lambda s: _each_string(out, s, expression)) 514 515 516 def main_each_line(w, r, expression, inputs) -> None: 517 def _each_line(w, src, expression: Any) -> None: 518 # `comprehension` expressions seem to ignore local variables: even 519 # lambda-based workarounds fail 520 global i, nr, previous, prev, line, l, line, _ 521 522 previous = '' 523 prev = previous 524 525 for line in src: 526 l = line 527 res = eval(expression) 528 res = fix_value(res, line) 529 show_result(w, res) 530 i += 1 531 nr += 1 532 previous = line 533 prev = previous 534 _ = res 535 536 loop_lines_inputs(r, inputs, lambda r: _each_line(w, r, expression)) 537 538 539 def main_each_block(w, r, expression, inputs) -> None: 540 def _each_block(w, r, expression) -> None: 541 # `comprehension` expressions seem to ignore local variables: even 542 # lambda-based workarounds fail 543 global i, nr 544 global previous, prev, lines, block, par, para, paragraph, _ 545 546 for item in paragraphize(r): 547 lines = block = par = para = paragraph = item 548 res = eval(expression) 549 if isinstance(res, Skip): 550 previous = data 551 prev = previous 552 i += 1 553 nr += 1 554 continue 555 556 res = fix_value(res, lines) 557 show_result(w, res) 558 i += 1 559 nr += 1 560 prev = previous = lines 561 _ = res 562 563 loop_lines_inputs(r, inputs, lambda r: _each_block(w, r, expression)) 564 565 566 def main_all_lines(w, r, expression, inputs) -> None: 567 # `comprehension` expressions seem to ignore local variables: even 568 # lambda-based workarounds fail 569 global line, lines, data, values, d, l, v, dat, val 570 571 def _all_lines(w, r, expression) -> None: 572 # `comprehension` expressions seem to ignore local variables: even 573 # lambda-based workarounds fail 574 global lines, line, l 575 576 for e in r: 577 line = l = e 578 lines.append(line) 579 580 lines = [] 581 line = l = '' 582 loop_lines_inputs(r, inputs, lambda r: _all_lines(w, r, expression)) 583 data = values = d = v = dat = val = lines 584 res = eval(expression) 585 res = fix_value(res, lines) 586 show_result(w, res) 587 588 589 def main_all_bytes(w, r, expression, inputs) -> None: 590 # `comprehension` expressions seem to ignore local variables: even 591 # lambda-based workarounds fail 592 global data, values, d, v, dat, val 593 data = values = d = v = dat = val = r.buffer.read() 594 res = eval(expression) 595 res = fix_value(res, data) 596 show_result(w, res) 597 598 599 def main_no_input(w, r, expression, inputs) -> None: 600 res = eval(expression) 601 fix = lambda x: fix_value(x, None) 602 f = str if res is None or isinstance(res, bool) else fix 603 res = f(res) 604 show_result(w, res) 605 606 607 def main_json_lines(w, r, expression, inputs) -> None: 608 def _jsonl2json(w, src, expression: Any) -> None: 609 # `comprehension` expressions seem to ignore local variables: even 610 # lambda-based workarounds fail 611 global i, nr 612 global line, l, data, d, value, v, dat, val, prev, previous, _ 613 614 previous = None 615 prev = previous 616 617 for line in src: 618 if emptyish_re.match(line) or commented_re.match(line): 619 continue 620 l = line 621 622 data = value = d = v = dat = val = loads(line) 623 res = eval(expression) 624 625 if isinstance(res, Skip): 626 previous = data 627 prev = previous 628 i += 1 629 nr += 1 630 continue 631 res = fix_value(res, data) 632 633 if callable(res): 634 res = res(data) 635 if not conforms(res): 636 res = conform(res) 637 dump(res, w) 638 _ = res 639 w.write('\n') 640 641 previous = data 642 prev = previous 643 i += 1 644 nr += 1 645 646 loop_lines_inputs(r, inputs, lambda r: _jsonl2json(w, r, expression)) 647 648 649 # opts2modes simplifies option-handling in func main 650 opts2modes = { 651 '=': 'no-input', 652 '-nil': 'no-input', 653 '-no-input': 'no-input', 654 '-noinput': 'no-input', 655 '-none': 'no-input', 656 '-None': 'no-input', 657 '-null': 'no-input', 658 '-null-input': 'no-input', 659 '-nullinput': 'no-input', 660 '--n': 'no-input', 661 '--nil': 'no-input', 662 '--no-input': 'no-input', 663 '--noinput': 'no-input', 664 '--none': 'no-input', 665 '--None': 'no-input', 666 '--null': 'no-input', 667 '--null-input': 'no-input', 668 '--nullinput': 'no-input', 669 '-a': 'all-lines', 670 '-all': 'all-lines', 671 '-lines': 'all-lines', 672 '--a': 'all-lines', 673 '--all': 'all-lines', 674 '--lines': 'all-lines', 675 '-b': 'each-block', 676 '-blocks': 'each-block', 677 '-g': 'each-block', 678 '-groups': 'each-block', 679 '-p': 'each-block', 680 '-par': 'each-block', 681 '-para': 'each-block', 682 '-paragraphs': 'each-block', 683 '--b': 'each-block', 684 '--blocks': 'each-block', 685 '--g': 'each-block', 686 '--groups': 'each-block', 687 '--p': 'each-block', 688 '--par': 'each-block', 689 '--para': 'each-block', 690 '--paragraphs': 'each-block', 691 '-bytes': 'bytes', 692 '--bytes': 'bytes', 693 '-jl': 'json-lines', 694 '-jsonl': 'json-lines', 695 '-jsonlines': 'json-lines', 696 '-json-lines': 'json-lines', 697 '--jl': 'json-lines', 698 '--jsonl': 'json-lines', 699 '--jsonlines': 'json-lines', 700 '--json-lines': 'json-lines', 701 '-s': 'whole-strings', 702 '-str': 'whole-strings', 703 '-string': 'whole-strings', 704 '--s': 'whole-strings', 705 '--str': 'whole-strings', 706 '--string': 'whole-strings', 707 '-w': 'whole-strings', 708 '-whole': 'whole-strings', 709 '--w': 'whole-strings', 710 '--whole': 'whole-strings', 711 } 712 713 714 def blue(s: Any) -> str: 715 'Blue-style a plain string via ANSI-style sequences.' 716 return f'\x1b[38;5;26m{s}\x1b[0m' 717 718 def blueback(s: Any) -> str: 719 'Blue-background-style a plain string via ANSI-style sequences.' 720 return f'\x1b[48;5;26m\x1b[38;5;255m{s}\x1b[0m' 721 722 bluebg = blueback 723 724 def bold(s: Any) -> str: 725 'Bold-style a plain string via ANSI-style sequences.' 726 return f'\x1b[1m{s}\x1b[0m' 727 728 def gbm(s: str, good: Any = False, bad: Any = False, meh: Any = False) -> str: 729 ''' 730 Good, Bad, Meh ANSI-styles a plain string via ANSI-style sequences, 731 according to 1..3 conditions given as boolean(ish) values: these are 732 checked in order, so the first truish one wins. 733 ''' 734 735 if good: 736 return green(s) 737 if bad: 738 return red(s) 739 if meh: 740 return gray(s) 741 return s 742 743 def gray(s: Any) -> str: 744 'Gray-style a plain string via ANSI-style sequences.' 745 return f'\x1b[38;5;248m{s}\x1b[0m' 746 747 def grayback(s: Any) -> str: 748 'Gray-background-style a plain string via ANSI-style sequences.' 749 return f'\x1b[48;5;253m{s}\x1b[0m' 750 751 graybg = grayback 752 753 def green(s: Any) -> str: 754 'Green-style a plain string via ANSI-style sequences.' 755 return f'\x1b[38;5;29m{s}\x1b[0m' 756 757 def greenback(s: Any) -> str: 758 'Green-background-style a plain string via ANSI-style sequences.' 759 return f'\x1b[48;5;29m\x1b[38;5;255m{s}\x1b[0m' 760 761 greenbg = greenback 762 763 def highlight(s: Any) -> str: 764 'Highlight/reverse-style a plain string via ANSI-style sequences.' 765 return f'\x1b[7m{s}\x1b[0m' 766 767 hilite = highlight 768 769 def magenta(s: Any) -> str: 770 'Magenta-style a plain string via ANSI-style sequences.' 771 return f'\x1b[38;5;165m{s}\x1b[0m' 772 773 def magentaback(s: Any) -> str: 774 'Magenta-background-style a plain string via ANSI-style sequences.' 775 return f'\x1b[48;5;165m\x1b[38;5;255m{s}\x1b[0m' 776 777 magback = magentaback 778 magbg = magentaback 779 magentabg = magentaback 780 781 def orange(s: Any) -> str: 782 'Orange-style a plain string via ANSI-style sequences.' 783 return f'\x1b[38;5;166m{s}\x1b[0m' 784 785 def orangeback(s: Any) -> str: 786 'Orange-background-style a plain string via ANSI-style sequences.' 787 return f'\x1b[48;5;166m\x1b[38;5;255m{s}\x1b[0m' 788 789 orangebg = orangeback 790 orback = orangeback 791 orbg = orangeback 792 793 def purple(s: Any) -> str: 794 'Purple-style a plain string via ANSI-style sequences.' 795 return f'\x1b[38;5;99m{s}\x1b[0m' 796 797 def purpleback(s: Any) -> str: 798 'Purple-background-style a plain string via ANSI-style sequences.' 799 return f'\x1b[48;5;99m\x1b[38;5;255m{s}\x1b[0m' 800 801 purback = purpleback 802 purbg = purpleback 803 purplebg = purpleback 804 805 def red(s: Any) -> str: 806 'Red-style a plain string via ANSI-style sequences.' 807 return f'\x1b[38;5;1m{s}\x1b[0m' 808 809 def redback(s: Any) -> str: 810 'Red-background-style a plain string via ANSI-style sequences.' 811 return f'\x1b[48;5;1m\x1b[38;5;255m{s}\x1b[0m' 812 813 redbg = redback 814 815 def underline(s: Any) -> str: 816 'Underline-style a plain string via ANSI-style sequences.' 817 return f'\x1b[4m{s}\x1b[0m' 818 819 # def blue(s): 820 # return f'\x1b[38;2;0;95;215m{s}\x1b[0m' 821 822 # def blueback(s): 823 # return f'\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m{s}\x1b[0m' 824 825 # bluebg = blueback 826 827 # def bold(s): 828 # return f'\x1b[1m{s}\x1b[0m' 829 830 # bolded = bold 831 832 # def gray(s): 833 # return f'\x1b[38;2;168;168;168m{s}\x1b[0m' 834 835 # def grayback(s): 836 # return f'\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m{s}\x1b[0m' 837 838 # def green(s): 839 # return f'\x1b[38;2;0;135;95m{s}\x1b[0m' 840 841 # def greenback(s): 842 # return f'\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m{s}\x1b[0m' 843 844 # def highlight(s): 845 # return f'\x1b[7m{s}\x1b[0m' 846 847 # hilite = highlight 848 849 # def orange(s): 850 # return f'\x1b[38;2;215;95;0m{s}\x1b[0m' 851 852 # def orangeback(s): 853 # return f'\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m{s}\x1b[0m' 854 855 # def purple(s): 856 # return f'\x1b[38;2;135;95;255m{s}\x1b[0m' 857 858 # def purpleback(s): 859 # return f'\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m{s}\x1b[0m' 860 861 # def red(s): 862 # return f'\x1b[38;2;204;0;0m{s}\x1b[0m' 863 864 # def redback(s): 865 # return f'\x1b[38;2;204;0;0m\x1b[38;2;238;238;238m{s}\x1b[0m' 866 867 # def underline(s): 868 # return f'\x1b[4m{s}\x1b[0m' 869 870 # underlined = underline 871 872 873 def realign(lines: List[str], gap: int = 2) -> Iterable: 874 ''' 875 Pad lines so that their items align across/vertically: extra padding 876 is put between such `columns`, using 2 spaces by default. 877 ''' 878 879 widths: List[int] = [] 880 for l in lines: 881 items = awk_sep_re.split(l.strip()) 882 while len(widths) < len(items): 883 widths.append(0) 884 for i, s in enumerate(items): 885 widths[i] = max(widths[i], len(s)) 886 887 sb = StringIO() 888 gap = max(gap, 0) 889 890 for l in lines: 891 sb.truncate(0) 892 sb.seek(0) 893 894 padding = 0 895 items = awk_sep_re.split(l.strip()) 896 for s, w in zip(items, widths): 897 sb.write(padding * ' ') 898 sb.write(s) 899 padding = max(w - len(s), 0) + gap 900 901 yield sb.getvalue() 902 903 904 def stop_normal(x: Any, exit_code: int = 0) -> NoReturn: 905 show_result(stdout, fix_value(x, None)) 906 exit(exit_code) 907 908 909 def stop_json(x: Any, exit_code: int = 0) -> NoReturn: 910 dump(x, stdout) 911 stdout.write('\n') 912 stdout.flush() 913 exit(exit_code) 914 915 916 from base64 import \ 917 standard_b64encode, standard_b64decode, \ 918 standard_b64encode as base64bytes, standard_b64decode as debase64bytes 919 920 from collections import \ 921 ChainMap, Counter, defaultdict, deque, namedtuple, OrderedDict, \ 922 UserDict, UserList, UserString 923 924 from copy import copy, deepcopy 925 926 from datetime import \ 927 MAXYEAR, MINYEAR, date, datetime, time, timedelta, timezone, tzinfo 928 try: 929 from datetime import now, UTC 930 except Exception: 931 now = lambda: datetime(2000, 1, 1).now() 932 933 from decimal import Decimal, getcontext 934 935 from difflib import \ 936 context_diff, diff_bytes, Differ, get_close_matches, HtmlDiff, \ 937 IS_CHARACTER_JUNK, IS_LINE_JUNK, ndiff, restore, SequenceMatcher, \ 938 unified_diff 939 940 from fractions import Fraction 941 942 import functools 943 from functools import \ 944 cache, cached_property, cmp_to_key, get_cache_token, lru_cache, \ 945 namedtuple, partial, partialmethod, recursive_repr, reduce, \ 946 singledispatch, singledispatchmethod, total_ordering, update_wrapper, \ 947 wraps 948 949 from glob import glob, iglob 950 951 try: 952 from graphlib import CycleError, TopologicalSorter 953 except Exception: 954 pass 955 956 from hashlib import \ 957 file_digest, md5, pbkdf2_hmac, scrypt, sha1, sha224, sha256, sha384, \ 958 sha512 959 960 from inspect import getfullargspec, getsource 961 962 import itertools 963 from itertools import \ 964 accumulate, chain, combinations, combinations_with_replacement, \ 965 compress, count, cycle, dropwhile, filterfalse, groupby, islice, \ 966 permutations, product, repeat, starmap, takewhile, tee, zip_longest 967 try: 968 from itertools import pairwise 969 from itertools import batched 970 except Exception: 971 pass 972 973 from json import dump, dumps, loads 974 975 import math 976 Math = math 977 from math import \ 978 acos, acosh, asin, asinh, atan, atan2, atanh, ceil, comb, \ 979 copysign, cos, cosh, degrees, dist, e, erf, erfc, exp, expm1, \ 980 fabs, factorial, floor, fmod, frexp, fsum, gamma, gcd, hypot, inf, \ 981 isclose, isfinite, isinf, isnan, isqrt, lcm, ldexp, lgamma, log, \ 982 log10, log1p, log2, modf, nan, nextafter, perm, pi, pow, prod, \ 983 radians, remainder, sin, sinh, sqrt, tan, tanh, tau, trunc, ulp 984 try: 985 from math import cbrt, exp2 986 except Exception: 987 pass 988 989 power = pow 990 991 import operator 992 993 from pathlib import Path 994 995 from pprint import \ 996 isreadable, isrecursive, pformat, pp, pprint, PrettyPrinter, saferepr 997 998 from random import \ 999 betavariate, choice, choices, expovariate, gammavariate, gauss, \ 1000 getrandbits, getstate, lognormvariate, normalvariate, paretovariate, \ 1001 randbytes, randint, random, randrange, sample, seed, setstate, \ 1002 shuffle, triangular, uniform, vonmisesvariate, weibullvariate 1003 1004 compile_py = compile # keep built-in func compile for later 1005 from re import compile as compile_uncached, Pattern, IGNORECASE 1006 1007 import statistics 1008 from statistics import \ 1009 bisect_left, bisect_right, fmean, \ 1010 geometric_mean, harmonic_mean, mean, median, \ 1011 median_grouped, median_high, median_low, mode, multimode, pstdev, \ 1012 pvariance, quantiles, stdev, variance 1013 try: 1014 from statistics import \ 1015 correlation, covariance, linear_regression, mul 1016 except Exception: 1017 pass 1018 1019 import string 1020 from string import \ 1021 Formatter, Template, ascii_letters, ascii_lowercase, ascii_uppercase, \ 1022 capwords, digits, hexdigits, octdigits, printable, punctuation, \ 1023 whitespace 1024 1025 alphabet = ascii_letters 1026 letters = ascii_letters 1027 lowercase = ascii_lowercase 1028 uppercase = ascii_uppercase 1029 1030 from textwrap import dedent, fill, indent, shorten, wrap 1031 1032 from time import \ 1033 altzone, asctime, \ 1034 ctime, daylight, get_clock_info, \ 1035 gmtime, localtime, mktime, monotonic, monotonic_ns, perf_counter, \ 1036 perf_counter_ns, process_time, process_time_ns, \ 1037 sleep, strftime, strptime, struct_time, thread_time, thread_time_ns, \ 1038 time, time_ns, timezone, tzname 1039 try: 1040 from time import \ 1041 clock_getres, clock_gettime, clock_gettime_ns, clock_settime, \ 1042 clock_settime_ns, pthread_getcpuclockid, tzset 1043 except Exception: 1044 pass 1045 1046 from unicodedata import \ 1047 bidirectional, category, combining, decimal, decomposition, digit, \ 1048 east_asian_width, is_normalized, lookup, mirrored, name, normalize, \ 1049 numeric 1050 1051 from urllib.parse import \ 1052 parse_qs, parse_qsl, quote, quote_from_bytes, quote_plus, unquote, \ 1053 unquote_plus, unquote_to_bytes, unwrap, urldefrag, urlencode, urljoin, \ 1054 urlparse, urlsplit, urlunparse, urlunsplit 1055 1056 1057 class Skip: 1058 'Custom type which some funcs type-check to skip values in containers.' 1059 1060 def __init__(self, *args) -> None: 1061 pass 1062 1063 1064 # skip is a ready-to-use value which some funcs filter against: this way 1065 # filtering values becomes a special case of transforming values 1066 skip = Skip() 1067 1068 # re_cache is used by custom func compile to cache previously-compiled 1069 # regular-expressions, which makes them quicker to (re)use in formulas 1070 re_cache: Dict[str, Pattern] = {} 1071 1072 # ire_cache is like re_cache, except it's for case-insensitive regexes 1073 ire_cache: Dict[str, Pattern] = {} 1074 1075 # ansi_style_re detects the most commonly-used ANSI-style sequences, and 1076 # is used in func plain 1077 ansi_style_re = compile_uncached('''\x1b\[([0-9;]+m|[0-9]*[A-HJKST])''') 1078 1079 # number_re detects numbers, and is used in func numbers 1080 number_re = compile_uncached('''\W(-?[0-9]+(\.[0-9]*)?)\W''') 1081 1082 # link_re detects web links, and is used in func links 1083 link_re_src = 'https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*' 1084 link_re = compile_uncached(link_re_src) 1085 1086 # paddable_tab_re detects single tabs and possible runs of spaces around 1087 # them, and is used in func squeeze 1088 paddable_tab_re = compile_uncached(' *\t *') 1089 1090 # seen remembers values already given to func `once` 1091 seen = set() 1092 1093 # commented_re detects strings/lines which start as unix-style comments 1094 commented_re = compile_uncached('^ *#') 1095 1096 # emptyish_re detects empty/emptyish strings/lines, the latter being strings 1097 # with only spaces in them 1098 emptyish_re = compile_uncached('^ *\r?\n?$') 1099 1100 # spaces_re detects runs of 2 or more spaces, and is used in func squeeze 1101 spaces_re = compile_uncached(' +') 1102 1103 # awk_sep_re splits like AWK does by default, and is used in func fields 1104 awk_sep_re = compile_uncached(' *\t *| +') 1105 1106 1107 # some convenience aliases to commonly-used values 1108 1109 false = False 1110 true = True 1111 nil = None 1112 nihil = None 1113 none = None 1114 null = None 1115 s = '' 1116 1117 months = [ 1118 'January', 'February', 'March', 'April', 'May', 'June', 1119 'July', 'August', 'September', 'October', 'November', 'December', 1120 ] 1121 1122 monweek = [ 1123 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 1124 'Saturday', 'Sunday', 1125 ] 1126 1127 sunweek = [ 1128 'Sunday', 1129 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 1130 ] 1131 1132 phy = { 1133 'kilo': 1_000, 1134 'mega': 1_000_000, 1135 'giga': 1_000_000_000, 1136 'tera': 1_000_000_000_000, 1137 'peta': 1_000_000_000_000_000, 1138 'exa': 1_000_000_000_000_000_000, 1139 'zetta': 1_000_000_000_000_000_000_000, 1140 1141 'c': 299_792_458, 1142 'kcd': 683, 1143 'na': 602214076000000000000000, 1144 1145 'femto': 1e-15, 1146 'pico': 1e-12, 1147 'nano': 1e-9, 1148 'micro': 1e-6, 1149 'milli': 1e-3, 1150 1151 'e': 1.602176634e-19, 1152 'f': 96_485.33212, 1153 'h': 6.62607015e-34, 1154 'k': 1.380649e-23, 1155 'mu': 1.66053906892e-27, 1156 1157 'ge': 9.7803267715, 1158 'gn': 9.80665, 1159 } 1160 1161 physics = phy 1162 1163 # using literal strings on the cmd-line is often tricky/annoying: some of 1164 # these aliases can help get around multiple levels of string-quoting; no 1165 # quotes are needed as the script will later make these values accessible 1166 # via the property/dot syntax 1167 sym = { 1168 'amp': '&', 1169 'ampersand': '&', 1170 'ansiclear': '\x1b[0m', 1171 'ansinormal': '\x1b[0m', 1172 'ansireset': '\x1b[0m', 1173 'apo': '\'', 1174 'apos': '\'', 1175 'ast': '*', 1176 'asterisk': '*', 1177 'at': '@', 1178 'backquote': '`', 1179 'backslash': '\\', 1180 'backtick': '`', 1181 'ball': '●', 1182 'bang': '!', 1183 'bigsigma': 'Σ', 1184 'block': '█', 1185 'bquo': '`', 1186 'bquote': '`', 1187 'bslash': '\\', 1188 'btick': '`', 1189 'bullet': '•', 1190 'caret': '^', 1191 'cdot': '·', 1192 'circle': '●', 1193 'colon': ':', 1194 'comma': ',', 1195 'cr': '\r', 1196 'crlf': '\r\n', 1197 'cross': '×', 1198 'cs': ', ', 1199 'dash': '—', 1200 'dollar': '$', 1201 'dot': '.', 1202 'dquo': '"', 1203 'dquote': '"', 1204 'emark': '!', 1205 'emdash': '—', 1206 'empty': '', 1207 'endash': '–', 1208 'eq': '=', 1209 'et': '&', 1210 'euro': '€', 1211 'ge': '≥', 1212 'geq': '≥', 1213 'gt': '>', 1214 'hellip': '…', 1215 'hole': '○', 1216 'hyphen': '-', 1217 'infinity': '∞', 1218 'lcurly': '{', 1219 'ldquo': '“', 1220 'ldquote': '“', 1221 'le': '≤', 1222 'leq': '≤', 1223 'lf': '\n', 1224 'lt': '<', 1225 'mdash': '—', 1226 'mdot': '·', 1227 'miniball': '•', 1228 'minus': '-', 1229 'ndash': '–', 1230 'neq': '≠', 1231 'perc': '%', 1232 'percent': '%', 1233 'period': '.', 1234 'plus': '+', 1235 'qmark': '?', 1236 'que': '?', 1237 'rcurly': '}', 1238 'rdquo': '”', 1239 'rdquote': '”', 1240 'sball': '•', 1241 'semi': ';', 1242 'semicolon': ';', 1243 'sharp': '#', 1244 'slash': '/', 1245 'space': ' ', 1246 'square': '■', 1247 'squo': '\'', 1248 'squote': '\'', 1249 'tab': '\t', 1250 'tilde': '~', 1251 'underscore': '_', 1252 'uscore': '_', 1253 'utf8bom': '\xef\xbb\xbf', 1254 'utf16be': '\xfe\xff', 1255 'utf16le': '\xff\xfe', 1256 } 1257 1258 symbols = sym 1259 1260 units = { 1261 'cup2l': 0.23658824, 1262 'floz2l': 0.0295735295625, 1263 'floz2ml': 29.5735295625, 1264 'ft2m': 0.3048, 1265 'gal2l': 3.785411784, 1266 'in2cm': 2.54, 1267 'lb2kg': 0.45359237, 1268 'mi2km': 1.609344, 1269 'mpg2kpl': 0.425143707, 1270 'nmi2km': 1.852, 1271 'oz2g': 28.34952312, 1272 'psi2pa': 6894.757293168, 1273 'ton2kg': 907.18474, 1274 'yd2m': 0.9144, 1275 1276 'mol': 602214076000000000000000, 1277 'mole': 602214076000000000000000, 1278 1279 'hour': 3_600, 1280 'day': 86_400, 1281 'week': 604_800, 1282 1283 'hr': 3_600, 1284 'wk': 604_800, 1285 1286 'kb': 1024, 1287 'mb': 1024**2, 1288 'gb': 1024**3, 1289 'tb': 1024**4, 1290 'pb': 1024**5, 1291 } 1292 1293 # some convenience aliases to various funcs from the python stdlib 1294 geomean = geometric_mean 1295 harmean = harmonic_mean 1296 sd = stdev 1297 popsd = pstdev 1298 var = variance 1299 popvar = pvariance 1300 randbeta = betavariate 1301 randexp = expovariate 1302 randgamma = gammavariate 1303 randlognorm = lognormvariate 1304 randnorm = normalvariate 1305 randweibull = weibullvariate 1306 1307 capitalize = str.capitalize 1308 casefold = str.casefold 1309 center = str.center 1310 # count = str.count 1311 decode = bytes.decode 1312 encode = str.encode 1313 endswith = str.endswith 1314 expandtabs = str.expandtabs 1315 find = str.find 1316 format = str.format 1317 index = str.index 1318 isalnum = str.isalnum 1319 isalpha = str.isalpha 1320 isascii = str.isascii 1321 isdecimal = str.isdecimal 1322 isdigit = str.isdigit 1323 isidentifier = str.isidentifier 1324 islower = str.islower 1325 isnumeric = str.isnumeric 1326 isprintable = str.isprintable 1327 isspace = str.isspace 1328 istitle = str.istitle 1329 isupper = str.isupper 1330 # join = str.join 1331 ljust = str.ljust 1332 lower = str.lower 1333 lowered = str.lower 1334 lstrip = str.lstrip 1335 maketrans = str.maketrans 1336 partition = str.partition 1337 removeprefix = str.removeprefix 1338 removesuffix = str.removesuffix 1339 replace = str.replace 1340 rfind = str.rfind 1341 rindex = str.rindex 1342 rjust = str.rjust 1343 rpartition = str.rpartition 1344 rsplit = str.rsplit 1345 rstrip = str.rstrip 1346 # split = str.split 1347 splitlines = str.splitlines 1348 startswith = str.startswith 1349 strip = str.strip 1350 swapcase = str.swapcase 1351 title = str.title 1352 translate = str.translate 1353 upper = str.upper 1354 uppered = str.upper 1355 zfill = str.zfill 1356 1357 every = all 1358 rev = reversed 1359 reverse = reversed 1360 some = any 1361 1362 length = len 1363 1364 blowtabs = str.expandtabs 1365 hasprefix = str.startswith 1366 hassuffix = str.endswith 1367 ltrim = str.lstrip 1368 stripstart = str.lstrip 1369 trimspace = str.strip 1370 trimstart = str.lstrip 1371 rtrim = str.rstrip 1372 stripend = str.rstrip 1373 trimend = str.rstrip 1374 stripped = str.strip 1375 trim = str.strip 1376 trimmed = str.strip 1377 trimprefix = str.removeprefix 1378 trimsuffix = str.removesuffix 1379 1380 1381 def required_arg_count(f: Callable) -> int: 1382 if isinstance(f, type): 1383 return 1 1384 1385 meta = getfullargspec(f) 1386 n = len(meta.args) 1387 if meta.defaults: 1388 n -= len(meta.defaults) 1389 return n 1390 1391 1392 def identity(x: Any) -> Any: 1393 ''' 1394 Return the value given: this is the default transformer for several 1395 higher-order funcs, which effectively keeps original items as given. 1396 ''' 1397 return x 1398 1399 idem = identity 1400 iden = identity 1401 1402 1403 def after(x: Union[str, Iterable], what: Any) -> Union[str, Iterable]: 1404 'Skip parts of strings/sequences up to the substring/value given.' 1405 return (strafter if isinstance(x, str) else itemsafter)(x, what) 1406 1407 def afterlast(x: Union[str, Iterable], what: Any) -> Union[str, Iterable]: 1408 'Skip parts of strings/sequences up to the last substring/value given.' 1409 return (strafterlast if isinstance(x, str) else itemsafterlast)(x, what) 1410 1411 afterfinal = afterlast 1412 1413 def arrayish(x: Any) -> bool: 1414 'Check if a value is array-like enough.' 1415 return isinstance(x, (list, tuple, range, Generator)) 1416 1417 isarrayish = arrayish 1418 1419 def base64(x): 1420 return base64bytes(str(x).encode()).decode() 1421 1422 def basename(s: str) -> str: 1423 'Get a filepath\'s last part, if present.' 1424 return Path(s).name 1425 1426 def before(x: Union[str, Iterable], what: Any) -> Union[str, Iterable]: 1427 'End strings/sequences right before a substring/value\'s appearance.' 1428 return (strbefore if isinstance(x, str) else itemsbefore)(x, what) 1429 1430 def beforelast(x: Union[str, Iterable], what: Any) -> Union[str, Iterable]: 1431 'End strings/sequences right before a substring/value\'s last appearance.' 1432 return (strbeforelast if isinstance(x, str) else itemsbeforelast)(x, what) 1433 1434 beforefinal = beforelast 1435 1436 def cases(x: Any, *args: Any) -> Any: 1437 ''' 1438 Simulate a switch statement on a value, using matches/result pairs from 1439 the arguments given; when given an even number of extra args, None is 1440 used as a final fallback result; when given an odd number of extra args, 1441 the last argument is used as a final `default` value, if needed. 1442 ''' 1443 1444 for i in range(0, len(args) - len(args) % 2, 2): 1445 test, res = args[i], args[i+1] 1446 if isinstance(test, (list, tuple)) and x in test: 1447 return res 1448 if isinstance(test, float) and isnan(test) and isnan(x): 1449 return res 1450 if x == test: 1451 return res 1452 return None if len(args) % 2 == 0 else args[-1] 1453 1454 switch = cases 1455 1456 def chunk(items: Iterable, chunk_size: int) -> Iterable: 1457 'Break iterable into chunks, each with up to the item-count given.' 1458 1459 if isinstance(items, str): 1460 n = len(items) 1461 while n >= chunk_size: 1462 yield items[:chunk_size] 1463 items = items[chunk_size:] 1464 n -= chunk_size 1465 if n > 0: 1466 yield items 1467 return 1468 1469 if not isinstance(chunk_size, int): 1470 raise Exception('non-integer chunk-size') 1471 if chunk_size < 1: 1472 raise Exception('non-positive chunk-size') 1473 1474 it = iter(items) 1475 while True: 1476 head = tuple(islice(it, chunk_size)) 1477 if not head: 1478 return 1479 yield head 1480 1481 chunked = chunk 1482 1483 def commented(s: str) -> bool: 1484 'Check if a string starts as a unix-style comment.' 1485 return commented_re.match(s) != None 1486 1487 iscommented = commented 1488 1489 def compile(s: str, case_sensitive: bool = True) -> Pattern: 1490 'Cached regex `compiler`, so it\'s quicker to (re)use in formulas.' 1491 1492 cache = re_cache if case_sensitive else ire_cache 1493 options = 0 if case_sensitive else IGNORECASE 1494 1495 if s in cache: 1496 return cache[s] 1497 e = compile_uncached(s, options) 1498 cache[s] = e 1499 return e 1500 1501 def compose(*what: Callable) -> Callable: 1502 def composite(x: Any) -> Any: 1503 for f in what: 1504 x = f(x) 1505 return x 1506 return composite 1507 1508 composed = compose 1509 lcompose = compose 1510 lcomposed = compose 1511 1512 def cond(*args: Any) -> Any: 1513 ''' 1514 Simulate a chain of if-else statements, using condition/result pairs 1515 from the arguments given; when given an even number of args, None is 1516 used as a final fallback result; when given an odd number of args, the 1517 last argument is used as a final `else` value, if needed. 1518 ''' 1519 1520 for i in range(0, len(args) - len(args) % 2, 2): 1521 if args[i]: 1522 return args[i+1] 1523 return None if len(args) % 2 == 0 else args[-1] 1524 1525 def conform(x: Any, denan: Any = None, deinf: Any = None, fn = str) -> Any: 1526 'Make values JSON-compatible.' 1527 1528 if isinstance(x, float): 1529 # turn NaNs and Infinities into the replacement values given 1530 if isnan(x): 1531 return denan 1532 if isinf(x): 1533 return deinf 1534 return x 1535 1536 if isinstance(x, (bool, int, float, str)): 1537 return x 1538 1539 if isinstance(x, dict): 1540 return { 1541 str(k): conform(v) for k, v in x.items() if not 1542 (isinstance(k, Skip) or isinstance(v, Skip)) 1543 } 1544 1545 if isinstance(x, Iterable): 1546 return [conform(e) for e in x if not isinstance(e, Skip)] 1547 1548 if isinstance(x, DotCallable): 1549 return x.value 1550 1551 return fn(x) 1552 1553 fix = conform 1554 1555 def countif(src: Iterable, check: Callable) -> int: 1556 ''' 1557 Count how many values make the func given true-like. This func works with 1558 sequences, dictionaries, and strings. 1559 ''' 1560 1561 if callable(src): 1562 src, check = check, src 1563 check = predicate(check) 1564 1565 total = 0 1566 if isinstance(src, dict): 1567 for v in src.values(): 1568 if check(v): 1569 total += 1 1570 else: 1571 for v in src: 1572 if check(v): 1573 total += 1 1574 return total 1575 1576 # def debase64(x): 1577 # return debase64bytes(str(x).encode()).decode() 1578 1579 def debase64(s: str) -> bytes: 1580 'Convert away from base64 encoding, including data-URIs.' 1581 1582 if s.startswith('data:'): 1583 i = s.find(',') 1584 if i >= 0: 1585 return standard_b64decode(s[i + 1:]) 1586 return standard_b64decode(s) 1587 1588 unbase64 = debase64 1589 1590 def dedup(v: Iterable) -> Iterable: 1591 'Ignore reappearing items from iterables, after their first occurrence.' 1592 1593 got = set() 1594 for e in v: 1595 if not e in got: 1596 got.add(e) 1597 yield e 1598 1599 dedupe = dedup 1600 deduped = dedup 1601 deduplicate = dedup 1602 deduplicated = dedup 1603 undup = dedup 1604 undupe = dedup 1605 unduped = dedup 1606 unduplicate = dedup 1607 unduplicated = dedup 1608 unique = dedup 1609 uniqued = dedup 1610 1611 def defunc(x: Any) -> Any: 1612 'Call if value is a func, or return it back as given.' 1613 return x() if callable(x) else x 1614 1615 callmemaybe = defunc 1616 defunct = defunc 1617 unfunc = defunc 1618 unfunct = defunc 1619 1620 def dejson(x: Any, catch: Union[Callable[[Exception], Any], Any] = None) -> Any: 1621 'Safely parse JSON from strings.' 1622 try: 1623 return loads(x) if isinstance(x, str) else x 1624 except Exception as e: 1625 return catch(e) if callable(catch) else catch 1626 1627 unjson = dejson 1628 1629 def denan(x: Any, fallback: Any = None) -> Any: 1630 'Replace floating-point NaN with the alternative value given.' 1631 return x if not (isinstance(x, float) and isnan(x)) else fallback 1632 1633 def denil(*args: Any) -> Any: 1634 'Avoid None values, if possible: first value which isn\'t None wins.' 1635 for e in args: 1636 if e != None: 1637 return e 1638 return None 1639 1640 denone = denil 1641 denull = denil 1642 1643 def dirname(s: str) -> str: 1644 'Ignore the last part of a filepath.' 1645 return str(Path(s).parent) 1646 1647 def dive(into: Any, doing: Callable) -> Any: 1648 'Transform a nested value by calling a func via depth-first recursion.' 1649 1650 # support args in either order 1651 if callable(into): 1652 into, doing = doing, into 1653 1654 return _dive_kv(None, into, doing) 1655 1656 deepmap = dive 1657 dive1 = dive 1658 1659 def divebin(x: Any, y: Any, doing: Callable) -> Any: 1660 'Nested 2-value version of depth-first-recursive func dive.' 1661 1662 # support args in either order 1663 if callable(x): 1664 x, y, doing = y, doing, x 1665 1666 narg = required_arg_count(doing) 1667 if narg == 2: 1668 return dive(x, lambda a: dive(y, lambda b: doing(a, b))) 1669 if narg == 4: 1670 return dive(x, lambda i, a: dive(y, lambda j, b: doing(i, a, j, b))) 1671 raise Exception('divebin(...) only supports funcs with 2 or 4 args') 1672 1673 bindive = divebin 1674 # diveboth = divebin 1675 # dualdive = divebin 1676 # duodive = divebin 1677 dive2 = divebin 1678 1679 def _dive_kv(key: Any, into: Any, doing: Callable) -> Any: 1680 if isinstance(into, dict): 1681 return {k: _dive_kv(k, v, doing) for k, v in into.items()} 1682 if isinstance(into, Iterable) and not isinstance(into, str): 1683 return [_dive_kv(i, e, doing) for i, e in enumerate(into)] 1684 1685 narg = required_arg_count(doing) 1686 return doing(key, into) if narg == 2 else doing(into) 1687 1688 class DotCallable: 1689 'Enable convenient dot-syntax calling of 1-input funcs.' 1690 1691 def __init__(self, value: Any): 1692 self.value = value 1693 1694 def __getattr__(self, key: str) -> Any: 1695 return DotCallable(globals()[key](self.value)) 1696 1697 class Dottable: 1698 'Enable convenient dot-syntax access to dictionary values.' 1699 1700 def __getattr__(self, key: Any) -> Any: 1701 return self.__dict__[key] if key in self.__dict__ else None 1702 1703 def __getitem__(self, key: Any) -> Any: 1704 return self.__dict__[key] if key in self.__dict__ else None 1705 1706 def __iter__(self) -> Iterable: 1707 return iter(self.__dict__) 1708 1709 def dotate(x: Any) -> Union[Dottable, Any]: 1710 'Recursively ensure all dictionaries in a value are dot-accessible.' 1711 1712 if isinstance(x, dict): 1713 d = Dottable() 1714 d.__dict__ = {k: dotate(v) for k, v in x.items()} 1715 return d 1716 if isinstance(x, list): 1717 return [dotate(e) for e in x] 1718 if isinstance(x, tuple): 1719 return tuple(dotate(e) for e in x) 1720 return x 1721 1722 dotated = dotate 1723 dote = dotate 1724 doted = dotate 1725 dotified = dotate 1726 dotify = dotate 1727 dottified = dotate 1728 dottify = dotate 1729 1730 # make dictionaries `physics`, `symbols`, and `units` easier to use 1731 phy = dotate(phy) 1732 physics = phy 1733 sym = dotate(sym) 1734 symbols = sym 1735 units = dotate(units) 1736 1737 def drop(src: Any, *what) -> Any: 1738 ''' 1739 Either ignore all substrings occurrences, or ignore all keys given from 1740 an object, or even from a sequence of objects. 1741 ''' 1742 1743 if isinstance(src, str): 1744 return strdrop(src, *what) 1745 return _itemsdrop(src, set(what)) 1746 1747 dropped = drop 1748 # ignore = drop 1749 # ignored = drop 1750 1751 def _itemsdrop(src: Any, what: Set) -> Any: 1752 if isinstance(src, dict): 1753 kv = {} 1754 for k, v in src.items(): 1755 if not (k in what): 1756 kv[k] = v 1757 return kv 1758 1759 if isinstance(src, Iterable): 1760 return [_itemsdrop(e, what) for e in src] 1761 1762 return None 1763 1764 def each(src: Iterable, f: Callable) -> Any: 1765 ''' 1766 A generalization of built-in func map, which can also handle dictionaries 1767 and strings. 1768 ''' 1769 1770 if callable(src): 1771 src, f = f, src 1772 1773 if isinstance(src, dict): 1774 return mapkv(src, lambda k, _: k, f) 1775 1776 if isinstance(src, str): 1777 s = StringIO() 1778 f = loopify(f) 1779 for i, c in enumerate(src): 1780 v = f(i, c) 1781 if not isinstance(v, Skip): 1782 s.write(str(v)) 1783 return s.getvalue() 1784 1785 return tuple(f(i, v) for i, v in enumerate(src)) 1786 1787 mapped = each 1788 1789 def emptyish(x: Any) -> bool: 1790 ''' 1791 Check if a value can be considered empty, which includes non-empty 1792 strings which only have spaces in them. 1793 ''' 1794 1795 def check(x: Any) -> bool: 1796 if not x: 1797 return True 1798 if isinstance(x, str): 1799 return bool(emptyish_re.match(x)) 1800 return False 1801 1802 if check(x): 1803 return True 1804 if isinstance(x, Iterable): 1805 return all(check(e) for e in x) 1806 return False 1807 1808 isemptyish = emptyish 1809 1810 def endict(x: Any) -> Dict[str, Any]: 1811 'Turn non-dictionary values into dictionaries with string keys.' 1812 1813 if isinstance(x, dict): 1814 return {str(k): v for k, v in x.items()} 1815 if arrayish(x): 1816 return {str(e): e for e in x} 1817 return {str(x): x} 1818 1819 dicted = endict 1820 endicted = endict 1821 indict = endict 1822 todict = endict 1823 1824 def enfloat(x: Any, fallback: float = nan) -> float: 1825 try: 1826 return float(x) 1827 except Exception: 1828 return fallback 1829 1830 enfloated = enfloat 1831 floated = enfloat 1832 floatify = enfloat 1833 floatize = enfloat 1834 tofloat = enfloat 1835 1836 def enint(x: Any, fallback: Any = None) -> Any: 1837 try: 1838 return int(x) 1839 except Exception: 1840 return fallback 1841 1842 eninted = enint 1843 inted = enint 1844 integered = enint 1845 intify = enint 1846 intize = enint 1847 toint = enint 1848 1849 def enlist(x: Any) -> List[Any]: 1850 'Turn non-list values into lists.' 1851 return list(x) if arrayish(x) else [x] 1852 1853 # inlist = enlist 1854 enlisted = enlist 1855 listify = enlist 1856 listize = enlist 1857 tolist = enlist 1858 1859 def entuple(x: Any) -> Tuple[Any, ...]: 1860 'Turn non-tuple values into tuples.' 1861 return tuple(x) if arrayish(x) else (x, ) 1862 1863 entupled = entuple 1864 ntuple = entuple 1865 ntupled = entuple 1866 tuplify = entuple 1867 tuplize = entuple 1868 toentuple = entuple 1869 tontuple = entuple 1870 totuple = entuple 1871 1872 def error(message: Any) -> Exception: 1873 return Exception(str(message)) 1874 1875 err = error 1876 1877 def ext(s: str) -> str: 1878 'Get a filepath\'s extension, if present.' 1879 1880 name = Path(s).name 1881 i = name.rfind('.') 1882 return name[i:] if i >= 0 else '' 1883 1884 filext = ext 1885 1886 def fail(message: Any, error_code: int = 255) -> NoReturn: 1887 stdout.flush() 1888 print(f'\x1b[31m{message}\x1b[0m', file=stderr) 1889 quit(error_code) 1890 1891 abort = fail 1892 bail = fail 1893 1894 def fields(s: str) -> Iterable[str]: 1895 'Split fields AWK-style from the string given.' 1896 return awk_sep_re.split(s.strip()) 1897 1898 # items = fields 1899 splitfields = fields 1900 splititems = fields 1901 words = fields 1902 1903 def first(items: SupportsIndex, fallback: Any = None) -> Any: 1904 return items[0] if len(items) > 0 else fallback 1905 1906 def flappend(*args: Any) -> List[Any]: 1907 'Turn arbitrarily-nested values/sequences into a single flat sequence.' 1908 1909 flat = [] 1910 def dig(x: Any) -> None: 1911 if arrayish(x): 1912 for e in x: 1913 dig(e) 1914 elif isinstance(x, dict): 1915 for e in x.values(): 1916 dig(e) 1917 else: 1918 flat.append(x) 1919 1920 for e in args: 1921 dig(e) 1922 return flat 1923 1924 def flat(*args: Any) -> Iterable: 1925 'Turn arbitrarily-nested values/sequences into a single flat sequence.' 1926 1927 def _flat_rec(x: Any) -> Iterable: 1928 if x is None: 1929 return 1930 1931 if isinstance(x, dict): 1932 yield from _flat_rec(x.values()) 1933 1934 if isinstance(x, str): 1935 yield x 1936 return 1937 1938 if isinstance(x, Iterable): 1939 for e in x: 1940 yield from _flat_rec(e) 1941 return 1942 1943 yield x 1944 1945 for x in args: 1946 yield from _flat_rec(x) 1947 1948 flatten = flat 1949 flattened = flat 1950 1951 def fromto(start, stop, f: Callable = identity) -> Iterable: 1952 'Sequence all integers between the numbers given, end-value included.' 1953 return (f(e) for e in range(start, stop + 1)) 1954 1955 def fuzz(x: Union[int, float]) -> Union[float, Dict[str, float]]: 1956 ''' 1957 Deapproximate numbers to their max range before approximation: the 1958 result is a dictionary with the guessed lower-bound number, the number 1959 given, and the guessed upper-bound number which can approximate to the 1960 original number given. NaNs and the infinities are returned as given, 1961 instead of resulting in a dictionary. 1962 ''' 1963 1964 if isnan(x) or isinf(x): 1965 return x 1966 1967 if x == 0: 1968 return {'-0.5': -0.5, '0': 0.0, '0.5': +0.5} 1969 1970 if x % 1 != 0: 1971 # return surrounding integers when given non-integers 1972 a = floor(x) 1973 b = ceil(x) 1974 return {str(a): a, str(x): x, str(b): b} 1975 1976 if x % 10 != 0: 1977 a = x - 0.5 1978 b = x + 0.5 1979 return {str(a): a, str(x): x, str(b): b} 1980 1981 # find the integer log10 of the absolute value; 0 was handled previously 1982 y = int(abs(x)) 1983 p10 = 1 1984 while True: 1985 if y % p10 != 0: 1986 p10 /= 10 1987 break 1988 p10 *= 10 1989 delta = p10 / 2 1990 1991 s = +1 if x > 0 else -1 1992 ux = abs(x) 1993 a = s * ux - delta 1994 b = s * ux + delta 1995 return {str(a): a, str(x): x, str(b): b} 1996 1997 def generated(src: Any) -> Any: 1998 'Make tuples out of generators, or return non-generator values as given.' 1999 return tuple(src) if isinstance(src, (Generator, range)) else src 2000 2001 concrete = generated 2002 concreted = generated 2003 concretize = generated 2004 concretized = generated 2005 degen = generated 2006 degenerate = generated 2007 degenerated = generated 2008 degenerator = generated 2009 gen = generated 2010 generate = generated 2011 synth = generated 2012 synthed = generated 2013 synthesize = generated 2014 synthesized = generated 2015 2016 def group(src: Iterable, by: Callable = identity) -> Dict: 2017 ''' 2018 Separate transformed items into arrays, the final result being a dict 2019 whose keys are all the transformed values, and whose values are lists 2020 of all the original values which did transform to their group's key. 2021 ''' 2022 2023 if callable(src): 2024 src, by = by, src 2025 2026 by = loopify(by) 2027 kv = src.items() if isinstance(src, dict) else enumerate(src) 2028 2029 groups = {} 2030 for k, v in kv: 2031 dk = by(k, v) 2032 if isinstance(dk, Skip) or isinstance(v, Skip): 2033 continue 2034 if dk in groups: 2035 groups[dk].append(v) 2036 else: 2037 groups[dk] = [v] 2038 return groups 2039 2040 grouped = group 2041 2042 def gire(src: Iterable[str], using: Iterable[str], fallback: Any = '') -> Dict: 2043 ''' 2044 Group matched items into arrays, the final result being a dict whose 2045 keys are all the matchable regexes given, and whose values are lists 2046 of all the original values which did case-insensitively match their 2047 group's key as a regex. 2048 ''' 2049 2050 using = tuple(using) 2051 return group(src, lambda x: imatch(x, using, fallback)) 2052 2053 gbire = gire 2054 groupire = gire 2055 2056 def gre(src: Iterable[str], using: Iterable[str], fallback: Any = '') -> Dict: 2057 ''' 2058 Group matched items into arrays, the final result being a dict whose 2059 keys are all the matchable regexes given, and whose values are lists 2060 of all the original values which did regex-match their group's key. 2061 ''' 2062 2063 using = tuple(using) 2064 return group(src, lambda x: match(x, using, fallback)) 2065 2066 gbre = gre 2067 groupre = gre 2068 2069 def gsub(s: str, what: str, repl: str) -> str: 2070 'Replace all regex-matches with the string given.' 2071 return compile(what).sub(repl, s) 2072 2073 def harden(f: Callable, fallback: Any = None) -> Callable: 2074 def _hardened_caller(*args): 2075 try: 2076 return f(*args) 2077 except Exception: 2078 return fallback 2079 return _hardened_caller 2080 2081 hardened = harden 2082 insure = harden 2083 insured = harden 2084 2085 def horner(coeffs: List[float], x: Union[int, float]) -> float: 2086 if isinstance(coeffs, (int, float)): 2087 coeffs, x = x, coeffs 2088 2089 if len(coeffs) == 0: 2090 return 0 2091 2092 y = coeffs[0] 2093 for c in islice(coeffs, 1, None): 2094 y *= x 2095 y += c 2096 return y 2097 2098 polyval = horner 2099 2100 def idiota(n: int, f: Callable = identity) -> Dict[int, int]: 2101 'ID (keys) version of func iota.' 2102 return { v: v for v in (f(e) for e in range(1, n + 1))} 2103 2104 dictiota = idiota 2105 kviota = idiota 2106 2107 def imatch(what: str, using: Iterable[str], fallback: str = '') -> str: 2108 'Try to case-insensitively match a string with any of the regexes given.' 2109 2110 if not isinstance(what, str): 2111 what, using = using, what 2112 2113 for s in using: 2114 expr = compile(s, False) 2115 m = expr.search(what) 2116 if m: 2117 # return what[m.start():m.end()] 2118 return s 2119 return fallback 2120 2121 def indices(x: Any) -> Iterable[Any]: 2122 'List all indices/keys, or get an exclusive range from an int.' 2123 2124 if isinstance(x, int): 2125 return range(x) 2126 if isinstance(x, dict): 2127 return x.keys() 2128 if isinstance(x, (str, list, tuple)): 2129 return range(len(x)) 2130 return tuple() 2131 2132 keys = indices 2133 2134 def ints(start, stop, f: Callable = identity) -> Iterable[int]: 2135 'Sequence integers, end-value included.' 2136 2137 if isnan(start) or isnan(stop) or isinf(start) or isinf(stop): 2138 return tuple() 2139 return (f(e) for e in range(int(ceil(start)), int(stop) + 1)) 2140 2141 integers = ints 2142 2143 def iota(n: int, f: Callable = identity) -> Iterable[int]: 2144 'Sequence all integers from 1 up to (and including) the int given.' 2145 return (f(e) for e in range(1, n + 1)) 2146 2147 def itemsafter(x: Iterable, what: Any) -> Iterable: 2148 ok = False 2149 check = predicate(what) 2150 for e in x: 2151 if ok: 2152 yield e 2153 elif check(e): 2154 ok = True 2155 2156 def itemsafterlast(x: Iterable, what: Any) -> Iterable: 2157 rest: List[Any] = [] 2158 check = predicate(what) 2159 for e in x: 2160 if check(e): 2161 rest.clear() 2162 else: 2163 rest.append(e) 2164 2165 for e in islice(rest, 1, len(rest)): 2166 yield e 2167 2168 def itemsbefore(x: Iterable, what: Any) -> Iterable: 2169 check = predicate(what) 2170 for e in x: 2171 if check(e): 2172 return 2173 yield e 2174 2175 def itemsbeforelast(x: Iterable, what: Any) -> Iterable: 2176 items = [] 2177 for e in x: 2178 items.append(e) 2179 2180 i = -1 2181 check = predicate(what) 2182 for j, e in enumerate(reversed(items)): 2183 if check(e): 2184 i = j 2185 break 2186 2187 if i < 0: 2188 return items 2189 if i == 0: 2190 return tuple() 2191 for e in islice(items, 0, i): 2192 yield e 2193 2194 def itemssince(x: Iterable, what: Any) -> Iterable: 2195 ok = False 2196 check = predicate(what) 2197 for e in x: 2198 ok = ok or check(e) 2199 if ok: 2200 yield e 2201 2202 def itemssincelast(x: Iterable, what: Any) -> Iterable: 2203 rest: List[Any] = [] 2204 check = predicate(what) 2205 for e in x: 2206 if check(e): 2207 rest.clear() 2208 else: 2209 rest.append(e) 2210 return rest 2211 2212 def itemsuntil(x: Iterable, what: Any) -> Iterable: 2213 check = predicate(what) 2214 for e in x: 2215 yield e 2216 if check(e): 2217 return 2218 2219 def itemsuntillast(x: Iterable, what: Any) -> Iterable: 2220 items = [] 2221 for e in x: 2222 items.append(e) 2223 2224 i = -1 2225 check = predicate(what) 2226 for j, e in enumerate(reversed(items)): 2227 if check(e): 2228 i = j 2229 break 2230 2231 if i < 0: 2232 return items 2233 for e in islice(items, 0, i + 1): 2234 yield e 2235 2236 itemsuntilfinal = itemsuntillast 2237 2238 def join(items: Iterable, sep: Union[str, Iterable] = ' ') -> Union[str, Dict]: 2239 ''' 2240 Join iterables using the separator-string given: its 2 arguments 2241 can come in either order, and are sorted out if needed. When given 2242 2 non-string iterables, the result is an object whose keys are from 2243 the first argument, and whose values are from the second one. 2244 2245 You can use it any of the following ways, where `keys` and `values` are 2246 sequences (lists, tuples, or generators), and `separator` is a string: 2247 2248 join(values) 2249 join(values, separator) 2250 join(separator, values) 2251 join(keys, values) 2252 ''' 2253 2254 if arrayish(items) and arrayish(sep): 2255 return {k: v for k, v in zip(items, sep)} 2256 if isinstance(items, str): 2257 items, sep = sep, items 2258 return sep.join(str(e) for e in items) 2259 2260 def joined_paragraphs(lines: Iterable[str]) -> Iterable[Sequence[str]]: 2261 ''' 2262 Regroup lines into individual paragraphs, each of which can span multiple 2263 lines: such paragraphs have no empty lines in them, and never end with a 2264 trailing line-feed. 2265 ''' 2266 2267 par: List[str] = [] 2268 for l in lines: 2269 if (not l) and par: 2270 yield '\n'.join(par) 2271 par.clear() 2272 else: 2273 par.append(l) 2274 2275 if len(par) > 0: 2276 yield '\n'.join(par) 2277 2278 def json0(x: Any) -> str: 2279 'Encode value into a minimal single-line JSON string.' 2280 return dumps(x, separators=(',', ':'), allow_nan=False, indent=None) 2281 2282 j0 = json0 2283 2284 def json2(x: Any) -> str: 2285 ''' 2286 Encode value into a (possibly multiline) JSON string, using 2 spaces for 2287 each indentation level. 2288 ''' 2289 return dumps(x, separators=(',', ': '), allow_nan=False, indent=2) 2290 2291 j2 = json2 2292 2293 def jsonl(x: Any) -> Iterable: 2294 'Turn value into multiple JSON-encoded strings, known as JSON Lines.' 2295 2296 if x is None: 2297 yield dumps(x, allow_nan=False) 2298 elif isinstance(x, (bool, int, float, dict, str)): 2299 yield dumps(x, allow_nan=False) 2300 elif isinstance(x, Iterable): 2301 for e in x: 2302 yield dumps(e, allow_nan=False) 2303 else: 2304 yield dumps(str(x), allow_nan=False) 2305 2306 jsonlines = jsonl 2307 ndjson = jsonl 2308 tojsonl = jsonl 2309 tojsonlines = jsonl 2310 2311 def keep(src: Iterable, pred: Any) -> Iterable: 2312 ''' 2313 A generalization of built-in func filter, which can also handle dicts and 2314 strings. 2315 ''' 2316 2317 if callable(src): 2318 src, pred = pred, src 2319 pred = predicate(pred) 2320 pred = loopify(pred) 2321 2322 if isinstance(src, str): 2323 out = StringIO() 2324 for i, c in enumerate(src): 2325 if pred(i, c): 2326 out.write(c) 2327 return out.getvalue() 2328 2329 if isinstance(src, dict): 2330 return { k: v for k, v in src.items() if pred(k, v) } 2331 return (e for i, e in enumerate(src) if pred(i, e)) 2332 2333 filtered = keep 2334 kept = keep 2335 2336 def last(items: SupportsIndex, fallback: Any = None) -> Any: 2337 return items[-1] if len(items) > 0 else fallback 2338 2339 def links(src: Any) -> Iterable: 2340 'Auto-detect all (HTTP/HTTPS) hyperlink-like substrings.' 2341 2342 if isinstance(src, str): 2343 for match in link_re.finditer(src): 2344 # yield src[match.start():match.end()] 2345 yield match.group(0) 2346 elif isinstance(src, dict): 2347 for k, v in src.items(): 2348 yield from k 2349 yield from links(v) 2350 elif isinstance(src, Iterable): 2351 for v in src: 2352 yield from links(v) 2353 2354 def loopify(x: Callable) -> Callable: 2355 nargs = required_arg_count(x) 2356 if nargs == 2: 2357 return x 2358 elif nargs == 1: 2359 return lambda _, v: x(v) 2360 else: 2361 raise Exception('only funcs with 1 or 2 args are supported') 2362 2363 def mapkv(src: Iterable, key: Callable, value: Callable = identity) -> Dict: 2364 ''' 2365 A map-like func for dictionaries, which uses 2 mapping funcs, the first 2366 for the keys, the second for the values. 2367 ''' 2368 2369 if key is None: 2370 key = lambda k, _: k 2371 2372 if callable(src): 2373 src, key, value = value, src, key 2374 2375 if required_arg_count(key) != 2: 2376 oldkey = key 2377 key = lambda k, _: oldkey(k) 2378 2379 key = loopify(key) 2380 value = loopify(value) 2381 # if isinstance(src, dict): 2382 # return { key(k, v): value(k, v) for k, v in src.items() } 2383 # return { key(i, v): value(i, v) for i, v in enumerate(src) } 2384 2385 def add(k, v, to): 2386 dk = key(k, v) 2387 dv = value(k, v) 2388 if isinstance(dk, Skip) or isinstance(dv, Skip): 2389 return 2390 to[dk] = dv 2391 2392 res = {} 2393 kv = src.items() if isinstance(src, dict) else enumerate(src) 2394 for k, v in kv: 2395 add(k, v, res) 2396 return res 2397 2398 def match(what: str, using: Iterable[str], fallback: str = '') -> str: 2399 'Try to match a string with any of the regexes given.' 2400 2401 if not isinstance(what, str): 2402 what, using = using, what 2403 2404 for s in using: 2405 expr = compile(s) 2406 m = expr.search(what) 2407 if m: 2408 # return what[m.start():m.end()] 2409 return s 2410 return fallback 2411 2412 def maybe(f: Callable, x: Any) -> Any: 2413 ''' 2414 Try calling a func on a value, using the same value as a fallback result, 2415 in case of exceptions. 2416 ''' 2417 2418 if not callable(f): 2419 f, x = x, f 2420 try: 2421 return f(x) 2422 except Exception: 2423 return x 2424 2425 def mappend(*args) -> Dict: 2426 kv = {} 2427 for src in args: 2428 if isinstance(src, dict): 2429 for k, v in src.items(): 2430 kv[k] = v 2431 else: 2432 raise Exception('mappend only works with dictionaries') 2433 return kv 2434 2435 def message(x: Any, result: Any = skip) -> Any: 2436 print(x, file=stderr) 2437 return result 2438 2439 msg = message 2440 2441 def must(cond: Any, errmsg: str = 'condition given not always true') -> None: 2442 'Enforce conditions, raising an exception on failure.' 2443 if not cond: 2444 raise Exception(errmsg) 2445 2446 demand = must 2447 enforce = must 2448 2449 def nowdict() -> dict: 2450 v = datetime(2000, 1, 1).now() 2451 return { 2452 'year': v.year, 2453 'month': v.month, 2454 'day': v.day, 2455 'hour': v.hour, 2456 'minute': v.minute, 2457 'second': v.second, 2458 'text': v.strftime('%Y-%m-%d %H:%M:%S %b %a'), 2459 'weekday': v.strftime('%A'), 2460 } 2461 2462 def number(x: Any) -> Union[int, float, Any]: 2463 ''' 2464 Try to turn the value given into a number, using a fallback value instead 2465 of raising exceptions. 2466 ''' 2467 2468 if isinstance(x, float): 2469 return x 2470 2471 try: 2472 return int(x) 2473 except Exception: 2474 return float(x) 2475 2476 def numbers(src: Any) -> Iterable: 2477 'Auto-detect all number-like substrings.' 2478 2479 if isinstance(src, str): 2480 for match in number_re.finditer(src): 2481 yield match.group(0).strip() 2482 # yield src[match.start():match.end()].strip() 2483 elif isinstance(src, dict): 2484 for k, v in src.items(): 2485 yield from k 2486 yield from links(v) 2487 elif isinstance(src, Iterable): 2488 for v in src: 2489 yield from links(v) 2490 2491 def numsign(x: Union[int, float]) -> Union[int, float]: 2492 'Get a number\'s sign, or NaN if the number given is a NaN.' 2493 2494 if isinstance(x, int): 2495 if x > 0: 2496 return +1 2497 if x < 0: 2498 return -1 2499 return 0 2500 2501 if isnan(x): 2502 return x 2503 2504 if x > 0: 2505 return +1.0 2506 if x < 0: 2507 return -1.0 2508 return 0.0 2509 2510 def numstats(src: Any) -> Dict[str, Union[float, int]]: 2511 'Gather several single-pass numeric statistics.' 2512 2513 n = mean_sq = ln_sum = 0 2514 least = +inf 2515 most = -inf 2516 total = mean = 0 2517 prod = 1 2518 nans = ints = pos = zero = neg = 0 2519 2520 def update_numstats(x: Any) -> None: 2521 nonlocal nans, n, ints, pos, neg, zero, least, most, total, prod 2522 nonlocal ln_sum, mean, mean_sq 2523 2524 if not isinstance(x, (float, int)): 2525 return 2526 2527 if isnan(x): 2528 nans += 1 2529 return 2530 2531 n += 1 2532 ints += int(isinstance(x, int) or x == floor(x)) 2533 2534 if x > 0: 2535 pos += 1 2536 elif x < 0: 2537 neg += 1 2538 else: 2539 zero += 1 2540 2541 least = min(least, x) 2542 most = max(most, x) 2543 2544 # total += x 2545 prod *= x 2546 ln_sum += log(x) 2547 2548 d1 = x - mean 2549 mean += d1 / n 2550 d2 = x - mean 2551 mean_sq += d1 * d2 2552 2553 def _numstats_rec(src: Any) -> None: 2554 if isinstance(src, dict): 2555 for e in src.values(): 2556 _numstats_rec(e) 2557 elif isinstance(src, Iterable) and not isinstance(src, str): 2558 for e in src: 2559 _numstats_rec(e) 2560 else: 2561 update_numstats(src) 2562 2563 _numstats_rec(src) 2564 2565 sd = nan 2566 geomean = nan 2567 if n > 0: 2568 sd = sqrt(mean_sq / n) 2569 geomean = exp(ln_sum / n) if not isinf(ln_sum) else nan 2570 total = n * mean 2571 2572 return { 2573 'n': n, 2574 'nan': nans, 2575 'min': least, 2576 'max': most, 2577 'sum': total, 2578 'mean': mean, 2579 'geomean': geomean, 2580 'sd': sd, 2581 'product': prod, 2582 'integer': ints, 2583 'positive': pos, 2584 'zero': zero, 2585 'negative': neg, 2586 } 2587 2588 def once(x: Any, replacement: Any = None) -> Any: 2589 ''' 2590 Replace the first argument given after the first time this func has been 2591 given it: this is a deliberately stateful function, given its purpose. 2592 ''' 2593 2594 if not (x in seen): 2595 seen.add(x) 2596 return x 2597 else: 2598 return replacement 2599 2600 onced = once 2601 2602 def pad(s: str, n: int, pad: str = ' ') -> str: 2603 l = len(s) 2604 return s if l >= n else s + int((n - l) / len(pad)) * pad 2605 2606 def padcenter(s: str, n: int, pad: str = ' ') -> str: 2607 return s.center(n, pad) 2608 2609 centerpad = padcenter 2610 centerpadded = padcenter 2611 cjust = padcenter 2612 cpad = padcenter 2613 padc = padcenter 2614 paddedcenter = padcenter 2615 2616 def padend(s: str, n: int, pad: str = ' ') -> str: 2617 return s.rjust(n, pad) 2618 2619 padr = padend 2620 padright = padend 2621 paddedend = padend 2622 paddedright = padend 2623 rpad = padend 2624 rightpad = padend 2625 rightpadded = padend 2626 2627 def padstart(s: str, n: int, pad: str = ' ') -> str: 2628 return s.ljust(n, pad) 2629 2630 lpad = padstart 2631 leftpad = padstart 2632 leftpadded = padstart 2633 padl = padstart 2634 padleft = padstart 2635 paddedleft = padstart 2636 paddedstart = padstart 2637 2638 def panic(x: Any) -> None: 2639 raise Exception(x) 2640 2641 def paragraphize(lines: Iterable[str]) -> Iterable[Sequence[str]]: 2642 ''' 2643 Regroup lines into individual paragraphs, each of which is a list of 2644 single-line strings, none of which never end with a trailing line-feed. 2645 ''' 2646 2647 par: List[str] = [] 2648 for l in lines: 2649 if (not l) and par: 2650 yield par 2651 par.clear() 2652 else: 2653 par.append(l) 2654 2655 if len(par) > 0: 2656 yield par 2657 2658 paragraphed = paragraphize 2659 paragraphs = paragraphize 2660 paragroup = paragraphize 2661 pargroup = paragraphize 2662 2663 def parse(s: str, fallback: Any = None) -> Any: 2664 'Try to parse JSON, ignoring exceptions in favor of a fallback value.' 2665 2666 try: 2667 return loads(s) 2668 except Exception: 2669 return fallback 2670 2671 fromjson = parse 2672 parsed = parse 2673 loaded = parse 2674 unjson = parse 2675 2676 def pick(src: Any, *what) -> Any: 2677 'Pick only the keys given from an object, or even a sequence of objects.' 2678 2679 if isinstance(src, dict): 2680 kv = {} 2681 for k in what: 2682 kv[k] = src[k] 2683 return kv 2684 2685 if isinstance(src, Iterable): 2686 return [pick(e, *what) for e in src] 2687 2688 return None 2689 2690 picked = pick 2691 2692 def plain(s: str) -> str: 2693 'Ignore all ANSI-style sequences in a string.' 2694 return ansi_style_re.sub('', s) 2695 2696 def predicate(x: Any) -> Callable: 2697 'Helps various higher-order funcs, by standardizing `predicate` values.' 2698 2699 if callable(x): 2700 return x 2701 2702 if isinstance(x, float): 2703 if isnan(x): 2704 return lambda y: isinstance(y, float) and isnan(y) 2705 if isinf(x): 2706 return lambda y: isinstance(y, float) and isinf(y) 2707 2708 return lambda y: x == y 2709 2710 pred = predicate 2711 2712 def quoted(s: str, quote: str = '"') -> str: 2713 'Surround a string with quotes.' 2714 return f'{quote}{s}{quote}' 2715 2716 def recover(*args) -> Any: 2717 ''' 2718 Catch exceptions using a lambda/callback func, in one of 6 ways 2719 recover(zero_args_func) 2720 recover(zero_args_func, exception_replacement_value) 2721 recover(zero_args_func, one_arg_exception_handling_func) 2722 recover(one_arg_func, arg) 2723 recover(one_arg_func, arg, exception_replacement_value) 2724 recover(one_arg_func, arg, one_arg_exception_handling_func) 2725 ''' 2726 2727 if len(args) == 1: 2728 f = args[0] 2729 try: 2730 return f() 2731 except Exception: 2732 return None 2733 elif len(args) == 2: 2734 f, fallback = args[0], args[1] 2735 if callable(f) and callable(fallback): 2736 try: 2737 return f() 2738 except Exception as e: 2739 nargs = required_arg_count(fallback) 2740 return fallback(e) if nargs == 1 else fallback() 2741 else: 2742 try: 2743 return f() if required_arg_count(f) == 0 else f(args[1]) 2744 except Exception: 2745 return fallback 2746 elif len(args) == 3: 2747 f, x, fallback = args[0], args[1], args[2] 2748 if callable(f) and callable(fallback): 2749 try: 2750 return f(x) 2751 except Exception as e: 2752 nargs = required_arg_count(fallback) 2753 return fallback(e) if nargs == 1 else fallback() 2754 else: 2755 try: 2756 return f(x) 2757 except Exception: 2758 return fallback 2759 else: 2760 raise Exception('recover(...) only works with 1, 2, or 3 args') 2761 2762 attempt = recover 2763 attempted = recover 2764 recovered = recover 2765 recoverred = recover 2766 rescue = recover 2767 rescued = recover 2768 trycall = recover 2769 2770 def reject(src: Iterable, pred: Any) -> Iterable: 2771 ''' 2772 A generalization of built-in func filter, which uses predicate funcs the 2773 opposite way, and which can also handle dicts and strings. 2774 ''' 2775 2776 if callable(src): 2777 src, pred = pred, src 2778 pred = predicate(pred) 2779 pred = loopify(pred) 2780 2781 if isinstance(src, str): 2782 out = StringIO() 2783 for i, c in enumerate(src): 2784 if not pred(i, c): 2785 out.write(c) 2786 return out.getvalue() 2787 2788 if isinstance(src, dict): 2789 return { k: v for k, v in src.items() if not pred(k, v) } 2790 return (e for i, e in enumerate(src) if not pred(i, e)) 2791 2792 avoid = reject 2793 avoided = reject 2794 keepout = reject 2795 keptout = reject 2796 rejected = reject 2797 2798 def retype(x: Any) -> Any: 2799 'Try to narrow the type of the value given.' 2800 2801 if isinstance(x, float): 2802 return int(x) if floor(x) == x else x 2803 2804 if not isinstance(x, str): 2805 return x 2806 2807 try: 2808 return loads(x) 2809 except Exception: 2810 pass 2811 2812 try: 2813 return int(x) 2814 except Exception: 2815 pass 2816 2817 try: 2818 return float(x) 2819 except Exception: 2820 pass 2821 2822 return x 2823 2824 autocast = retype 2825 mold = retype 2826 molded = retype 2827 narrow = retype 2828 narrowed = retype 2829 recast = retype 2830 recasted = retype 2831 remold = retype 2832 remolded = retype 2833 retyped = retype 2834 2835 def revcompose(*what: Callable) -> Callable: 2836 def composite(x: Any) -> Any: 2837 for f in reversed(what): 2838 x = f(x) 2839 return x 2840 return composite 2841 2842 rcompose = revcompose 2843 rcomposed = revcompose 2844 revcomposed = revcompose 2845 2846 def revsort(iterable: Iterable, key: Optional[Callable] = None) -> Iterable: 2847 return sorted(iterable, key=key, reverse=True) 2848 2849 revsorted = revsort 2850 2851 # def revsortkv(src: Dict, key: Callable = None) -> Dict: 2852 # if not key: 2853 # key = lambda kv: (kv[1], kv[0]) 2854 # return sortkv(src, key, reverse=True) 2855 2856 def revsortkv(src: Dict, key: Callable = None) -> Dict: 2857 if key is None: 2858 key = lambda x: x[1] 2859 return sortkv(src, key, reverse=True) 2860 2861 revsortedkv = revsortkv 2862 2863 def rstripdecs(s: str) -> str: 2864 ''' 2865 Ignore trailing zero decimals on number-like strings; even ignore 2866 the decimal dot if trailing. 2867 ''' 2868 2869 try: 2870 f = float(s) 2871 if isnan(f) or isinf(f): 2872 return s 2873 2874 dot = s.find('.') 2875 if dot < 0: 2876 return s 2877 2878 s = s.rstrip('0') 2879 return s[:-1] if s.endswith('.') else s 2880 except Exception: 2881 return s 2882 2883 chopdecs = rstripdecs 2884 2885 def scale(x: float, x0: float, x1: float, y0: float, y1: float) -> float: 2886 'Transform a value from a linear domain into another linear one.' 2887 return (y1 - y0) * (x - x0) / (x1 - x0) + y0 2888 2889 rescale = scale 2890 rescaled = scale 2891 scaled = scale 2892 2893 def shortened(s: str, maxlen: int, trailer: str = '') -> str: 2894 'Limit strings to the symbol-count given, including an optional trailer.' 2895 maxlen = max(maxlen, 0) 2896 return s if len(s) <= maxlen else s[:maxlen - len(trailer)] + trailer 2897 2898 def shuffled(x: Any) -> Any: 2899 'Return a shuffled copy of the list given.' 2900 y = copy(x) 2901 shuffle(y) 2902 return y 2903 2904 def split(src: Union[str, Sequence], n: Union[str, int]) -> Iterable: 2905 'Split/break a string/sequence into several chunks/parts.' 2906 2907 if isinstance(src, str) and isinstance(n, str): 2908 return src.split(n) 2909 if not (isinstance(src, (str, Sequence)) and isinstance(n, int)): 2910 raise Exception('unsupported type-pair of arguments') 2911 2912 if n < 1: 2913 return [] 2914 2915 l = len(src) 2916 if l <= n: 2917 return src.split('') if isinstance(src, str) else src 2918 2919 chunks = [] 2920 csize = int(ceil(l / n)) 2921 while len(src) > 0: 2922 chunks.append(src[:csize]) 2923 src = src[csize:] 2924 return chunks 2925 2926 broken = split 2927 splitted = split 2928 splitten = split 2929 2930 def strdrop(x: str, *what: str) -> str: 2931 'Ignore all occurrences of all substrings given.' 2932 2933 for s in what: 2934 x = x.replace(s, '') 2935 return x 2936 2937 strignore = strdrop 2938 2939 def stringify(x: Any) -> str: 2940 'Fancy alias for func dumps, named after JavaScript\'s func.' 2941 return dumps(x, separators=(', ', ': '), allow_nan=False, indent=None) 2942 2943 jsonate = stringify 2944 jsonify = stringify 2945 tojson = stringify 2946 2947 def strafter(x: str, what: str) -> str: 2948 i = x.find(what) 2949 return '' if i < 0 else x[i+len(what):] 2950 2951 def strafterlast(x: str, what: str) -> str: 2952 i = x.rfind(what) 2953 return '' if i < 0 else x[i+len(what):] 2954 2955 def strbefore(x: str, what: str) -> str: 2956 i = x.find(what) 2957 return x if i < 0 else x[:i] 2958 2959 def strbeforelast(x: str, what: str) -> str: 2960 i = x.rfind(what) 2961 return x if i < 0 else x[:i] 2962 2963 def strsince(x: str, what: str) -> str: 2964 i = x.find(what) 2965 return '' if i < 0 else x[i:] 2966 2967 def strsincelast(x: str, what: str) -> str: 2968 i = x.rfind(what) 2969 return '' if i < 0 else x[i:] 2970 2971 def struntil(x: str, what: str) -> str: 2972 i = x.find(what) 2973 return x if i < 0 else x[:i+len(what)] 2974 2975 def struntillast(x: str, what: str) -> str: 2976 i = x.rfind(what) 2977 return x if i < 0 else x[:i+len(what)] 2978 2979 struntilfinal = struntillast 2980 2981 def since(x: Union[str, Iterable], what: Any) -> Union[str, Iterable]: 2982 'Start strings/sequences with a substring/value\'s appearance.' 2983 return (strsince if isinstance(x, str) else itemssince)(x, what) 2984 2985 def sincelast(x: Union[str, Iterable], what: Any) -> Union[str, Iterable]: 2986 'Start strings/sequences with a substring/value\'s last appearance.' 2987 return (strsincelast if isinstance(x, str) else itemssincelast)(x, what) 2988 2989 sincefinal = sincelast 2990 2991 def sortk(x: Dict, key: Callable = identity, reverse: bool = False) -> Dict: 2992 keys = sorted(x.keys(), key=key, reverse=reverse) 2993 return {k: x[k] for k in keys} 2994 2995 sortkeys = sortk 2996 sortedkeys = sortk 2997 2998 def sortkv(src: Dict, key: Callable = None, reverse: bool = False) -> Dict: 2999 if key is None: 3000 key = lambda x: x[1] 3001 kv = sorted(src.items(), key=key, reverse=reverse) 3002 return {k: v for (k, v) in kv} 3003 3004 sortedkv = sortkv 3005 3006 def squeeze(s: str) -> str: 3007 ''' 3008 A more aggressive way to rid strings of extra spaces which, unlike string 3009 method strip, also turns inner runs of multiple spaces into single ones. 3010 ''' 3011 s = s.strip() 3012 s = spaces_re.sub(' ', s) 3013 s = paddable_tab_re.sub('\t', s) 3014 return s 3015 3016 squeezed = squeeze 3017 3018 def stround(x: Union[int, float], decimals: int = 6) -> str: 3019 'Format numbers into a string with the given decimal-digit count.' 3020 3021 if decimals >= 0: 3022 return f'{x:.{decimals}f}' 3023 else: 3024 return f'{round(x, decimals):.0f}' 3025 3026 def tally(src: Iterable, by: Callable = identity) -> Dict[Any, int]: 3027 ''' 3028 Count all distinct (transformed) values, the result being a dictionary 3029 whose keys are all the transformed values, and whose items are positive 3030 integers. 3031 ''' 3032 3033 if callable(src): 3034 src, by = by, src 3035 3036 tally: Dict[Any, int] = {} 3037 by = loopify(by) 3038 3039 if isinstance(src, dict): 3040 for k, v in src.items(): 3041 dk = by(k, v) 3042 if dk in tally: 3043 tally[dk] += 1 3044 else: 3045 tally[dk] = 1 3046 else: 3047 for i, v in enumerate(src): 3048 dk = by(i, v) 3049 if dk in tally: 3050 tally[dk] += 1 3051 else: 3052 tally[dk] = 1 3053 return tally 3054 3055 tallied = tally 3056 3057 def transpose(src: Any) -> Any: 3058 'Turn lists/objects inside-out like socks, so to speak.' 3059 3060 if isinstance(src, dict): 3061 return { v: k for k, v in src.items() } 3062 3063 if not arrayish(src): 3064 msg = 'transpose only supports objects or iterables of objects' 3065 raise ValueError(msg) 3066 3067 kv: Dict[Any, Any] = {} 3068 seq: List[Any] = [] 3069 3070 for e in src: 3071 if isinstance(e, dict): 3072 for k, v in e.items(): 3073 if k in kv: 3074 kv[k].append(v) 3075 else: 3076 kv[k] = [v] 3077 elif isinstance(e, Iterable): 3078 for i, v in enumerate(e): 3079 if i < len(seq): 3080 seq[i].append(v) 3081 else: 3082 seq.append([v]) 3083 else: 3084 msg = 'transpose(...): not all items are iterables/objects' 3085 raise ValueError(msg) 3086 3087 if len(kv) > 0 and len(seq) > 0: 3088 msg = 'transpose(...): mix of iterables and objects not supported' 3089 raise ValueError(msg) 3090 return kv if len(seq) == 0 else seq 3091 3092 tr = transpose 3093 transp = transpose 3094 transposed = transpose 3095 3096 def trap(x: Callable, y: Union[Callable[[Exception], Any], Any] = None) -> Any: 3097 'Try running a func, handing exceptions over to a fallback func.' 3098 3099 try: 3100 return x() if callable(x) else x 3101 except Exception as e: 3102 if callable(y): 3103 nargs = required_arg_count(y) 3104 return y(e) if nargs == 1 else y() 3105 else: 3106 return y 3107 3108 catch = trap 3109 catched = trap 3110 caught = trap 3111 noerr = trap 3112 noerror = trap 3113 noerrors = trap 3114 safe = trap 3115 save = trap 3116 saved = trap 3117 trapped = trap 3118 3119 def tsv(x: str, fn: Union[Callable, None] = None) -> Any: 3120 if fn is None: 3121 return x.split('\t') 3122 if callable(x): 3123 x, fn = fn, x 3124 return fn(x.split('\t')) 3125 3126 def typename(x: Any) -> str: 3127 if x is None: 3128 return 'null' 3129 if isinstance(x, bool): 3130 return 'boolean' 3131 if isinstance(x, str): 3132 return 'string' 3133 if isinstance(x, (int, float)): 3134 return 'number' 3135 if isinstance(x, (list, tuple)): 3136 return 'array' 3137 if isinstance(x, dict): 3138 return 'object' 3139 return type(x).__name__ 3140 3141 def typeof(x: Any) -> str: 3142 'Get a value\'s JS-like typeof type-string.' 3143 3144 if callable(x): 3145 return 'function' 3146 3147 return { 3148 bool: 'boolean', 3149 int: 'number', 3150 float: 'number', 3151 str: 'string', 3152 }.get(type(x), 'object') 3153 3154 def unixify(s: str) -> str: 3155 ''' 3156 Make plain-text `unix-style`, ignoring a leading UTF-8 BOM if present, 3157 and turning any/all CRLF byte-pairs into line-feed bytes. 3158 ''' 3159 s = s.lstrip('\xef\xbb\xbf') 3160 return s.replace('\r\n', '\n') if '\r\n' in s else s 3161 3162 def unquoted(s: str) -> str: 3163 'Ignore surrounding quotes in a string.' 3164 3165 if s.startswith('"') and s.endswith('"'): 3166 return s[1:-1] 3167 if s.startswith('\'') and s.endswith('\''): 3168 return s[1:-1] 3169 if s.startswith('`') and s.endswith('`'): 3170 return s[1:-1] 3171 if s.startswith('”') and s.endswith('“'): 3172 return s[1:-1] 3173 if s.startswith('“') and s.endswith('”'): 3174 return s[1:-1] 3175 return s 3176 3177 dequote = unquoted 3178 dequoted = unquoted 3179 3180 def until(x: Union[str, Iterable], what: Any) -> Union[str, Iterable]: 3181 'End strings/sequences with a substring/value\'s appearance.' 3182 return (struntil if isinstance(x, str) else itemsuntil)(x, what) 3183 3184 def untillast(x: Union[str, Iterable], what: Any) -> Union[str, Iterable]: 3185 'End strings/sequences with a substring/value\'s last appearance.' 3186 return (struntillast if isinstance(x, str) else itemsuntillast)(x, what) 3187 3188 untilfinal = untillast 3189 3190 3191 def wait(seconds: Union[int, float], result: Any) -> Any: 3192 'Wait the given number of seconds, before returning its latter arg.' 3193 3194 t = (int, float) 3195 if (not isinstance(seconds, t)) and isinstance(result, t): 3196 seconds, result = result, seconds 3197 sleep(seconds) 3198 return result 3199 3200 delay = wait 3201 3202 def wat(*args) -> None: 3203 'What Are These (wat) shows help/doc messages for funcs given to it.' 3204 3205 from pydoc import doc 3206 3207 c = 0 3208 w = stderr 3209 3210 for e in args: 3211 if not callable(e): 3212 continue 3213 3214 if c > 0: 3215 print(file=w) 3216 3217 print(f'\x1b[48;5;253m\x1b[38;5;26m{e.__name__:80}\x1b[0m', file=w) 3218 doc(e, output=w) 3219 c += 1 3220 3221 return Skip() 3222 3223 def wit(*args) -> None: 3224 'What Is This (wit) shows help/doc messages for funcs given to it.' 3225 return wat(*args) 3226 3227 def zoom(x: Any, *keys_indices) -> Any: 3228 for k in keys_indices: 3229 # allow int-indexing dicts the same way lists/tuples can be 3230 if isinstance(x, dict) and isinstance(k, int): 3231 l = len(x) 3232 if i < 0: 3233 i += l 3234 if i < 0 or i >= len(x): 3235 x = None 3236 continue 3237 for i, e in enumerate(x.values()): 3238 if i == k: 3239 x = e 3240 break 3241 continue 3242 3243 # regular key/index access for dicts/lists/tuples 3244 x = x[k] 3245 3246 return x 3247 3248 3249 # args is the `proper` list of arguments given to the script 3250 args = argv[1:] 3251 run_mode = '' 3252 trace_exceptions = False 3253 profile_run = False 3254 pipe_mode = False 3255 3256 if len(args) == 0: 3257 # show help message when given no arguments 3258 print(info.strip(), file=stderr) 3259 exit(0) 3260 3261 trace_opts = ( 3262 '-t', '--t', '-trace', '--trace', '-traceback', '--traceback', 3263 ) 3264 pipe_opts = ('-p', '--p', '-pipe', '--pipe') 3265 profile_opts = ('-p', '--p', '-prof', '--prof', '-profile', '--profile') 3266 3267 # handle all other leading options; the explicit help options are 3268 # handled earlier in the script 3269 while len(args) > 0: 3270 if args[0] == '--': 3271 args = args[1:] 3272 break 3273 3274 if args[0] in trace_opts: 3275 trace_exceptions = True 3276 args = args[1:] 3277 continue 3278 3279 if args[0] in pipe_opts: 3280 pipe_mode = True 3281 args = args[1:] 3282 break 3283 3284 if args[0] in profile_opts: 3285 profile_run = True 3286 args = args[1:] 3287 continue 3288 3289 s = opts2modes.get(args[0], '') 3290 if not s: 3291 break 3292 3293 run_mode = s 3294 args = args[1:] 3295 3296 inputs = [] 3297 expression = '' 3298 if len(args) > 0: 3299 expression = args[0] 3300 inputs = args[1:] 3301 3302 if not run_mode: 3303 run_mode = 'each-line' 3304 3305 if not expression and not (run_mode in ('json-lines', 'each-line')): 3306 # show help message when given no expression 3307 print(info.strip(), file=stderr) 3308 exit(0) 3309 3310 glo = globals() 3311 for e in (physics, symbols, units): 3312 for k, v in e.__dict__.items(): 3313 if not k in glo: 3314 glo[k] = v 3315 3316 exec = disabled_exec 3317 3318 3319 def handle_pipe(src, funcs): 3320 # `comprehension` expressions seem to ignore local variables: even 3321 # lambda-based workarounds fail 3322 global i, n, l, line, v, val, value, e, err, error 3323 # variable names `o` and `p` work like in the `pyp` tool, except 3324 # the pipeline steps were given as separate cmd-line arguments 3325 global o, p 3326 3327 i = 0 3328 n = 1 3329 e = err = error = None 3330 3331 for l in src: 3332 l = l.rstrip('\r\n').rstrip('\n') 3333 if i == 0: 3334 l = l.lstrip('\xef\xbb\xbf') 3335 3336 line = l 3337 o = p = prev = line 3338 # seen is used by func `once` to remember previously-given values 3339 seen.clear() 3340 3341 try: 3342 e = err = error = None 3343 v = val = value = loads(l) 3344 except BrokenPipeError as e: 3345 raise e 3346 except Exception as ex: 3347 e = err = error = ex 3348 v = val = value = Skip() 3349 3350 for f in funcs: 3351 p = f(p) 3352 if callable(p): 3353 p = p(prev) 3354 prev = p 3355 3356 res = p 3357 i += 1 3358 n += 1 3359 3360 if isinstance(res, (list, range, tuple, Generator)): 3361 for e in res: 3362 e = adapt_result(e, None) 3363 if not (e is None): 3364 print(e, flush=True) 3365 continue 3366 3367 res = adapt_result(res, line) 3368 if not (res is None): 3369 print(res, flush=True) 3370 3371 3372 try: 3373 if pipe_mode: 3374 steps = [eval(s) for s in args] 3375 compile = None 3376 eval = None 3377 exec = None 3378 open = None 3379 handle_pipe(stdin, steps) 3380 exit(0) 3381 3382 # compile the expression to speed it up, since they're all (re)run 3383 # for each line from standard input; also, handle a single-dot as 3384 # an identity expression, using the current line as is 3385 if expression in ('', '.'): 3386 expression = { 3387 'all-lines': 'lines', 3388 'all-bytes': 'data', 3389 'each-block': 'block', 3390 'each-line': 'line', 3391 'json-lines': 'data', 3392 'no-input': 'info.strip()', 3393 'whole-strings': 'value', 3394 }[run_mode] 3395 expression = compile_py(expression, expression, 'eval') 3396 3397 # `comprehension` expressions seem to ignore local variables: even 3398 # lambda-based workarounds fail 3399 i = 0 3400 c = 1 3401 nr = 1 3402 _ = None 3403 3404 fn = { 3405 'each-line': stop_normal, 3406 'each-block': stop_normal, 3407 'all-lines': stop_normal, 3408 'all-bytes': stop_normal, 3409 'json-lines': stop_json, 3410 'no-input': stop_normal, 3411 'whole-strings': stop_normal, 3412 }[run_mode] 3413 glo['halt'] = fn 3414 glo['stop'] = fn 3415 3416 fn = { 3417 'each-line': main_each_line, 3418 'each-block': main_each_block, 3419 'all-lines': main_all_lines, 3420 'all-bytes': main_all_bytes, 3421 'json-lines': main_json_lines, 3422 'no-input': main_no_input, 3423 'whole-strings': main_whole_strings, 3424 }[run_mode] 3425 3426 if fn is None: 3427 raise Exception(f'internal error: invalid run-mode {run_mode}') 3428 3429 if profile_run: 3430 from cProfile import Profile 3431 # using a profiler in a `with` context adds many irrelevant 3432 # entries to its output 3433 prof = Profile() 3434 prof.enable() 3435 fn(stdout, stdin, expression, inputs) 3436 prof.disable() 3437 prof.print_stats() 3438 else: 3439 fn(stdout, stdin, expression, inputs) 3440 except BrokenPipeError: 3441 # quit quietly, instead of showing a confusing error message 3442 stderr.close() 3443 except KeyboardInterrupt: 3444 exit(2) 3445 except Exception as e: 3446 if trace_exceptions: 3447 raise e 3448 s = str(e) 3449 s = s if s else '<generic exception>' 3450 print(f'\x1b[31m{s}\x1b[0m', file=stderr) 3451 exit(1)