lkj 0.1.47__tar.gz → 0.1.48__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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: lkj
3
- Version: 0.1.47
3
+ Version: 0.1.48
4
4
  Summary: A dump of homeless useful utils
5
5
  Home-page: https://github.com/thorwhalen/lkj
6
6
  Author: Thor Whalen
@@ -8,6 +8,8 @@ License: apache-2.0
8
8
  Platform: any
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
+ Provides-Extra: testing
12
+ Dynamic: license-file
11
13
 
12
14
  # lkj
13
15
 
@@ -15,7 +15,12 @@ from lkj.dicts import (
15
15
  merge_dicts, # Merge multiple dictionaries recursively
16
16
  compare_field_values, # Compare two dictionaries' values
17
17
  )
18
- from lkj.filesys import get_app_data_dir, get_watermarked_dir, enable_sourcing_from_file
18
+ from lkj.filesys import (
19
+ get_app_data_dir,
20
+ get_watermarked_dir,
21
+ enable_sourcing_from_file,
22
+ search_folder_fast, # Fast search for a term in files under a folder (uses rg command)
23
+ )
19
24
  from lkj.strings import (
20
25
  print_list, # Print a list in a nice format (or get a string to process yourself)
21
26
  FindReplaceTool, # Tool for finding and replacing substrings in a string
@@ -3,29 +3,26 @@
3
3
  from itertools import zip_longest, chain, islice
4
4
 
5
5
  from typing import (
6
- Iterable,
7
6
  Union,
8
7
  Dict,
9
8
  List,
10
9
  Tuple,
11
- Mapping,
12
10
  TypeVar,
13
- Iterator,
14
- Callable,
15
11
  Optional,
16
12
  T,
17
13
  )
14
+ from collections.abc import Iterable, Mapping, Iterator, Callable
18
15
 
19
16
  KT = TypeVar("KT") # there's a typing.KT, but pylance won't allow me to use it!
20
17
  VT = TypeVar("VT") # there's a typing.VT, but pylance won't allow me to use it!
21
18
 
22
19
 
23
20
  def chunk_iterable(
24
- iterable: Union[Iterable[T], Mapping[KT, VT]],
21
+ iterable: Iterable[T] | Mapping[KT, VT],
25
22
  chk_size: int,
26
23
  *,
27
- chunk_type: Optional[Callable[..., Union[Iterable[T], Mapping[KT, VT]]]] = None,
28
- ) -> Iterator[Union[List[T], Tuple[T, ...], Dict[KT, VT]]]:
24
+ chunk_type: Callable[..., Iterable[T] | Mapping[KT, VT]] | None = None,
25
+ ) -> Iterator[list[T] | tuple[T, ...] | dict[KT, VT]]:
29
26
  """
30
27
  Divide an iterable into chunks/batches of a specific size.
31
28
 
@@ -78,7 +75,7 @@ def chunk_iterable(
78
75
 
79
76
  def chunker(
80
77
  a: Iterable[T], chk_size: int, *, include_tail: bool = True
81
- ) -> Iterator[Tuple[T, ...]]:
78
+ ) -> Iterator[tuple[T, ...]]:
82
79
  """
83
80
  Chunks an iterable into non-overlapping chunks of size `chk_size`.
84
81
 
@@ -45,8 +45,8 @@ def exclusive_subdict(d, exclude):
45
45
  def truncate_dict_values(
46
46
  d: dict,
47
47
  *,
48
- max_list_size: Optional[int] = 2,
49
- max_string_size: Optional[int] = 66,
48
+ max_list_size: int | None = 2,
49
+ max_string_size: int | None = 66,
50
50
  middle_marker: str = "...",
51
51
  ) -> dict:
52
52
  """
@@ -106,7 +106,8 @@ def truncate_dict_values(
106
106
  return d
107
107
 
108
108
 
109
- from typing import Mapping, Callable, TypeVar, Iterable, Tuple
109
+ from typing import TypeVar, Tuple
110
+ from collections.abc import Mapping, Callable, Iterable
110
111
 
111
112
  KT = TypeVar("KT") # Key type
112
113
  VT = TypeVar("VT") # Value type
@@ -119,7 +120,7 @@ def merge_dicts(
119
120
  *mappings: Mapping[KT, VT],
120
121
  recursive_condition: Callable[[VT], bool] = lambda v: isinstance(v, Mapping),
121
122
  conflict_resolver: Callable[[VT, VT], VT] = lambda x, y: y,
122
- mapping_constructor: Callable[[Iterable[Tuple[KT, VT]]], Mapping[KT, VT]] = dict,
123
+ mapping_constructor: Callable[[Iterable[tuple[KT, VT]]], Mapping[KT, VT]] = dict,
123
124
  ) -> Mapping[KT, VT]:
124
125
  """
125
126
  Merge multiple mappings into a single mapping, recursively if needed,
@@ -218,7 +219,8 @@ def merge_dicts(
218
219
 
219
220
 
220
221
  import operator
221
- from typing import Callable, Dict, Any
222
+ from typing import Dict, Any
223
+ from collections.abc import Callable
222
224
 
223
225
  Comparison = Any
224
226
  Comparator = Callable[[dict, dict], Comparison]
@@ -232,10 +234,10 @@ def compare_field_values(
232
234
  dict1,
233
235
  dict2,
234
236
  *,
235
- field_comparators: Dict[KT, Comparator] = {},
237
+ field_comparators: dict[KT, Comparator] = {},
236
238
  default_comparator: Comparator = operator.eq,
237
239
  aggregator: Callable[
238
- [Dict[KT, Comparison]], Any
240
+ [dict[KT, Comparison]], Any
239
241
  ] = lambda d: d, # lambda d: np.mean(list(d.values())),
240
242
  get_comparison_fields: Callable[[dict], Iterable[KT]] = _common_keys_list,
241
243
  ):
@@ -1,7 +1,160 @@
1
1
  """File system utils"""
2
2
 
3
+ # -------------------------------------------------------------------------------------
4
+ # Search (on linux and macOS) using rg (ripgrep)
5
+ import subprocess
6
+ import json
7
+ import shutil
8
+ from typing import Callable, Any, Optional
9
+
10
+ # simple parser that returns list of dicts objects from a JSONL string
11
+ simple_jsonl_parser = lambda string: list(map(json.loads, string.splitlines()))
12
+
13
+ # --- Default Egress Function ---
14
+
15
+
16
+ def _ripgrep_json_parser(rg_output: str) -> list[dict]:
17
+ """
18
+ Parses the line-by-line JSON stream output from ripgrep (rg --json).
19
+
20
+ This output is NOT a single valid JSON object, but a stream of JSON lines.
21
+ We only care about 'match' entries for the results.
22
+ """
23
+ results = []
24
+
25
+ # ripgrep outputs one JSON object per line, so we iterate line by line
26
+ for line in rg_output.strip().split("\n"):
27
+ if not line:
28
+ continue
29
+
30
+ try:
31
+ data = json.loads(line)
32
+ except json.JSONDecodeError:
33
+ # Skip any lines that aren't valid JSON (shouldn't happen with --json)
34
+ continue
35
+
36
+ if data.get("type") == "match":
37
+ match_data = data.get("data", {})
38
+
39
+ # Extract key information from the match
40
+ result = {
41
+ "path": match_data.get("path", {}).get("text"),
42
+ "line_number": match_data.get("line_number"),
43
+ # The text of the matched line, decoded
44
+ "line_text": match_data.get("lines", {}).get("text"),
45
+ "submatches": [
46
+ {
47
+ "match_text": sub.get("match", {}).get("text"),
48
+ "start": sub.get("start"),
49
+ "end": sub.get("end"),
50
+ }
51
+ for sub in match_data.get("submatches", [])
52
+ ],
53
+ }
54
+ results.append(result)
55
+
56
+ return results
57
+
58
+
59
+ def search_folder_fast(
60
+ search_term: str,
61
+ path_to_search: str = ".",
62
+ *, # Enforce 'egress' as a keyword-only argument
63
+ egress: Optional[Callable[[str], Any]] = _ripgrep_json_parser,
64
+ ) -> Any:
65
+ """
66
+ Executes a fast, recursive search using ripgrep and processes the results.
67
+
68
+ :param search_term: The regex pattern or text string to search for.
69
+ :param path_to_search: The folder path to start searching from. Defaults to current directory.
70
+ :param egress: A callable function to process the raw ripgrep output string.
71
+ Defaults to a parser that returns a list of dictionaries for matches.
72
+ If set to None, it defaults to a lambda returning the raw output (string).
73
+ You can also give it simple_jsonl_parser to get a list of all JSON objects in the output.
74
+ :return: The output of the 'egress' function.
75
+
76
+ Example usage:
77
+ -------------
78
+ >>> results = search_folder_fast("my_function_name", path_to_search='/path/to/project') # doctest: +SKIP
79
+ >>> for match in results: # doctest: +SKIP
80
+ ... print(f"Found in {match['path']} at line {match['line_number']}: {match['line_text']}") # doctest: +SKIP
81
+
82
+
83
+ """
84
+
85
+ if shutil.which("rg") is None:
86
+ print("=" * 60)
87
+ print(
88
+ "🚨 Error: The 'rg' (ripgrep) command was not found in your system's PATH."
89
+ )
90
+ print("\nTo install ripgrep:")
91
+ print(" - Linux (Debian/Ubuntu): sudo apt install ripgrep")
92
+ print(" - macOS (Homebrew): brew install ripgrep")
93
+ print(" - Windows (Chocolatey): choco install ripgrep")
94
+ print(f"\nMore details: https://github.com/BurntSushi/ripgrep#installation")
95
+ print("=" * 60)
96
+ return None
97
+
98
+ # Standard ripgrep command with JSON output
99
+ # -i: case-insensitive, -r: recursive, -n: show line numbers
100
+ # --json: outputs machine-readable JSON format
101
+ # --color never: ensures no ANSI color codes in the output stream
102
+ command = [
103
+ "rg",
104
+ "-i",
105
+ "-r",
106
+ "-n",
107
+ "--json",
108
+ "--color",
109
+ "never",
110
+ search_term,
111
+ path_to_search,
112
+ ]
113
+
114
+ # Handle the default None case for egress (return raw string)
115
+ if egress is None:
116
+ egress = lambda x: x
117
+
118
+ try:
119
+ # Use text=True and capture_output=True for string output and capturing stdout/stderr
120
+ result = subprocess.run(command, capture_output=True, text=True, check=True)
121
+
122
+ # Call the egress function on the raw standard output
123
+ return egress(result.stdout)
124
+
125
+ except subprocess.CalledProcessError as e:
126
+ # ripgrep returns a non-zero code if no matches are found.
127
+ # This is expected behavior and should not be treated as an error
128
+ # unless stderr indicates a true issue.
129
+ # Check stderr for real errors
130
+ if e.stderr:
131
+ # Handle genuine errors like permissions or file issues
132
+ print(f"A genuine ripgrep error occurred: {e.stderr.strip()}")
133
+ return None
134
+
135
+ # If check=True and return code is 1, it usually means "no matches found."
136
+ # If there's no stderr, return the egress of an empty string (or handle as no results)
137
+ return egress("")
138
+
139
+ except FileNotFoundError:
140
+ print(
141
+ "Error: ripgrep ('rg') command not found. Please ensure it is installed and in your PATH."
142
+ )
143
+ return None
144
+
145
+
146
+ # Example usage will require 'rg' installed and a file system to search
147
+ # For demonstration purposes, assume 'rg' is installed and you are searching for 'wrap_kvs'
148
+ # search_results = search_folder_fast("wrap_kvs", path_to_search='/path/to/your/project')
149
+ #
150
+ # print(search_results)
151
+
152
+ # -------------------------------------------------------------------------------------
153
+ # General utils for file system operations
154
+
3
155
  import os
4
- from typing import Callable, Any
156
+ from typing import Any
157
+ from collections.abc import Callable
5
158
  from pathlib import Path
6
159
  from functools import wraps, partial
7
160
 
@@ -32,7 +185,7 @@ def enable_sourcing_from_file(func=None, *, write_output=False):
32
185
  if args and isinstance(args[0], str) and os.path.isfile(args[0]):
33
186
  file_path = args[0]
34
187
  # Read the file content
35
- with open(file_path, "r") as file:
188
+ with open(file_path) as file:
36
189
  file_content = file.read()
37
190
  # Call the function with the file content and other arguments
38
191
  new_args = (file_content,) + args[1:]
@@ -1,6 +1,7 @@
1
1
  """Tools with iterables (dicts, lists, tuples, sets, etc.)."""
2
2
 
3
- from typing import Sequence, Mapping, KT, VT, Iterable, Iterable, NamedTuple
3
+ from typing import KT, VT, NamedTuple
4
+ from collections.abc import Sequence, Mapping, Iterable, Iterable
4
5
 
5
6
 
6
7
  class SetsComparisonResult(NamedTuple):
@@ -1,6 +1,7 @@
1
1
  """Utils for logging."""
2
2
 
3
- from typing import Callable, Tuple, Any, Optional, Union, Iterable
3
+ from typing import Tuple, Any, Optional, Union
4
+ from collections.abc import Callable, Iterable
4
5
  from functools import partial, wraps
5
6
  from operator import attrgetter
6
7
 
@@ -9,7 +10,7 @@ from operator import attrgetter
9
10
  # TODO: Merge with wrap_text_with_exact_spacing
10
11
  # TODO: Add doctests for string
11
12
  def wrapped_print(
12
- items: Union[str, Iterable],
13
+ items: str | Iterable,
13
14
  sep=", ",
14
15
  max_width=80,
15
16
  *,
@@ -166,15 +167,15 @@ def clog(condition, *args, log_func=print, **kwargs):
166
167
  return log_func(*args, **kwargs)
167
168
 
168
169
 
169
- def _calling_name(func_name: str, args: Tuple, kwargs: dict) -> str:
170
+ def _calling_name(func_name: str, args: tuple, kwargs: dict) -> str:
170
171
  return f"Calling {func_name}..."
171
172
 
172
173
 
173
- def _done_calling_name(func_name: str, args: Tuple, kwargs: dict, result: Any) -> str:
174
+ def _done_calling_name(func_name: str, args: tuple, kwargs: dict, result: Any) -> str:
174
175
  return f".... Done calling {func_name}"
175
176
 
176
177
 
177
- def _always_log(func: Callable, args: Tuple, kwargs: dict) -> bool:
178
+ def _always_log(func: Callable, args: tuple, kwargs: dict) -> bool:
178
179
  """Return True no matter what"""
179
180
  return True
180
181
 
@@ -183,10 +184,10 @@ def log_calls(
183
184
  func: Callable = None,
184
185
  *,
185
186
  logger: Callable[[str], None] = print,
186
- ingress_msg: Callable[[str, Tuple, dict], str] = _calling_name,
187
- egress_msg: Callable[[str, Tuple, dict, Any], str] = _done_calling_name,
187
+ ingress_msg: Callable[[str, tuple, dict], str] = _calling_name,
188
+ egress_msg: Callable[[str, tuple, dict, Any], str] = _done_calling_name,
188
189
  func_name: Callable[[Callable], str] = attrgetter("__name__"),
189
- log_condition: Callable[[Callable, Tuple, dict], bool] = _always_log,
190
+ log_condition: Callable[[Callable, tuple, dict], bool] = _always_log,
190
191
  ) -> Callable:
191
192
  """
192
193
  Decorator that adds logging before and after the function's call.
@@ -337,10 +338,12 @@ log_calls.instance_flag_is_set = instance_flag_is_set
337
338
  # Error handling
338
339
 
339
340
 
340
- from typing import Callable, Tuple, Any
341
+ from typing import Tuple, Any
342
+ from collections.abc import Callable
341
343
  from dataclasses import dataclass
342
344
  import traceback
343
- from typing import Callable, Any, Tuple
345
+ from typing import Any, Tuple
346
+ from collections.abc import Callable
344
347
  from functools import partial, wraps
345
348
  from operator import attrgetter
346
349
 
@@ -371,7 +374,7 @@ def dflt_error_info_processor(
371
374
  def return_error_info_on_error(
372
375
  func,
373
376
  *,
374
- caught_error_types: Tuple[Exception] = (Exception,),
377
+ caught_error_types: tuple[Exception] = (Exception,),
375
378
  error_info_processor: Callable[[ErrorInfo], Any] = dflt_error_info_processor,
376
379
  ):
377
380
  """Decorator that returns traceback and local variables on error.
@@ -2,7 +2,8 @@
2
2
 
3
3
  from operator import attrgetter, ge, gt, le, lt
4
4
  from functools import partial
5
- from typing import Callable, T, Optional, Any
5
+ from typing import T, Optional, Any
6
+ from collections.abc import Callable
6
7
 
7
8
 
8
9
  def identity(x):
@@ -14,8 +15,8 @@ def value_in_interval(
14
15
  /,
15
16
  *,
16
17
  get_val: Callable[[Any], T] = identity,
17
- min_val: Optional[T] = None,
18
- max_val: Optional[T] = None,
18
+ min_val: T | None = None,
19
+ max_val: T | None = None,
19
20
  is_minimum: Callable[[T, T], bool] = ge,
20
21
  is_maximum: Callable[[T, T], bool] = lt,
21
22
  ):
@@ -22,7 +22,8 @@ These utilities are designed to make it easier to display, format, and manipulat
22
22
  """
23
23
 
24
24
  import re
25
- from typing import Iterable, Sequence, Callable, Optional, Any, Literal
25
+ from typing import Optional, Any, Literal
26
+ from collections.abc import Iterable, Sequence, Callable
26
27
  from functools import partial
27
28
 
28
29
 
@@ -327,7 +328,7 @@ def regex_based_substitution(replacements: dict, regex=None, s: str = None):
327
328
  )
328
329
 
329
330
 
330
- from typing import Callable, Iterable, Sequence
331
+ from collections.abc import Callable, Iterable, Sequence
331
332
 
332
333
 
333
334
  class TrieNode:
@@ -453,12 +454,13 @@ def unique_affixes(
453
454
  return affixes
454
455
 
455
456
 
456
- from typing import Union, Callable, Dict, Any
457
+ from typing import Union, Dict, Any
458
+ from collections.abc import Callable
457
459
 
458
460
  # A match is represented as a dictionary (keys like "start", "end", etc.)
459
461
  # and the replacement is either a static string or a callable that takes that
460
462
  # dictionary and returns a string.
461
- Replacement = Union[str, Callable[[Dict[str, Any]], str]]
463
+ Replacement = Union[str, Callable[[dict[str, Any]], str]]
462
464
 
463
465
 
464
466
  class FindReplaceTool:
@@ -770,7 +772,7 @@ class FindReplaceTool:
770
772
 
771
773
 
772
774
  def print_list(
773
- items: Optional[Iterable[Any]] = None,
775
+ items: Iterable[Any] | None = None,
774
776
  *,
775
777
  style: Literal[
776
778
  "wrapped", "columns", "numbered", "bullet", "table", "compact"
@@ -779,7 +781,7 @@ def print_list(
779
781
  sep: str = ", ",
780
782
  line_prefix: str = "",
781
783
  items_per_line=None,
782
- show_count: Union[bool, Callable[[int], str]] = False,
784
+ show_count: bool | Callable[[int], str] = False,
783
785
  title=None,
784
786
  print_func=print,
785
787
  ):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: lkj
3
- Version: 0.1.47
3
+ Version: 0.1.48
4
4
  Summary: A dump of homeless useful utils
5
5
  Home-page: https://github.com/thorwhalen/lkj
6
6
  Author: Thor Whalen
@@ -8,6 +8,8 @@ License: apache-2.0
8
8
  Platform: any
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
+ Provides-Extra: testing
12
+ Dynamic: license-file
11
13
 
12
14
  # lkj
13
15
 
@@ -16,6 +16,7 @@ lkj.egg-info/PKG-INFO
16
16
  lkj.egg-info/SOURCES.txt
17
17
  lkj.egg-info/dependency_links.txt
18
18
  lkj.egg-info/not-zip-safe
19
+ lkj.egg-info/requires.txt
19
20
  lkj.egg-info/top_level.txt
20
21
  lkj/tests/__init__.py
21
22
  lkj/tests/test_strings.py
@@ -0,0 +1,2 @@
1
+
2
+ [testing]
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = lkj
3
- version = 0.1.47
3
+ version = 0.1.48
4
4
  url = https://github.com/thorwhalen/lkj
5
5
  platforms = any
6
6
  description_file = README.md
@@ -19,6 +19,9 @@ include_package_data = True
19
19
  zip_safe = False
20
20
  install_requires =
21
21
 
22
+ [options.extras_require]
23
+ testing =
24
+
22
25
  [egg_info]
23
26
  tag_build =
24
27
  tag_date = 0
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