lkj 0.1.35__tar.gz → 0.1.37__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lkj-0.1.35 → lkj-0.1.37}/PKG-INFO +31 -2
- {lkj-0.1.35 → lkj-0.1.37}/README.md +30 -1
- {lkj-0.1.35 → lkj-0.1.37}/lkj/__init__.py +1 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj/dicts.py +2 -2
- {lkj-0.1.35 → lkj-0.1.37}/lkj/strings.py +278 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj.egg-info/PKG-INFO +31 -2
- {lkj-0.1.35 → lkj-0.1.37}/setup.cfg +1 -1
- {lkj-0.1.35 → lkj-0.1.37}/LICENSE +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj/chunking.py +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj/filesys.py +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj/funcs.py +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj/importing.py +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj/iterables.py +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj/loggers.py +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj/misc.py +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj.egg-info/SOURCES.txt +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj.egg-info/dependency_links.txt +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj.egg-info/not-zip-safe +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/lkj.egg-info/top_level.txt +0 -0
- {lkj-0.1.35 → lkj-0.1.37}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lkj
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.37
|
|
4
4
|
Summary: A dump of homeless useful utils
|
|
5
5
|
Home-page: https://github.com/thorwhalen/lkj
|
|
6
6
|
Author: Thor Whalen
|
|
@@ -25,9 +25,38 @@ That is, modules are all self contained (so can easily be copy-paste-vendored
|
|
|
25
25
|
Further, many functions will contain their own imports: Those functions can even be
|
|
26
26
|
copy-paste-vendored by just copying the function body.
|
|
27
27
|
|
|
28
|
-
|
|
29
28
|
# Examples of utils
|
|
30
29
|
|
|
30
|
+
## Find and replace
|
|
31
|
+
|
|
32
|
+
`FindReplaceTool` is a general-purpose find-and-replace tool that can treat the input text as a continuous sequence of characters,
|
|
33
|
+
even if operations such as viewing context are performed line by line.
|
|
34
|
+
|
|
35
|
+
The basic usage is
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
FindReplaceTool("apple banana apple").find_and_print_matches(r'apple')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Match 0 (around line 1):
|
|
42
|
+
apple banana apple
|
|
43
|
+
^^^^^
|
|
44
|
+
----------------------------------------
|
|
45
|
+
Match 1 (around line 1):
|
|
46
|
+
apple banana apple
|
|
47
|
+
^^^^^
|
|
48
|
+
----------------------------------------
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
FindReplaceTool("apple banana apple").find_and_replace(r'apple', "orange")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
'orange banana orange'
|
|
55
|
+
|
|
56
|
+
[See more examples in documentation](https://i2mint.github.io/lkj/module_docs/lkj/strings.html#lkj.strings.FindReplaceTool)
|
|
57
|
+
|
|
58
|
+
[See here a example of how I used this to edit my CI yamls](https://github.com/i2mint/lkj/discussions/4#discussioncomment-12104547)
|
|
59
|
+
|
|
31
60
|
## loggers
|
|
32
61
|
|
|
33
62
|
### clog
|
|
@@ -14,9 +14,38 @@ That is, modules are all self contained (so can easily be copy-paste-vendored
|
|
|
14
14
|
Further, many functions will contain their own imports: Those functions can even be
|
|
15
15
|
copy-paste-vendored by just copying the function body.
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
# Examples of utils
|
|
19
18
|
|
|
19
|
+
## Find and replace
|
|
20
|
+
|
|
21
|
+
`FindReplaceTool` is a general-purpose find-and-replace tool that can treat the input text as a continuous sequence of characters,
|
|
22
|
+
even if operations such as viewing context are performed line by line.
|
|
23
|
+
|
|
24
|
+
The basic usage is
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
FindReplaceTool("apple banana apple").find_and_print_matches(r'apple')
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Match 0 (around line 1):
|
|
31
|
+
apple banana apple
|
|
32
|
+
^^^^^
|
|
33
|
+
----------------------------------------
|
|
34
|
+
Match 1 (around line 1):
|
|
35
|
+
apple banana apple
|
|
36
|
+
^^^^^
|
|
37
|
+
----------------------------------------
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
FindReplaceTool("apple banana apple").find_and_replace(r'apple', "orange")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
'orange banana orange'
|
|
44
|
+
|
|
45
|
+
[See more examples in documentation](https://i2mint.github.io/lkj/module_docs/lkj/strings.html#lkj.strings.FindReplaceTool)
|
|
46
|
+
|
|
47
|
+
[See here a example of how I used this to edit my CI yamls](https://github.com/i2mint/lkj/discussions/4#discussioncomment-12104547)
|
|
48
|
+
|
|
20
49
|
## loggers
|
|
21
50
|
|
|
22
51
|
### clog
|
|
@@ -16,6 +16,7 @@ from lkj.dicts import (
|
|
|
16
16
|
)
|
|
17
17
|
from lkj.filesys import get_app_data_dir, get_watermarked_dir, enable_sourcing_from_file
|
|
18
18
|
from lkj.strings import (
|
|
19
|
+
FindReplaceTool, # Tool for finding and replacing substrings in a string
|
|
19
20
|
indent_lines, # Indent all lines of a string
|
|
20
21
|
most_common_indent, # Get the most common indent of a multiline string
|
|
21
22
|
regex_based_substitution,
|
|
@@ -18,8 +18,8 @@ def inclusive_subdict(d, include):
|
|
|
18
18
|
include (set): The set of keys to include in the new dictionary.
|
|
19
19
|
|
|
20
20
|
Example:
|
|
21
|
-
|
|
22
|
-
{'a': 1, 'c': 3}
|
|
21
|
+
|
|
22
|
+
>>> assert inclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'}) == {'a': 1, 'c': 3}
|
|
23
23
|
|
|
24
24
|
"""
|
|
25
25
|
return {k: d[k] for k in d.keys() & include}
|
|
@@ -408,3 +408,281 @@ def unique_affixes(
|
|
|
408
408
|
# Postprocess affixes using egress
|
|
409
409
|
affixes = list(map(egress, affixes))
|
|
410
410
|
return affixes
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
from typing import Union, Callable, Dict, Any
|
|
414
|
+
|
|
415
|
+
# A match is represented as a dictionary (keys like "start", "end", etc.)
|
|
416
|
+
# and the replacement is either a static string or a callable that takes that
|
|
417
|
+
# dictionary and returns a string.
|
|
418
|
+
Replacement = Union[str, Callable[[Dict[str, Any]], str]]
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class FindReplaceTool:
|
|
422
|
+
r"""
|
|
423
|
+
A general-purpose find-and-replace tool that can treat the input text
|
|
424
|
+
as a continuous sequence of characters, even if operations such as viewing
|
|
425
|
+
context are performed line by line. The tool can analyze matches based on
|
|
426
|
+
a user-supplied regular expression, navigate through the matches with context,
|
|
427
|
+
and perform replacements either interactively or in bulk. Replacements can be
|
|
428
|
+
provided as either a static string or via a callback function that receives details
|
|
429
|
+
of the match.
|
|
430
|
+
|
|
431
|
+
Instead of keeping a single modified text, this version maintains a history of
|
|
432
|
+
text versions in self._text_versions, where self._text_versions[0] is the original
|
|
433
|
+
text and self._text_versions[-1] is the current text. Each edit is performed on the
|
|
434
|
+
current version and appended to the history. Additional methods allow reverting changes.
|
|
435
|
+
|
|
436
|
+
1: Basic usage
|
|
437
|
+
-----------------------------------------------------
|
|
438
|
+
>>> FindReplaceTool("apple banana apple").find_and_print_matches(r'apple')
|
|
439
|
+
Match 0 (around line 1):
|
|
440
|
+
apple banana apple
|
|
441
|
+
^^^^^
|
|
442
|
+
----------------------------------------
|
|
443
|
+
Match 1 (around line 1):
|
|
444
|
+
apple banana apple
|
|
445
|
+
^^^^^
|
|
446
|
+
----------------------------------------
|
|
447
|
+
>>> FindReplaceTool("apple banana apple").find_and_replace(r'apple', "orange")
|
|
448
|
+
'orange banana orange'
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
2: Using line_mode=True with a static replacement.
|
|
452
|
+
--------------------------------------------------------
|
|
453
|
+
>>> text1 = "apple\nbanana apple\ncherry"
|
|
454
|
+
>>> tool = FindReplaceTool(text1, line_mode=True, flags=re.MULTILINE)
|
|
455
|
+
>>> import re
|
|
456
|
+
>>> # Find all occurrences of "apple" (two in total).
|
|
457
|
+
>>> _ = tool.analyze(r'apple')
|
|
458
|
+
>>> len(tool._matches)
|
|
459
|
+
2
|
|
460
|
+
>>> # Replace the first occurrence ("apple" on the first line) with "orange".
|
|
461
|
+
>>> tool.replace_one(0, "orange").get_modified_text()
|
|
462
|
+
'orange\nbanana apple\ncherry'
|
|
463
|
+
|
|
464
|
+
3: Using line_mode=False with a callback replacement.
|
|
465
|
+
-----------------------------------------------------------
|
|
466
|
+
>>> text2 = "apple banana apple"
|
|
467
|
+
>>> tool2 = FindReplaceTool(text2, line_mode=False)
|
|
468
|
+
>>> # Find all occurrences of "apple" in the continuous text.
|
|
469
|
+
>>> len(tool2.analyze(r'apple')._matches)
|
|
470
|
+
2
|
|
471
|
+
>>> # Define a callback that converts each matched text to uppercase.
|
|
472
|
+
>>> def to_upper(match):
|
|
473
|
+
... return match["matched_text"].upper()
|
|
474
|
+
>>> tool2.replace_all(to_upper).get_modified_text()
|
|
475
|
+
'APPLE banana APPLE'
|
|
476
|
+
|
|
477
|
+
4: Reverting changes.
|
|
478
|
+
---------------------------
|
|
479
|
+
>>> text3 = "one two three"
|
|
480
|
+
>>> tool3 = FindReplaceTool(text3)
|
|
481
|
+
>>> import re
|
|
482
|
+
>>> # Analyze to match the first word "one" (at the start of the text).
|
|
483
|
+
>>> tool3.analyze(r'^one').replace_one(0, "ONE").get_modified_text()
|
|
484
|
+
'ONE two three'
|
|
485
|
+
>>> # Revert the edit.
|
|
486
|
+
>>> tool3.revert()
|
|
487
|
+
'one two three'
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
def __init__(
|
|
491
|
+
self,
|
|
492
|
+
text: str,
|
|
493
|
+
*,
|
|
494
|
+
line_mode: bool = False,
|
|
495
|
+
flags: int = 0,
|
|
496
|
+
show_line_numbers: bool = True,
|
|
497
|
+
context_size: int = 2,
|
|
498
|
+
highlight_char: str = "^",
|
|
499
|
+
):
|
|
500
|
+
# Maintain a list of text versions; the first element is the original text.
|
|
501
|
+
self._text_versions = [text]
|
|
502
|
+
self.line_mode = line_mode
|
|
503
|
+
self.flags = flags
|
|
504
|
+
self.show_line_numbers = show_line_numbers
|
|
505
|
+
self.context_size = context_size
|
|
506
|
+
self.highlight_char = highlight_char
|
|
507
|
+
|
|
508
|
+
# Internal storage for matches; each entry is a dict with:
|
|
509
|
+
# "start": start offset in the current text,
|
|
510
|
+
# "end": end offset in the current text,
|
|
511
|
+
# "matched_text": the text that was matched,
|
|
512
|
+
# "groups": any named groups from the regex,
|
|
513
|
+
# "line_number": the line number where the match occurs.
|
|
514
|
+
self._matches = []
|
|
515
|
+
|
|
516
|
+
# ----------------------------------------------------------------------------------
|
|
517
|
+
# Main methods
|
|
518
|
+
|
|
519
|
+
# TODO: Would like to have these functions be stateless
|
|
520
|
+
def find_and_print_matches(self, pattern: str) -> None:
|
|
521
|
+
"""
|
|
522
|
+
Searches the current text (the last version) for occurrences matching the given
|
|
523
|
+
regular expression. Any match data (including group captures) is stored internally.
|
|
524
|
+
"""
|
|
525
|
+
return self.analyze(pattern).view_matches()
|
|
526
|
+
|
|
527
|
+
def find_and_replace(self, pattern: str, replacement: Replacement) -> None:
|
|
528
|
+
"""
|
|
529
|
+
Searches the current text (the last version) for occurrences matching the given
|
|
530
|
+
regular expression. Any match data (including group captures) is stored internally.
|
|
531
|
+
"""
|
|
532
|
+
return self.analyze(pattern).replace_all(replacement).get_modified_text()
|
|
533
|
+
|
|
534
|
+
# ----------------------------------------------------------------------------------
|
|
535
|
+
# Advanced methods
|
|
536
|
+
|
|
537
|
+
def analyze(self, pattern: str) -> None:
|
|
538
|
+
"""
|
|
539
|
+
Searches the current text (the last version) for occurrences matching the given
|
|
540
|
+
regular expression. Any match data (including group captures) is stored internally.
|
|
541
|
+
"""
|
|
542
|
+
import re
|
|
543
|
+
|
|
544
|
+
self._matches.clear()
|
|
545
|
+
current_text = self._text_versions[-1]
|
|
546
|
+
for match in re.finditer(pattern, current_text, self.flags):
|
|
547
|
+
match_data = {
|
|
548
|
+
"start": match.start(),
|
|
549
|
+
"end": match.end(),
|
|
550
|
+
"matched_text": match.group(0),
|
|
551
|
+
"groups": match.groupdict(),
|
|
552
|
+
"line_number": current_text.count("\n", 0, match.start()),
|
|
553
|
+
}
|
|
554
|
+
self._matches.append(match_data)
|
|
555
|
+
|
|
556
|
+
return self
|
|
557
|
+
|
|
558
|
+
def view_matches(self) -> None:
|
|
559
|
+
"""
|
|
560
|
+
Displays all stored matches along with surrounding context. When line_mode
|
|
561
|
+
is enabled, the context is provided in full lines with (optionally) line numbers,
|
|
562
|
+
and a line is added below the matched line to indicate the matched portion.
|
|
563
|
+
In non-line mode, a snippet of characters around the match is shown.
|
|
564
|
+
"""
|
|
565
|
+
current_text = self._text_versions[-1]
|
|
566
|
+
if not self._matches:
|
|
567
|
+
print("No matches found.")
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
if self.line_mode:
|
|
571
|
+
lines = current_text.splitlines()
|
|
572
|
+
for idx, m in enumerate(self._matches):
|
|
573
|
+
line_num = m["line_number"]
|
|
574
|
+
start_pos = m["start"]
|
|
575
|
+
end_pos = m["end"]
|
|
576
|
+
start_context = max(0, line_num - self.context_size)
|
|
577
|
+
end_context = min(len(lines), line_num + self.context_size + 1)
|
|
578
|
+
print(f"Match {idx} at line {line_num+1}:")
|
|
579
|
+
for ln in range(start_context, end_context):
|
|
580
|
+
prefix = f"{ln+1:>4} " if self.show_line_numbers else ""
|
|
581
|
+
print(prefix + lines[ln])
|
|
582
|
+
# For the match line, mark the position of the match.
|
|
583
|
+
if ln == line_num:
|
|
584
|
+
pos_in_line = start_pos - (
|
|
585
|
+
len("\n".join(lines[:ln])) + (1 if ln > 0 else 0)
|
|
586
|
+
)
|
|
587
|
+
highlight = " " * (
|
|
588
|
+
len(prefix) + pos_in_line
|
|
589
|
+
) + self.highlight_char * (end_pos - start_pos)
|
|
590
|
+
print(highlight)
|
|
591
|
+
print("-" * 40)
|
|
592
|
+
else:
|
|
593
|
+
snippet_radius = 20
|
|
594
|
+
for idx, m in enumerate(self._matches):
|
|
595
|
+
start, end = m["start"], m["end"]
|
|
596
|
+
if self.line_mode:
|
|
597
|
+
snippet_start = current_text.rfind("\n", 0, start) + 1
|
|
598
|
+
else:
|
|
599
|
+
snippet_start = max(0, start - snippet_radius)
|
|
600
|
+
snippet_end = min(len(current_text), end + snippet_radius)
|
|
601
|
+
snippet = current_text[snippet_start:snippet_end]
|
|
602
|
+
print(f"Match {idx} (around line {m['line_number']+1}):")
|
|
603
|
+
print(snippet)
|
|
604
|
+
highlight = " " * (start - snippet_start) + self.highlight_char * (
|
|
605
|
+
end - start
|
|
606
|
+
)
|
|
607
|
+
print(highlight)
|
|
608
|
+
print("-" * 40)
|
|
609
|
+
|
|
610
|
+
def replace_one(self, match_index: int, replacement: Replacement) -> None:
|
|
611
|
+
"""
|
|
612
|
+
Replaces a single match, identified by match_index, with a new string.
|
|
613
|
+
The 'replacement' argument may be either a static string or a callable.
|
|
614
|
+
When it is a callable, it is called with a dictionary containing the match data
|
|
615
|
+
(including any captured groups) and should return the replacement string.
|
|
616
|
+
The replacement is performed on the current text version, and the new text is
|
|
617
|
+
appended as a new version in the history.
|
|
618
|
+
"""
|
|
619
|
+
|
|
620
|
+
if match_index < 0 or match_index >= len(self._matches):
|
|
621
|
+
print(f"Invalid match index: {match_index}")
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
m = self._matches[match_index]
|
|
625
|
+
start, end = m["start"], m["end"]
|
|
626
|
+
current_text = self._text_versions[-1]
|
|
627
|
+
|
|
628
|
+
# Determine the replacement string.
|
|
629
|
+
if callable(replacement):
|
|
630
|
+
new_replacement = replacement(m)
|
|
631
|
+
else:
|
|
632
|
+
new_replacement = replacement
|
|
633
|
+
|
|
634
|
+
# Create the new text version.
|
|
635
|
+
new_text = current_text[:start] + new_replacement + current_text[end:]
|
|
636
|
+
self._text_versions.append(new_text)
|
|
637
|
+
offset_diff = len(new_replacement) - (end - start)
|
|
638
|
+
|
|
639
|
+
# Update offsets for subsequent matches (so they refer to the new text version).
|
|
640
|
+
for i in range(match_index + 1, len(self._matches)):
|
|
641
|
+
self._matches[i]["start"] += offset_diff
|
|
642
|
+
self._matches[i]["end"] += offset_diff
|
|
643
|
+
|
|
644
|
+
# Update the current match record.
|
|
645
|
+
m["end"] = start + len(new_replacement)
|
|
646
|
+
m["matched_text"] = new_replacement
|
|
647
|
+
|
|
648
|
+
return self
|
|
649
|
+
|
|
650
|
+
def replace_all(self, replacement: Replacement) -> None:
|
|
651
|
+
"""
|
|
652
|
+
Replaces all stored matches in the current text version. The 'replacement' argument may
|
|
653
|
+
be a static string or a callable (see replace_one for details). Replacements are performed
|
|
654
|
+
from the last match to the first, so that earlier offsets are not affected.
|
|
655
|
+
"""
|
|
656
|
+
for idx in reversed(range(len(self._matches))):
|
|
657
|
+
self.replace_one(idx, replacement)
|
|
658
|
+
|
|
659
|
+
return self
|
|
660
|
+
|
|
661
|
+
def get_original_text(self) -> str:
|
|
662
|
+
"""Returns the original text (first version)."""
|
|
663
|
+
return self._text_versions[0]
|
|
664
|
+
|
|
665
|
+
def get_modified_text(self) -> str:
|
|
666
|
+
"""Returns the current (latest) text version."""
|
|
667
|
+
return self._text_versions[-1]
|
|
668
|
+
|
|
669
|
+
def revert(self, steps: int = 1):
|
|
670
|
+
"""
|
|
671
|
+
Reverts the current text version by removing the last 'steps' versions
|
|
672
|
+
from the history. The original text (version 0) is never removed.
|
|
673
|
+
Returns the new current text.
|
|
674
|
+
|
|
675
|
+
>>> text = "one two three"
|
|
676
|
+
>>> tool = FindReplaceTool(text)
|
|
677
|
+
>>> import re
|
|
678
|
+
>>> tool.analyze(r'^one').replace_one(0, "ONE").get_modified_text()
|
|
679
|
+
'ONE two three'
|
|
680
|
+
>>> tool.revert()
|
|
681
|
+
'one two three'
|
|
682
|
+
"""
|
|
683
|
+
if steps < 1:
|
|
684
|
+
return self.get_modified_text()
|
|
685
|
+
while steps > 0 and len(self._text_versions) > 1:
|
|
686
|
+
self._text_versions.pop()
|
|
687
|
+
steps -= 1
|
|
688
|
+
return self.get_modified_text()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lkj
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.37
|
|
4
4
|
Summary: A dump of homeless useful utils
|
|
5
5
|
Home-page: https://github.com/thorwhalen/lkj
|
|
6
6
|
Author: Thor Whalen
|
|
@@ -25,9 +25,38 @@ That is, modules are all self contained (so can easily be copy-paste-vendored
|
|
|
25
25
|
Further, many functions will contain their own imports: Those functions can even be
|
|
26
26
|
copy-paste-vendored by just copying the function body.
|
|
27
27
|
|
|
28
|
-
|
|
29
28
|
# Examples of utils
|
|
30
29
|
|
|
30
|
+
## Find and replace
|
|
31
|
+
|
|
32
|
+
`FindReplaceTool` is a general-purpose find-and-replace tool that can treat the input text as a continuous sequence of characters,
|
|
33
|
+
even if operations such as viewing context are performed line by line.
|
|
34
|
+
|
|
35
|
+
The basic usage is
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
FindReplaceTool("apple banana apple").find_and_print_matches(r'apple')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Match 0 (around line 1):
|
|
42
|
+
apple banana apple
|
|
43
|
+
^^^^^
|
|
44
|
+
----------------------------------------
|
|
45
|
+
Match 1 (around line 1):
|
|
46
|
+
apple banana apple
|
|
47
|
+
^^^^^
|
|
48
|
+
----------------------------------------
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
FindReplaceTool("apple banana apple").find_and_replace(r'apple', "orange")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
'orange banana orange'
|
|
55
|
+
|
|
56
|
+
[See more examples in documentation](https://i2mint.github.io/lkj/module_docs/lkj/strings.html#lkj.strings.FindReplaceTool)
|
|
57
|
+
|
|
58
|
+
[See here a example of how I used this to edit my CI yamls](https://github.com/i2mint/lkj/discussions/4#discussioncomment-12104547)
|
|
59
|
+
|
|
31
60
|
## loggers
|
|
32
61
|
|
|
33
62
|
### clog
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|