lkj 0.1.45__tar.gz → 0.1.47__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.45 → lkj-0.1.47}/PKG-INFO +1 -1
- {lkj-0.1.45 → lkj-0.1.47}/lkj/strings.py +55 -19
- lkj-0.1.47/lkj/tests/__init__.py +1 -0
- lkj-0.1.47/lkj/tests/test_strings.py +48 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj.egg-info/PKG-INFO +1 -1
- {lkj-0.1.45 → lkj-0.1.47}/lkj.egg-info/SOURCES.txt +3 -1
- {lkj-0.1.45 → lkj-0.1.47}/setup.cfg +1 -1
- {lkj-0.1.45 → lkj-0.1.47}/LICENSE +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/README.md +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj/__init__.py +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj/chunking.py +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj/dicts.py +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj/filesys.py +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj/funcs.py +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj/importing.py +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj/iterables.py +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj/loggers.py +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj/misc.py +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj.egg-info/dependency_links.txt +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj.egg-info/not-zip-safe +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/lkj.egg-info/top_level.txt +0 -0
- {lkj-0.1.45 → lkj-0.1.47}/setup.py +0 -0
|
@@ -633,21 +633,59 @@ class FindReplaceTool:
|
|
|
633
633
|
print(highlight)
|
|
634
634
|
print("-" * 40)
|
|
635
635
|
else:
|
|
636
|
+
# In non-line mode, a naive snippet (characters around the match) can
|
|
637
|
+
# include newlines. If we simply print that snippet and then print the
|
|
638
|
+
# highlight line relative to the snippet start, the visual caret
|
|
639
|
+
# markers may appear under a different printed line. To avoid this,
|
|
640
|
+
# find the full line that contains the match and print the context as
|
|
641
|
+
# lines: preceding context lines, the matched line, then the highlight
|
|
642
|
+
# directly under the matched line, then the following context lines.
|
|
636
643
|
snippet_radius = 20
|
|
637
644
|
for idx, m in enumerate(self._matches):
|
|
638
645
|
start, end = m["start"], m["end"]
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
646
|
+
# Find the boundaries of the line containing the match
|
|
647
|
+
line_start = current_text.rfind("\n", 0, start) + 1
|
|
648
|
+
line_end = current_text.find("\n", end)
|
|
649
|
+
if line_end == -1:
|
|
650
|
+
line_end = len(current_text)
|
|
651
|
+
|
|
652
|
+
# Context window (characters) around the matched line
|
|
653
|
+
context_start = max(0, line_start - snippet_radius)
|
|
654
|
+
context_end = min(len(current_text), line_end + snippet_radius)
|
|
655
|
+
context_text = current_text[context_start:context_end]
|
|
656
|
+
|
|
657
|
+
# Split into lines while preserving newlines so output looks natural
|
|
658
|
+
context_lines = context_text.splitlines(True)
|
|
659
|
+
|
|
660
|
+
# Determine which line in context_lines contains the match
|
|
661
|
+
acc = 0
|
|
662
|
+
match_line_idx = 0
|
|
663
|
+
rel_pos_in_context = line_start - context_start
|
|
664
|
+
for i, line in enumerate(context_lines):
|
|
665
|
+
if acc + len(line) > rel_pos_in_context:
|
|
666
|
+
match_line_idx = i
|
|
667
|
+
break
|
|
668
|
+
acc += len(line)
|
|
669
|
+
|
|
645
670
|
print(f"Match {idx} (around line {m['line_number']+1}):")
|
|
646
|
-
print
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
)
|
|
650
|
-
|
|
671
|
+
# Print each context line. For the match line, print a highlight
|
|
672
|
+
# line immediately after it so the caret markers line up under
|
|
673
|
+
# the matched text.
|
|
674
|
+
for i, line in enumerate(context_lines):
|
|
675
|
+
# Print the context line as-is (it may or may not contain a newline)
|
|
676
|
+
print(line, end="")
|
|
677
|
+
if i == match_line_idx:
|
|
678
|
+
# If the printed line did not end with a newline, ensure the
|
|
679
|
+
# caret highlight appears on the next line so it lines up
|
|
680
|
+
# visually beneath the matched characters.
|
|
681
|
+
if not line.endswith("\n"):
|
|
682
|
+
print()
|
|
683
|
+
# position of match within the printed line
|
|
684
|
+
pos_in_line = start - line_start
|
|
685
|
+
highlight = " " * pos_in_line + self.highlight_char * (
|
|
686
|
+
end - start
|
|
687
|
+
)
|
|
688
|
+
print(highlight)
|
|
651
689
|
print("-" * 40)
|
|
652
690
|
|
|
653
691
|
def replace_one(self, match_index: int, replacement: Replacement) -> None:
|
|
@@ -741,7 +779,7 @@ def print_list(
|
|
|
741
779
|
sep: str = ", ",
|
|
742
780
|
line_prefix: str = "",
|
|
743
781
|
items_per_line=None,
|
|
744
|
-
show_count=
|
|
782
|
+
show_count: Union[bool, Callable[[int], str]] = False,
|
|
745
783
|
title=None,
|
|
746
784
|
print_func=print,
|
|
747
785
|
):
|
|
@@ -755,7 +793,7 @@ def print_list(
|
|
|
755
793
|
sep: Separator for items
|
|
756
794
|
line_prefix: Prefix for each line
|
|
757
795
|
items_per_line: For columns style, how many items per line
|
|
758
|
-
show_count: Whether to
|
|
796
|
+
show_count: Whether to prefix with the count of items
|
|
759
797
|
title: Optional title to display before the list
|
|
760
798
|
print_func: Function to use for printing. Defaults to print.
|
|
761
799
|
If None, returns the string instead of printing.
|
|
@@ -765,19 +803,16 @@ def print_list(
|
|
|
765
803
|
|
|
766
804
|
# Wrapped style (default)
|
|
767
805
|
>>> print_list(items, max_width=30)
|
|
768
|
-
List (6 items):
|
|
769
806
|
apple, banana, cherry, date,
|
|
770
807
|
elderberry, fig
|
|
771
808
|
|
|
772
809
|
# Columns style
|
|
773
810
|
>>> print_list(items, style="columns", items_per_line=3)
|
|
774
|
-
List (6 items):
|
|
775
811
|
apple banana cherry
|
|
776
812
|
date elderberry fig
|
|
777
813
|
|
|
778
814
|
# Numbered style
|
|
779
815
|
>>> print_list(items, style="numbered")
|
|
780
|
-
List (6 items):
|
|
781
816
|
1. apple
|
|
782
817
|
2. banana
|
|
783
818
|
3. cherry
|
|
@@ -787,7 +822,6 @@ def print_list(
|
|
|
787
822
|
|
|
788
823
|
# Bullet style
|
|
789
824
|
>>> print_list(items, style="bullet")
|
|
790
|
-
List (6 items):
|
|
791
825
|
• apple
|
|
792
826
|
• banana
|
|
793
827
|
• cherry
|
|
@@ -796,7 +830,7 @@ def print_list(
|
|
|
796
830
|
• fig
|
|
797
831
|
|
|
798
832
|
# Return string instead of printing
|
|
799
|
-
>>> result = print_list(items, style="numbered", print_func=None)
|
|
833
|
+
>>> result = print_list(items, style="numbered", print_func=None, show_count=True)
|
|
800
834
|
>>> print(result)
|
|
801
835
|
List (6 items):
|
|
802
836
|
1. apple
|
|
@@ -847,6 +881,8 @@ def print_list(
|
|
|
847
881
|
print_func=print_func,
|
|
848
882
|
)
|
|
849
883
|
items = list(items) # Convert to list if it's an iterable
|
|
884
|
+
if show_count is True:
|
|
885
|
+
show_count = lambda n_items: f"List ({n_items} items):"
|
|
850
886
|
|
|
851
887
|
# Handle print_func=None by using StringAppender
|
|
852
888
|
if print_func is None:
|
|
@@ -860,7 +896,7 @@ def print_list(
|
|
|
860
896
|
if title:
|
|
861
897
|
print_func(title)
|
|
862
898
|
elif show_count:
|
|
863
|
-
print_func(
|
|
899
|
+
print_func(show_count(len(items)))
|
|
864
900
|
|
|
865
901
|
if not items:
|
|
866
902
|
print_func(f"{line_prefix}(empty list)")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for lkj"""
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from lkj.strings import FindReplaceTool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def capture_print(func, *args, **kwargs):
|
|
9
|
+
old_stdout = sys.stdout
|
|
10
|
+
try:
|
|
11
|
+
sys.stdout = io.StringIO()
|
|
12
|
+
func(*args, **kwargs)
|
|
13
|
+
return sys.stdout.getvalue()
|
|
14
|
+
finally:
|
|
15
|
+
sys.stdout = old_stdout
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_find_and_print_matches_highlight_under_match():
|
|
19
|
+
text = "apple banana apple\nsome other line\n"
|
|
20
|
+
tool = FindReplaceTool(text, line_mode=False)
|
|
21
|
+
out = capture_print(tool.find_and_print_matches, r"apple")
|
|
22
|
+
|
|
23
|
+
# We expect for the first match that the matched line appears, then the
|
|
24
|
+
# highlight line directly under it, then following context lines. Ensure the
|
|
25
|
+
# highlight caret appears on its own line immediately after the matched line.
|
|
26
|
+
# Locate the first occurrence of the matched line and the caret line that follows.
|
|
27
|
+
lines = out.splitlines()
|
|
28
|
+
|
|
29
|
+
# Find the index of the line that contains the first printed snippet for match 0
|
|
30
|
+
# It should contain 'apple banana apple'
|
|
31
|
+
match0_idx = None
|
|
32
|
+
for i, line in enumerate(lines):
|
|
33
|
+
if "apple banana apple" in line:
|
|
34
|
+
# Ensure the next non-empty line is the highlight
|
|
35
|
+
match0_idx = i
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
assert match0_idx is not None, "Did not find the matched line in output"
|
|
39
|
+
|
|
40
|
+
# The next line should be the caret highlight (contains at least one '^')
|
|
41
|
+
assert any(
|
|
42
|
+
c == "^" for c in lines[match0_idx + 1]
|
|
43
|
+
), "Highlight not directly under matched line"
|
|
44
|
+
|
|
45
|
+
# For completeness, ensure that the text 'some other line' appears after the caret
|
|
46
|
+
assert "some other line" in "\n".join(
|
|
47
|
+
lines[match0_idx + 2 :]
|
|
48
|
+
), "Following context not printed after highlight"
|
|
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
|
|
File without changes
|
|
File without changes
|