lkj 0.1.45__py3-none-any.whl → 0.1.47__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lkj/strings.py CHANGED
@@ -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
- if self.line_mode:
640
- snippet_start = current_text.rfind("\n", 0, start) + 1
641
- else:
642
- snippet_start = max(0, start - snippet_radius)
643
- snippet_end = min(len(current_text), end + snippet_radius)
644
- snippet = current_text[snippet_start:snippet_end]
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(snippet)
647
- highlight = " " * (start - snippet_start) + self.highlight_char * (
648
- end - start
649
- )
650
- print(highlight)
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=True,
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 show the count of items
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(f"List ({len(items)} items):")
899
+ print_func(show_count(len(items)))
864
900
 
865
901
  if not items:
866
902
  print_func(f"{line_prefix}(empty list)")
lkj/tests/__init__.py ADDED
@@ -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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lkj
3
- Version: 0.1.45
3
+ Version: 0.1.47
4
4
  Summary: A dump of homeless useful utils
5
5
  Home-page: https://github.com/thorwhalen/lkj
6
6
  Author: Thor Whalen
@@ -7,9 +7,11 @@ lkj/importing.py,sha256=TcW3qUDmw7jqswpxXnksjlHkkbOJq70NbUk1ZyaafT0,6658
7
7
  lkj/iterables.py,sha256=9jeO36w-IGiZryge7JKgXZOGZAgehUvhwKV3nHPcZWk,2801
8
8
  lkj/loggers.py,sha256=ImmBdacz89Lvb3dg_xI5jOct_44rSRv0hNI_CVehy60,13706
9
9
  lkj/misc.py,sha256=IZf05tkl0cgiMgBwCMH5cLSC59fRXwnemPRo8G0OxQg,1436
10
- lkj/strings.py,sha256=aib3KLFBuHl2WuDI2m7xC2JXM9E2-fk9CkXU7Pld6VU,41459
11
- lkj-0.1.45.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
- lkj-0.1.45.dist-info/METADATA,sha256=J8VwBFxfxTERN8kVlzKROEz3kkz0FvILqflc6rFVxeI,8221
13
- lkj-0.1.45.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
14
- lkj-0.1.45.dist-info/top_level.txt,sha256=3DZOUwYmyurJFBXQCvCmEIVm8z2b42O5Sx3RDQyePfg,4
15
- lkj-0.1.45.dist-info/RECORD,,
10
+ lkj/strings.py,sha256=eFPA1EzEyLaF8wPk0Srj8rzrdEN9GNXc03wfvV4MOGU,43744
11
+ lkj/tests/__init__.py,sha256=kReYfWiyz1T79AuZAy5m4PIBj3Oj_Dlc0E-T8HVQ504,20
12
+ lkj/tests/test_strings.py,sha256=Ix54io8WqWmYTY4guBgZShtKabtjd5uVpdxzxwJkgNA,1707
13
+ lkj-0.1.47.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
14
+ lkj-0.1.47.dist-info/METADATA,sha256=bpcIVJxkJl8wIa2ycVWRmbhVJ3-o-5y-S46A3wepHeo,8221
15
+ lkj-0.1.47.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
16
+ lkj-0.1.47.dist-info/top_level.txt,sha256=3DZOUwYmyurJFBXQCvCmEIVm8z2b42O5Sx3RDQyePfg,4
17
+ lkj-0.1.47.dist-info/RECORD,,
File without changes
File without changes