lkj 0.1.47__py3-none-any.whl → 0.1.49__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 +6 -1
- lkj/chunking.py +5 -8
- lkj/dicts.py +34 -13
- lkj/filesys.py +155 -2
- lkj/iterables.py +2 -1
- lkj/loggers.py +14 -11
- lkj/misc.py +4 -3
- lkj/strings.py +8 -6
- {lkj-0.1.47.dist-info → lkj-0.1.49.dist-info}/METADATA +4 -2
- lkj-0.1.49.dist-info/RECORD +17 -0
- {lkj-0.1.47.dist-info → lkj-0.1.49.dist-info}/WHEEL +1 -1
- lkj-0.1.47.dist-info/RECORD +0 -17
- {lkj-0.1.47.dist-info → lkj-0.1.49.dist-info/licenses}/LICENSE +0 -0
- {lkj-0.1.47.dist-info → lkj-0.1.49.dist-info}/top_level.txt +0 -0
lkj/__init__.py
CHANGED
|
@@ -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
|
|
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
|
lkj/chunking.py
CHANGED
|
@@ -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:
|
|
21
|
+
iterable: Iterable[T] | Mapping[KT, VT],
|
|
25
22
|
chk_size: int,
|
|
26
23
|
*,
|
|
27
|
-
chunk_type:
|
|
28
|
-
) -> Iterator[
|
|
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[
|
|
78
|
+
) -> Iterator[tuple[T, ...]]:
|
|
82
79
|
"""
|
|
83
80
|
Chunks an iterable into non-overlapping chunks of size `chk_size`.
|
|
84
81
|
|
lkj/dicts.py
CHANGED
|
@@ -45,20 +45,24 @@ def exclusive_subdict(d, exclude):
|
|
|
45
45
|
def truncate_dict_values(
|
|
46
46
|
d: dict,
|
|
47
47
|
*,
|
|
48
|
-
max_list_size:
|
|
49
|
-
max_string_size:
|
|
48
|
+
max_list_size: int | None = 2,
|
|
49
|
+
max_string_size: int | None = 66,
|
|
50
50
|
middle_marker: str = "...",
|
|
51
|
+
d_ingress: callable = lambda d: d,
|
|
51
52
|
) -> dict:
|
|
52
53
|
"""
|
|
53
54
|
Returns a new dictionary with the same nested keys structure, where:
|
|
54
55
|
- List values are reduced to a maximum size of max_list_size.
|
|
55
56
|
- String values longer than max_string_size are truncated in the middle.
|
|
57
|
+
- Values are preprocessed with d_ingress before type checking.
|
|
56
58
|
|
|
57
59
|
Parameters:
|
|
58
60
|
d (dict): The input dictionary.
|
|
59
61
|
max_list_size (int, optional): Maximum size for lists. Defaults to 2.
|
|
60
62
|
max_string_size (int, optional): Maximum length for strings. Defaults to None (no truncation).
|
|
61
63
|
middle_marker (str, optional): String to insert in the middle of truncated strings. Defaults to '...'.
|
|
64
|
+
d_ingress (callable, optional): Function applied to each value before type checking.
|
|
65
|
+
Defaults to identity function. Useful for handling special types like pandas Series.
|
|
62
66
|
|
|
63
67
|
Returns:
|
|
64
68
|
dict: A new dictionary with truncated lists and strings.
|
|
@@ -79,6 +83,14 @@ def truncate_dict_values(
|
|
|
79
83
|
... == large_dict
|
|
80
84
|
... )
|
|
81
85
|
|
|
86
|
+
For handling special types like pandas Series:
|
|
87
|
+
|
|
88
|
+
>>> def handle_pandas(obj):
|
|
89
|
+
... if hasattr(obj, 'head'): # pandas Series/DataFrame
|
|
90
|
+
... return f"<{type(obj).__name__}: {len(obj)} items>"
|
|
91
|
+
... return obj
|
|
92
|
+
>>> # truncate_dict_values(data_with_pandas, d_ingress=handle_pandas)
|
|
93
|
+
|
|
82
94
|
"""
|
|
83
95
|
|
|
84
96
|
def truncate_string(value, max_len, marker):
|
|
@@ -87,26 +99,34 @@ def truncate_dict_values(
|
|
|
87
99
|
half_len = (max_len - len(marker)) // 2
|
|
88
100
|
return value[:half_len] + marker + value[-half_len:]
|
|
89
101
|
|
|
102
|
+
# Apply d_ingress to preprocess the value
|
|
103
|
+
d = d_ingress(d)
|
|
104
|
+
|
|
90
105
|
kwargs = dict(
|
|
91
106
|
max_list_size=max_list_size,
|
|
92
107
|
max_string_size=max_string_size,
|
|
93
108
|
middle_marker=middle_marker,
|
|
109
|
+
d_ingress=d_ingress,
|
|
94
110
|
)
|
|
95
111
|
if isinstance(d, dict):
|
|
96
112
|
return {k: truncate_dict_values(v, **kwargs) for k, v in d.items()}
|
|
97
|
-
elif isinstance(d, list):
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
elif isinstance(d, (list, tuple)):
|
|
114
|
+
container_type = type(d)
|
|
115
|
+
if max_list_size is not None:
|
|
116
|
+
truncated_items = [
|
|
117
|
+
truncate_dict_values(v, **kwargs) for v in d[:max_list_size]
|
|
118
|
+
]
|
|
119
|
+
else:
|
|
120
|
+
truncated_items = [truncate_dict_values(v, **kwargs) for v in d]
|
|
121
|
+
return container_type(truncated_items)
|
|
103
122
|
elif isinstance(d, str):
|
|
104
123
|
return truncate_string(d, max_string_size, middle_marker)
|
|
105
124
|
else:
|
|
106
125
|
return d
|
|
107
126
|
|
|
108
127
|
|
|
109
|
-
from typing import
|
|
128
|
+
from typing import TypeVar, Tuple
|
|
129
|
+
from collections.abc import Mapping, Callable, Iterable
|
|
110
130
|
|
|
111
131
|
KT = TypeVar("KT") # Key type
|
|
112
132
|
VT = TypeVar("VT") # Value type
|
|
@@ -119,7 +139,7 @@ def merge_dicts(
|
|
|
119
139
|
*mappings: Mapping[KT, VT],
|
|
120
140
|
recursive_condition: Callable[[VT], bool] = lambda v: isinstance(v, Mapping),
|
|
121
141
|
conflict_resolver: Callable[[VT, VT], VT] = lambda x, y: y,
|
|
122
|
-
mapping_constructor: Callable[[Iterable[
|
|
142
|
+
mapping_constructor: Callable[[Iterable[tuple[KT, VT]]], Mapping[KT, VT]] = dict,
|
|
123
143
|
) -> Mapping[KT, VT]:
|
|
124
144
|
"""
|
|
125
145
|
Merge multiple mappings into a single mapping, recursively if needed,
|
|
@@ -218,7 +238,8 @@ def merge_dicts(
|
|
|
218
238
|
|
|
219
239
|
|
|
220
240
|
import operator
|
|
221
|
-
from typing import
|
|
241
|
+
from typing import Dict, Any
|
|
242
|
+
from collections.abc import Callable
|
|
222
243
|
|
|
223
244
|
Comparison = Any
|
|
224
245
|
Comparator = Callable[[dict, dict], Comparison]
|
|
@@ -232,10 +253,10 @@ def compare_field_values(
|
|
|
232
253
|
dict1,
|
|
233
254
|
dict2,
|
|
234
255
|
*,
|
|
235
|
-
field_comparators:
|
|
256
|
+
field_comparators: dict[KT, Comparator] = {},
|
|
236
257
|
default_comparator: Comparator = operator.eq,
|
|
237
258
|
aggregator: Callable[
|
|
238
|
-
[
|
|
259
|
+
[dict[KT, Comparison]], Any
|
|
239
260
|
] = lambda d: d, # lambda d: np.mean(list(d.values())),
|
|
240
261
|
get_comparison_fields: Callable[[dict], Iterable[KT]] = _common_keys_list,
|
|
241
262
|
):
|
lkj/filesys.py
CHANGED
|
@@ -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
|
|
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
|
|
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:]
|
lkj/iterables.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tools with iterables (dicts, lists, tuples, sets, etc.)."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
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):
|
lkj/loggers.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Utils for logging."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
187
|
-
egress_msg: Callable[[str,
|
|
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,
|
|
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
|
|
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
|
|
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:
|
|
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.
|
lkj/misc.py
CHANGED
|
@@ -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
|
|
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:
|
|
18
|
-
max_val:
|
|
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
|
):
|
lkj/strings.py
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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[[
|
|
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:
|
|
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:
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: lkj
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.49
|
|
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
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
lkj/__init__.py,sha256=z8IZi5ql-bCM0xRgcEERg48oi2SRkgX9qSZdyaKb62Y,7300
|
|
2
|
+
lkj/chunking.py,sha256=K7yakDuTgyn6t2rvFeDIWS5pO636BKGOe1spKk7v4Mc,3746
|
|
3
|
+
lkj/dicts.py,sha256=67E9qwxPLLsBeRj67cSDgkAGvB6xfmNsi8igXdFGwjA,11307
|
|
4
|
+
lkj/filesys.py,sha256=hYWztDoJUvVcPMD5AxRyYKkTB-sy5hx6_RDw4OyRcos,14628
|
|
5
|
+
lkj/funcs.py,sha256=LXJlj3gMMsbD0t2gn2NZZ6mOqmW5bxM-94uGoYgrhzI,8930
|
|
6
|
+
lkj/importing.py,sha256=TcW3qUDmw7jqswpxXnksjlHkkbOJq70NbUk1ZyaafT0,6658
|
|
7
|
+
lkj/iterables.py,sha256=JSuTm7XpaTjIlv_9XZTrApLBohu5hRu7ZHi0wDT_ky0,2828
|
|
8
|
+
lkj/loggers.py,sha256=eCudnqFaQhWz95qZtB57DDhvhXYvwl1JVCVernMhE5c,13781
|
|
9
|
+
lkj/misc.py,sha256=RqJg4HgcT1ZZRBtxOppwYibvvLIyn6pNqI-WMg0uKgI,1457
|
|
10
|
+
lkj/strings.py,sha256=JukQ4pvLlwGajBEK0PXyqsumEFKm8i1rx6SgASz7tqo,43798
|
|
11
|
+
lkj/tests/__init__.py,sha256=kReYfWiyz1T79AuZAy5m4PIBj3Oj_Dlc0E-T8HVQ504,20
|
|
12
|
+
lkj/tests/test_strings.py,sha256=Ix54io8WqWmYTY4guBgZShtKabtjd5uVpdxzxwJkgNA,1707
|
|
13
|
+
lkj-0.1.49.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
14
|
+
lkj-0.1.49.dist-info/METADATA,sha256=pURwBZHm355AXQBzLViKArzBWbJhCFLxiNiHRaNRih8,8267
|
|
15
|
+
lkj-0.1.49.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
16
|
+
lkj-0.1.49.dist-info/top_level.txt,sha256=3DZOUwYmyurJFBXQCvCmEIVm8z2b42O5Sx3RDQyePfg,4
|
|
17
|
+
lkj-0.1.49.dist-info/RECORD,,
|
lkj-0.1.47.dist-info/RECORD
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
lkj/__init__.py,sha256=IbBzGqZp9oXRt5oFyZwRFh9MHcFy8HW30S7Ox15RxEs,7191
|
|
2
|
-
lkj/chunking.py,sha256=RpNdx5jEuO4mFg2qoTkD47iL35neBneuZ5xgQ_cBkiM,3755
|
|
3
|
-
lkj/dicts.py,sha256=z2o7njvLNJkh1ZgSH-SLtz13SdW_YfUsTA1yTY-kVLE,10382
|
|
4
|
-
lkj/filesys.py,sha256=NbWDuc848h8O42gwX7d9yNJkrWBgzSFnkoEdSRgvBAg,8883
|
|
5
|
-
lkj/funcs.py,sha256=LXJlj3gMMsbD0t2gn2NZZ6mOqmW5bxM-94uGoYgrhzI,8930
|
|
6
|
-
lkj/importing.py,sha256=TcW3qUDmw7jqswpxXnksjlHkkbOJq70NbUk1ZyaafT0,6658
|
|
7
|
-
lkj/iterables.py,sha256=9jeO36w-IGiZryge7JKgXZOGZAgehUvhwKV3nHPcZWk,2801
|
|
8
|
-
lkj/loggers.py,sha256=ImmBdacz89Lvb3dg_xI5jOct_44rSRv0hNI_CVehy60,13706
|
|
9
|
-
lkj/misc.py,sha256=IZf05tkl0cgiMgBwCMH5cLSC59fRXwnemPRo8G0OxQg,1436
|
|
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
|