lkj 0.1.43__py3-none-any.whl → 0.1.44__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/__init__.py CHANGED
@@ -17,6 +17,7 @@ from lkj.dicts import (
17
17
  )
18
18
  from lkj.filesys import get_app_data_dir, get_watermarked_dir, enable_sourcing_from_file
19
19
  from lkj.strings import (
20
+ print_list, # Print a list in a nice format (or get a string to process yourself)
20
21
  FindReplaceTool, # Tool for finding and replacing substrings in a string
21
22
  indent_lines, # Indent all lines of a string
22
23
  most_common_indent, # Get the most common indent of a multiline string
lkj/strings.py CHANGED
@@ -1,6 +1,49 @@
1
- """Utils for strings"""
1
+ """
2
+ String Utilities Module
3
+
4
+ This module provides a comprehensive set of utility functions and classes for working with strings in Python.
5
+ It includes tools for string manipulation, formatting, pretty-printing, and find/replace operations.
6
+
7
+ Core Components:
8
+
9
+ - StringAppender: A helper class for collecting strings, useful for capturing output that would otherwise be printed.
10
+ - indent_lines: Indents each line of a string by a specified prefix.
11
+ - most_common_indent: Determines the most common indentation used in a multi-line string.
12
+ - FindReplaceTool: A class for advanced find-and-replace operations on strings, supporting regular expressions, match history, and undo functionality.
13
+
14
+ Pretty-Printing Functions:
15
+
16
+ - print_list: Prints lists in various human-friendly formats (wrapped, columns, numbered, bullet, table, compact), with options for width, separators, and custom print functions.
17
+ - print_list.as_table: Formats and prints a list (or list of lists) as a table, with optional headers and alignment.
18
+ - print_list.summary: Prints a summary of a list, showing first few and last few items if the list is long.
19
+ - print_list.compact, print_list.wrapped, print_list.columns, print_list.numbered, print_list.bullets: Convenience methods using print_list's partial functionality for common display styles.
20
+
21
+ These utilities are designed to make it easier to display, format, and manipulate strings and collections of strings in a readable and flexible way.
22
+ """
2
23
 
3
24
  import re
25
+ from typing import Iterable, Sequence, Callable, Optional, Any, Literal
26
+ from functools import partial
27
+
28
+
29
+ class StringAppender:
30
+ """Helper class to collect strings instead of printing them directly."""
31
+
32
+ def __init__(self, separator="\n"):
33
+ self.lines = []
34
+ self.separator = separator
35
+
36
+ def __call__(self, text):
37
+ """Append text to the internal list."""
38
+ self.lines.append(str(text))
39
+
40
+ def __str__(self):
41
+ """Return the collected string."""
42
+ return self.separator.join(self.lines)
43
+
44
+ def get_string(self):
45
+ """Alternative way to get the string."""
46
+ return str(self)
4
47
 
5
48
 
6
49
  def indent_lines(string: str, indent: str, *, line_sep="\n") -> str:
@@ -686,3 +729,446 @@ class FindReplaceTool:
686
729
  self._text_versions.pop()
687
730
  steps -= 1
688
731
  return self.get_modified_text()
732
+
733
+
734
+ def print_list(
735
+ items: Optional[Iterable[Any]] = None,
736
+ *,
737
+ style: Literal[
738
+ "wrapped", "columns", "numbered", "bullet", "table", "compact"
739
+ ] = "wrapped",
740
+ max_width: int = 80,
741
+ sep: str = ", ",
742
+ line_prefix: str = "",
743
+ items_per_line=None,
744
+ show_count=True,
745
+ title=None,
746
+ print_func=print,
747
+ ):
748
+ """
749
+ Print a list in a nice, readable format with multiple style options.
750
+
751
+ Args:
752
+ items: The list or iterable to print. If None, returns a partial function.
753
+ style: One of "wrapped", "columns", "numbered", "bullet", "table", "compact"
754
+ max_width: Maximum width for wrapped style
755
+ sep: Separator for items
756
+ line_prefix: Prefix for each line
757
+ items_per_line: For columns style, how many items per line
758
+ show_count: Whether to show the count of items
759
+ title: Optional title to display before the list
760
+ print_func: Function to use for printing. Defaults to print.
761
+ If None, returns the string instead of printing.
762
+
763
+ Examples:
764
+ >>> items = ["apple", "banana", "cherry", "date", "elderberry", "fig"]
765
+
766
+ # Wrapped style (default)
767
+ >>> print_list(items, max_width=30)
768
+ List (6 items):
769
+ apple, banana, cherry, date,
770
+ elderberry, fig
771
+
772
+ # Columns style
773
+ >>> print_list(items, style="columns", items_per_line=3)
774
+ List (6 items):
775
+ apple banana cherry
776
+ date elderberry fig
777
+
778
+ # Numbered style
779
+ >>> print_list(items, style="numbered")
780
+ List (6 items):
781
+ 1. apple
782
+ 2. banana
783
+ 3. cherry
784
+ 4. date
785
+ 5. elderberry
786
+ 6. fig
787
+
788
+ # Bullet style
789
+ >>> print_list(items, style="bullet")
790
+ List (6 items):
791
+ • apple
792
+ • banana
793
+ • cherry
794
+ • date
795
+ • elderberry
796
+ • fig
797
+
798
+ # Return string instead of printing
799
+ >>> result = print_list(items, style="numbered", print_func=None)
800
+ >>> print(result)
801
+ List (6 items):
802
+ 1. apple
803
+ 2. banana
804
+ 3. cherry
805
+ 4. date
806
+ 5. elderberry
807
+ 6. fig
808
+
809
+ Partial function functionality: If you don't specify the items (or items=None),
810
+ the function returns a partial function that can be called with the items later.
811
+ That is, the print_list acts as a factory function for different
812
+ printing styles.
813
+
814
+ >>> numbered_printer = print_list(style="numbered", show_count=False)
815
+ >>> numbered_printer(items)
816
+ 1. apple
817
+ 2. banana
818
+ 3. cherry
819
+ 4. date
820
+ 5. elderberry
821
+ 6. fig
822
+
823
+ >>> compact_printer = print_list(style="compact", max_width=60, show_count=False)
824
+ >>> compact_printer(items)
825
+ apple, banana, cherry, date, elderberry, fig
826
+
827
+ >>> bullet_printer = print_list(style="bullet", print_func=None, show_count=False)
828
+ >>> result = bullet_printer(items)
829
+ >>> print(result)
830
+ • apple
831
+ • banana
832
+ • cherry
833
+ • date
834
+ • elderberry
835
+ • fig
836
+ """
837
+ if items is None:
838
+ return partial(
839
+ print_list,
840
+ style=style,
841
+ max_width=max_width,
842
+ sep=sep,
843
+ line_prefix=line_prefix,
844
+ items_per_line=items_per_line,
845
+ show_count=show_count,
846
+ title=title,
847
+ print_func=print_func,
848
+ )
849
+ items = list(items) # Convert to list if it's an iterable
850
+
851
+ # Handle print_func=None by using StringAppender
852
+ if print_func is None:
853
+ string_appender = StringAppender()
854
+ print_func = string_appender
855
+ return_string = True
856
+ else:
857
+ return_string = False
858
+
859
+ # Show title and count
860
+ if title:
861
+ print_func(title)
862
+ elif show_count:
863
+ print_func(f"List ({len(items)} items):")
864
+
865
+ if not items:
866
+ print_func(f"{line_prefix}(empty list)")
867
+ return str(string_appender) if return_string else None
868
+
869
+ if style == "wrapped":
870
+ # Use the existing wrapped_print function with a safe fallback for doctest context
871
+ try:
872
+ from .loggers import wrapped_print # type: ignore
873
+ except Exception: # pragma: no cover - fallback when relative import fails
874
+ import textwrap
875
+
876
+ def wrapped_print(
877
+ items, sep=", ", max_width=80, line_prefix="", print_func=print
878
+ ):
879
+ text = sep.join(map(str, items))
880
+ return print_func(
881
+ line_prefix
882
+ + textwrap.fill(
883
+ text, width=max_width, subsequent_indent=line_prefix
884
+ )
885
+ )
886
+
887
+ wrapped_print(
888
+ items,
889
+ sep=sep,
890
+ max_width=max_width,
891
+ line_prefix=line_prefix,
892
+ print_func=print_func,
893
+ )
894
+
895
+ elif style == "columns":
896
+ if items_per_line is None:
897
+ # Auto-calculate based on max_width and average item length
898
+ avg_length = sum(len(str(item)) for item in items) / len(items)
899
+ items_per_line = max(1, int(max_width / (avg_length + len(sep))))
900
+
901
+ for i in range(0, len(items), items_per_line):
902
+ line_items = items[i : i + items_per_line]
903
+ # Calculate column widths across all rows for each column position
904
+ col_widths = []
905
+ for col in range(items_per_line):
906
+ col_items = items[col::items_per_line]
907
+ if col_items:
908
+ col_widths.append(max(len(str(item)) for item in col_items))
909
+ else:
910
+ col_widths.append(0)
911
+
912
+ # Print the line; pad all but the last column to avoid trailing spaces
913
+ parts = []
914
+ for j, item in enumerate(line_items):
915
+ text = str(item)
916
+ if j < len(line_items) - 1:
917
+ parts.append(text.ljust(col_widths[j]))
918
+ else:
919
+ parts.append(text)
920
+ print_func(f"{line_prefix}{' '.join(parts)}")
921
+
922
+ elif style == "numbered":
923
+ max_num_width = len(str(len(items)))
924
+ for i, item in enumerate(items, 1):
925
+ print_func(f"{line_prefix}{i:>{max_num_width}}. {item}")
926
+
927
+ elif style == "bullet":
928
+ for item in items:
929
+ print_func(f"{line_prefix}• {item}")
930
+
931
+ elif style == "table":
932
+ # Simple table format
933
+ if items and hasattr(items[0], "__iter__") and not isinstance(items[0], str):
934
+ # List of lists/tuples - treat as table data
935
+ if not items:
936
+ return str(string_appender) if return_string else None
937
+
938
+ # Find column widths
939
+ num_cols = len(items[0])
940
+ col_widths = [0] * num_cols
941
+ for row in items:
942
+ for j, cell in enumerate(row):
943
+ col_widths[j] = max(col_widths[j], len(str(cell)))
944
+
945
+ # Print table
946
+ for row in items:
947
+ formatted_row = []
948
+ for j, cell in enumerate(row):
949
+ formatted_row.append(str(cell).ljust(col_widths[j]))
950
+ print_func(f"{line_prefix}{' | '.join(formatted_row)}")
951
+ else:
952
+ # Single column table
953
+ max_width = max(len(str(item)) for item in items)
954
+ for item in items:
955
+ print_func(f"{line_prefix}{str(item).ljust(max_width)}")
956
+
957
+ elif style == "compact":
958
+ # Most compact form - all on one line if possible
959
+ items_str = sep.join(str(item) for item in items)
960
+ if len(items_str) <= max_width:
961
+ print_func(f"{line_prefix}{items_str}")
962
+ else:
963
+ # Fall back to wrapped style
964
+ try:
965
+ from .loggers import wrapped_print # type: ignore
966
+ except Exception: # pragma: no cover - fallback when relative import fails
967
+ import textwrap
968
+
969
+ def wrapped_print(
970
+ items, sep=", ", max_width=80, line_prefix="", print_func=print
971
+ ):
972
+ text = sep.join(map(str, items))
973
+ return print_func(
974
+ line_prefix
975
+ + textwrap.fill(
976
+ text, width=max_width, subsequent_indent=line_prefix
977
+ )
978
+ )
979
+
980
+ wrapped_print(
981
+ items,
982
+ sep=sep,
983
+ max_width=max_width,
984
+ line_prefix=line_prefix,
985
+ print_func=print_func,
986
+ )
987
+
988
+ else:
989
+ raise ValueError(
990
+ f"Unknown style: {style}. Use one of: wrapped, columns, numbered, bullet, table, compact"
991
+ )
992
+
993
+ return str(string_appender) if return_string else None
994
+
995
+
996
+ def print_list_as_table(
997
+ items, headers=None, *, max_width=80, align="left", print_func=print
998
+ ):
999
+ """
1000
+ Print a list as a nicely formatted table.
1001
+
1002
+ Args:
1003
+ items: List of items (strings, numbers, or objects with __str__)
1004
+ headers: Optional list of column headers
1005
+ max_width: Maximum width of the table
1006
+ align: Alignment for columns ("left", "right", "center")
1007
+ print_func: Function to use for printing. Defaults to print.
1008
+ If None, returns the string instead of printing.
1009
+
1010
+ Examples:
1011
+ >>> data = [["Name", "Age", "City"], ["Alice", 25, "NYC"], ["Bob", 30, "LA"]]
1012
+ >>> print_list_as_table(data)
1013
+ Name | Age | City
1014
+ -----|---|----
1015
+ Alice | 25 | NYC
1016
+ Bob | 30 | LA
1017
+
1018
+ # Return string instead of printing
1019
+ >>> result = print_list_as_table(data, print_func=None)
1020
+ >>> print(result)
1021
+ Name | Age | City
1022
+ -----|---|----
1023
+ Alice | 25 | NYC
1024
+ Bob | 30 | LA
1025
+ """
1026
+ # Handle print_func=None by using StringAppender
1027
+ if print_func is None:
1028
+ string_appender = StringAppender()
1029
+ print_func = string_appender
1030
+ return_string = True
1031
+ else:
1032
+ return_string = False
1033
+
1034
+ if not items:
1035
+ print_func("(empty table)")
1036
+ return str(string_appender) if return_string else None
1037
+
1038
+ # Convert items to list of lists if needed
1039
+ if not hasattr(items[0], "__iter__") or isinstance(items[0], str):
1040
+ # Single column
1041
+ table_data = [[str(item)] for item in items]
1042
+ if headers:
1043
+ headers = [headers] if isinstance(headers, str) else headers
1044
+ else:
1045
+ headers = ["Value"]
1046
+ else:
1047
+ # Multi-column
1048
+ table_data = [[str(cell) for cell in row] for row in items]
1049
+
1050
+ if headers:
1051
+ table_data.insert(0, headers)
1052
+
1053
+ # Calculate column widths
1054
+ num_cols = len(table_data[0])
1055
+ col_widths = [0] * num_cols
1056
+
1057
+ for row in table_data:
1058
+ for j, cell in enumerate(row):
1059
+ col_widths[j] = max(col_widths[j], len(cell))
1060
+
1061
+ # Adjust column widths to fit max_width
1062
+ total_width = sum(col_widths) + (num_cols - 1) * 3 # 3 for " | "
1063
+ if total_width > max_width:
1064
+ # Reduce column widths proportionally
1065
+ excess = total_width - max_width
1066
+ for j in range(num_cols):
1067
+ reduction = min(excess // num_cols, col_widths[j] // 2)
1068
+ col_widths[j] -= reduction
1069
+ excess -= reduction
1070
+
1071
+ # Determine if the first row should be treated as header
1072
+ header_present = bool(headers) or all(isinstance(c, str) for c in table_data[0])
1073
+
1074
+ # Helper to format a row without padding the last column
1075
+ def format_row(row):
1076
+ formatted = []
1077
+ for j, cell in enumerate(row):
1078
+ if j < num_cols - 1:
1079
+ formatted.append(cell.ljust(col_widths[j]))
1080
+ else:
1081
+ formatted.append(cell)
1082
+ return " | ".join(formatted)
1083
+
1084
+ for i, row in enumerate(table_data):
1085
+ print_func(format_row(row))
1086
+ if header_present and i == 0:
1087
+ # Print separator line without spaces around the pipes
1088
+ print_func("|".join("-" * w for w in col_widths))
1089
+
1090
+ return str(string_appender) if return_string else None
1091
+
1092
+
1093
+ def print_list_summary(
1094
+ items, *, max_items=10, show_total=True, title=None, print_func=print
1095
+ ):
1096
+ """
1097
+ Print a summary of a list, showing first few and last few items if the list is long.
1098
+
1099
+ Args:
1100
+ items: The list to summarize
1101
+ max_items: Maximum number of items to show (first + last)
1102
+ show_total: Whether to show the total count
1103
+ title: Optional title
1104
+ print_func: Function to use for printing. Defaults to print.
1105
+ If None, returns the string instead of printing.
1106
+
1107
+ Examples:
1108
+ >>> long_list = list(range(100))
1109
+ >>> print_list_summary(long_list, max_items=6)
1110
+ List (100 items):
1111
+ [0, 1, 2, ..., 97, 98, 99]
1112
+
1113
+ >>> print_list_summary(long_list, max_items=10)
1114
+ List (100 items):
1115
+ [0, 1, 2, 3, 4, ..., 95, 96, 97, 98, 99]
1116
+
1117
+ # Return string instead of printing
1118
+ >>> result = print_list_summary(long_list, max_items=6, print_func=None)
1119
+ >>> print(result)
1120
+ List (100 items):
1121
+ [0, 1, 2, ..., 97, 98, 99]
1122
+ """
1123
+ items = list(items)
1124
+
1125
+ # Handle print_func=None by using StringAppender
1126
+ if print_func is None:
1127
+ string_appender = StringAppender()
1128
+ print_func = string_appender
1129
+ return_string = True
1130
+ else:
1131
+ return_string = False
1132
+
1133
+ if title:
1134
+ print_func(title)
1135
+ elif show_total:
1136
+ print_func(f"List ({len(items)} items):")
1137
+
1138
+ if not items:
1139
+ print_func("(empty list)")
1140
+ return str(string_appender) if return_string else None
1141
+
1142
+ if len(items) <= max_items:
1143
+ print_func(items)
1144
+ else:
1145
+ # Show first and last items with ellipsis
1146
+ first_count = max_items // 2
1147
+ last_count = max_items - first_count
1148
+
1149
+ first_items = items[:first_count]
1150
+ last_items = items[-last_count:]
1151
+
1152
+ print_func(
1153
+ f"[{', '.join(map(str, first_items))}, ..., {', '.join(map(str, last_items))}]"
1154
+ )
1155
+
1156
+ return str(string_appender) if return_string else None
1157
+
1158
+
1159
+ # Convenience functions are now available as attributes of print_list
1160
+ # using the partial functionality:
1161
+ # - print_list.compact
1162
+ # - print_list.wrapped
1163
+ # - print_list.columns
1164
+ # - print_list.numbered
1165
+ # - print_list.bullets
1166
+
1167
+
1168
+ print_list.as_table = print_list_as_table
1169
+ print_list.summary = print_list_summary
1170
+ print_list.compact = print_list(style="compact", show_count=False)
1171
+ print_list.wrapped = print_list(style="wrapped", show_count=False)
1172
+ print_list.columns = print_list(style="columns", show_count=False)
1173
+ print_list.numbered = print_list(style="numbered", show_count=False)
1174
+ print_list.bullets = print_list(style="bullet", show_count=False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lkj
3
- Version: 0.1.43
3
+ Version: 0.1.44
4
4
  Summary: A dump of homeless useful utils
5
5
  Home-page: https://github.com/thorwhalen/lkj
6
6
  Author: Thor Whalen
@@ -1,4 +1,4 @@
1
- lkj/__init__.py,sha256=cGy0S6ZYacRlKA981i_lkqEJzDYSDuNx6LiQfBXvd5k,7104
1
+ lkj/__init__.py,sha256=IbBzGqZp9oXRt5oFyZwRFh9MHcFy8HW30S7Ox15RxEs,7191
2
2
  lkj/chunking.py,sha256=RpNdx5jEuO4mFg2qoTkD47iL35neBneuZ5xgQ_cBkiM,3755
3
3
  lkj/dicts.py,sha256=z2o7njvLNJkh1ZgSH-SLtz13SdW_YfUsTA1yTY-kVLE,10382
4
4
  lkj/filesys.py,sha256=NbWDuc848h8O42gwX7d9yNJkrWBgzSFnkoEdSRgvBAg,8883
@@ -7,9 +7,9 @@ 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=3YjlxOWUfzWqwu51uq_pv9XZReLqRFvziGtRsdzEtw8,24662
11
- lkj-0.1.43.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
- lkj-0.1.43.dist-info/METADATA,sha256=3aKQoeulKxTjgrPcjUzrY6qMbxjHeAAaFlqL__29zgA,4684
13
- lkj-0.1.43.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
14
- lkj-0.1.43.dist-info/top_level.txt,sha256=3DZOUwYmyurJFBXQCvCmEIVm8z2b42O5Sx3RDQyePfg,4
15
- lkj-0.1.43.dist-info/RECORD,,
10
+ lkj/strings.py,sha256=aib3KLFBuHl2WuDI2m7xC2JXM9E2-fk9CkXU7Pld6VU,41459
11
+ lkj-0.1.44.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
+ lkj-0.1.44.dist-info/METADATA,sha256=UIL9HF85Q1yjtn_UCjmh7LYpRv_XDEJHvVZQUlWGm84,4684
13
+ lkj-0.1.44.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
14
+ lkj-0.1.44.dist-info/top_level.txt,sha256=3DZOUwYmyurJFBXQCvCmEIVm8z2b42O5Sx3RDQyePfg,4
15
+ lkj-0.1.44.dist-info/RECORD,,
File without changes
File without changes