lkj 0.1.34__tar.gz → 0.1.36__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.34 → lkj-0.1.36}/PKG-INFO +1 -1
- {lkj-0.1.34 → lkj-0.1.36}/lkj/__init__.py +11 -3
- lkj-0.1.36/lkj/dicts.py +223 -0
- lkj-0.1.36/lkj/strings.py +688 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj.egg-info/PKG-INFO +1 -1
- {lkj-0.1.34 → lkj-0.1.36}/setup.cfg +1 -1
- lkj-0.1.34/lkj/dicts.py +0 -106
- lkj-0.1.34/lkj/strings.py +0 -364
- {lkj-0.1.34 → lkj-0.1.36}/LICENSE +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/README.md +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj/chunking.py +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj/filesys.py +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj/funcs.py +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj/importing.py +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj/iterables.py +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj/loggers.py +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj/misc.py +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj.egg-info/SOURCES.txt +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj.egg-info/dependency_links.txt +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj.egg-info/not-zip-safe +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/lkj.egg-info/top_level.txt +0 -0
- {lkj-0.1.34 → lkj-0.1.36}/setup.py +0 -0
|
@@ -8,18 +8,26 @@ from lkj.iterables import (
|
|
|
8
8
|
get_by_value, # Get a dictionary from a list of dictionaries by a field value
|
|
9
9
|
)
|
|
10
10
|
from lkj.funcs import mk_factory
|
|
11
|
-
from lkj.dicts import
|
|
11
|
+
from lkj.dicts import (
|
|
12
|
+
truncate_dict_values, # Truncate list and string values in a dictionary
|
|
13
|
+
inclusive_subdict, # new dictionary with only the keys in `include`
|
|
14
|
+
exclusive_subdict, # new dictionary with only the keys not in `exclude`.
|
|
15
|
+
merge_dicts, # Merge multiple dictionaries recursively
|
|
16
|
+
)
|
|
12
17
|
from lkj.filesys import get_app_data_dir, get_watermarked_dir, enable_sourcing_from_file
|
|
13
18
|
from lkj.strings import (
|
|
19
|
+
FindReplaceTool, # Tool for finding and replacing substrings in a string
|
|
14
20
|
indent_lines, # Indent all lines of a string
|
|
15
21
|
most_common_indent, # Get the most common indent of a multiline string
|
|
16
22
|
regex_based_substitution,
|
|
17
|
-
|
|
23
|
+
truncate_string, # Truncate a string to a maximum length, inserting a marker in the middle.
|
|
24
|
+
truncate_lines, # Truncate a multiline string to a maximum number of lines
|
|
18
25
|
unique_affixes, # Get unique prefixes or suffixes of a list of strings
|
|
19
26
|
camel_to_snake, # Convert CamelCase to snake_case
|
|
20
27
|
snake_to_camel, # Convert snake_case to CamelCase
|
|
21
28
|
fields_of_string_format, # Extract field names from a string format
|
|
22
|
-
fields_of_string_formats, # Extract field names from an iterable of string formats
|
|
29
|
+
fields_of_string_formats, # Extract field names from an iterable of string formats,
|
|
30
|
+
truncate_string_with_marker, # Deprecated: Backcompatibility alias
|
|
23
31
|
)
|
|
24
32
|
from lkj.loggers import (
|
|
25
33
|
print_with_timestamp,
|
lkj-0.1.36/lkj/dicts.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools for working with dictionaries (and other Mappings).
|
|
3
|
+
|
|
4
|
+
If you are looking for more, check out the `lkj.iterables` module too
|
|
5
|
+
(after all, dicts are iterables).
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def inclusive_subdict(d, include):
|
|
13
|
+
"""
|
|
14
|
+
Returns a new dictionary with only the keys in `include`.
|
|
15
|
+
|
|
16
|
+
Parameters:
|
|
17
|
+
d (dict): The input dictionary.
|
|
18
|
+
include (set): The set of keys to include in the new dictionary.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
|
|
22
|
+
>>> assert inclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'}) == {'a': 1, 'c': 3}
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
return {k: d[k] for k in d.keys() & include}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def exclusive_subdict(d, exclude):
|
|
29
|
+
"""
|
|
30
|
+
Returns a new dictionary with only the keys not in `exclude`.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
d (dict): The input dictionary.
|
|
34
|
+
exclude (set): The set of keys to exclude from the new dictionary.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> exclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'})
|
|
38
|
+
{'b': 2}
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
return {k: d[k] for k in d.keys() - exclude}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Note: There is a copy of truncate_dict_values in the ju package.
|
|
45
|
+
def truncate_dict_values(
|
|
46
|
+
d: dict,
|
|
47
|
+
*,
|
|
48
|
+
max_list_size: Optional[int] = 2,
|
|
49
|
+
max_string_size: Optional[int] = 66,
|
|
50
|
+
middle_marker: str = "...",
|
|
51
|
+
) -> dict:
|
|
52
|
+
"""
|
|
53
|
+
Returns a new dictionary with the same nested keys structure, where:
|
|
54
|
+
- List values are reduced to a maximum size of max_list_size.
|
|
55
|
+
- String values longer than max_string_size are truncated in the middle.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
d (dict): The input dictionary.
|
|
59
|
+
max_list_size (int, optional): Maximum size for lists. Defaults to 2.
|
|
60
|
+
max_string_size (int, optional): Maximum length for strings. Defaults to None (no truncation).
|
|
61
|
+
middle_marker (str, optional): String to insert in the middle of truncated strings. Defaults to '...'.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
dict: A new dictionary with truncated lists and strings.
|
|
65
|
+
|
|
66
|
+
This can be useful when you have a large dictionary that you want to investigate,
|
|
67
|
+
but printing/logging it takes too much space.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
|
|
71
|
+
>>> large_dict = {'a': [1, 2, 3, 4, 5], 'b': {'c': [6, 7, 8, 9], 'd': 'A string like this that is too long'}, 'e': [10, 11]}
|
|
72
|
+
>>> truncate_dict_values(large_dict, max_list_size=3, max_string_size=20)
|
|
73
|
+
{'a': [1, 2, 3], 'b': {'c': [6, 7, 8], 'd': 'A string...too long'}, 'e': [10, 11]}
|
|
74
|
+
|
|
75
|
+
You can use `None` to indicate "no max":
|
|
76
|
+
|
|
77
|
+
>>> assert (
|
|
78
|
+
... truncate_dict_values(large_dict, max_list_size=None, max_string_size=None)
|
|
79
|
+
... == large_dict
|
|
80
|
+
... )
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def truncate_string(value, max_len, marker):
|
|
85
|
+
if max_len is None or len(value) <= max_len:
|
|
86
|
+
return value
|
|
87
|
+
half_len = (max_len - len(marker)) // 2
|
|
88
|
+
return value[:half_len] + marker + value[-half_len:]
|
|
89
|
+
|
|
90
|
+
kwargs = dict(
|
|
91
|
+
max_list_size=max_list_size,
|
|
92
|
+
max_string_size=max_string_size,
|
|
93
|
+
middle_marker=middle_marker,
|
|
94
|
+
)
|
|
95
|
+
if isinstance(d, dict):
|
|
96
|
+
return {k: truncate_dict_values(v, **kwargs) for k, v in d.items()}
|
|
97
|
+
elif isinstance(d, list):
|
|
98
|
+
return (
|
|
99
|
+
[truncate_dict_values(v, **kwargs) for v in d[:max_list_size]]
|
|
100
|
+
if max_list_size is not None
|
|
101
|
+
else d
|
|
102
|
+
)
|
|
103
|
+
elif isinstance(d, str):
|
|
104
|
+
return truncate_string(d, max_string_size, middle_marker)
|
|
105
|
+
else:
|
|
106
|
+
return d
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
from typing import Mapping, Callable, TypeVar, Iterable, Tuple
|
|
110
|
+
|
|
111
|
+
KT = TypeVar("KT") # Key type
|
|
112
|
+
VT = TypeVar("VT") # Value type
|
|
113
|
+
|
|
114
|
+
# Note: Could have all function parameters (recursive_condition, etc.) also take the
|
|
115
|
+
# enumerated index of the mapping as an argument. That would give us even more
|
|
116
|
+
# flexibility, but it might be overkill and make the interface more complex.
|
|
117
|
+
from typing import Mapping, Callable, TypeVar, Iterable, Tuple
|
|
118
|
+
from collections import defaultdict
|
|
119
|
+
|
|
120
|
+
KT = TypeVar("KT") # Key type
|
|
121
|
+
VT = TypeVar("VT") # Value type
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def merge_dicts(
|
|
125
|
+
*mappings: Mapping[KT, VT],
|
|
126
|
+
recursive_condition: Callable[[VT], bool] = lambda v: isinstance(v, Mapping),
|
|
127
|
+
conflict_resolver: Callable[[VT, VT], VT] = lambda x, y: y,
|
|
128
|
+
mapping_constructor: Callable[[Iterable[Tuple[KT, VT]]], Mapping[KT, VT]] = dict,
|
|
129
|
+
) -> Mapping[KT, VT]:
|
|
130
|
+
"""
|
|
131
|
+
Merge multiple mappings into a single mapping, recursively if needed,
|
|
132
|
+
with customizable conflict resolution for non-mapping values.
|
|
133
|
+
|
|
134
|
+
This function generalizes the normal `dict.update()` method, which takes the union
|
|
135
|
+
of the keys and resolves conflicting values by overriding them with the last value.
|
|
136
|
+
While `dict.update()` performs a single-level merge, `merge_dicts` provides additional
|
|
137
|
+
flexibility to handle nested mappings. With `merge_dicts`, you can:
|
|
138
|
+
- Control when to recurse (e.g., based on whether a value is a `Mapping`).
|
|
139
|
+
- Specify how to resolve value conflicts (e.g., override, add, or accumulate in a list).
|
|
140
|
+
- Choose the type of mapping (e.g., `dict`, `defaultdict`) to use as the container.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
mappings: The mappings to merge.
|
|
144
|
+
recursive_condition: A callable to determine if values should be merged recursively.
|
|
145
|
+
By default, checks if the value is a `Mapping`.
|
|
146
|
+
conflict_resolver: A callable that resolves conflicts between two values.
|
|
147
|
+
By default, overrides with the last seen value (`lambda x, y: y`).
|
|
148
|
+
mapping_constructor: A callable to construct the resulting mapping.
|
|
149
|
+
Defaults to the standard `dict` constructor.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
A merged mapping that combines all the input mappings.
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
Basic usage with single-level merge (override behavior):
|
|
156
|
+
>>> dict1 = {"a": 1}
|
|
157
|
+
>>> dict2 = {"a": 2, "b": 3}
|
|
158
|
+
>>> merge_dicts(dict1, dict2)
|
|
159
|
+
{'a': 2, 'b': 3}
|
|
160
|
+
|
|
161
|
+
Handling nested mappings with default behavior (override conflicts):
|
|
162
|
+
>>> dict1 = {"a": 1, "b": {"x": 10, "y": 20}}
|
|
163
|
+
>>> dict2 = {"b": {"y": 30, "z": 40}, "c": 3}
|
|
164
|
+
>>> dict3 = {"b": {"x": 50}, "d": 4}
|
|
165
|
+
>>> merge_dicts(dict1, dict2, dict3)
|
|
166
|
+
{'a': 1, 'b': {'x': 50, 'y': 30, 'z': 40}, 'c': 3, 'd': 4}
|
|
167
|
+
|
|
168
|
+
Resolving conflicts by summing values:
|
|
169
|
+
>>> dict1 = {"a": 1}
|
|
170
|
+
>>> dict2 = {"a": 2}
|
|
171
|
+
>>> merge_dicts(dict1, dict2, conflict_resolver=lambda x, y: x + y)
|
|
172
|
+
{'a': 3}
|
|
173
|
+
|
|
174
|
+
Accumulating conflicting values into a list:
|
|
175
|
+
>>> dict1 = {"a": 1, "b": [1, 2]}
|
|
176
|
+
>>> dict2 = {"b": [3, 4]}
|
|
177
|
+
>>> merge_dicts(dict1, dict2, conflict_resolver=lambda x, y: x + y if isinstance(x, list) else [x, y])
|
|
178
|
+
{'a': 1, 'b': [1, 2, 3, 4]}
|
|
179
|
+
|
|
180
|
+
Recursing only on specific conditions:
|
|
181
|
+
>>> dict1 = {"a": {"nested": 1}}
|
|
182
|
+
>>> dict2 = {"a": {"nested": 2, "new": 3}}
|
|
183
|
+
>>> merge_dicts(dict1, dict2)
|
|
184
|
+
{'a': {'nested': 2, 'new': 3}}
|
|
185
|
+
|
|
186
|
+
>>> dict1 = {"a": {"nested": [1, 2]}}
|
|
187
|
+
>>> dict2 = {"a": {"nested": [3, 4]}}
|
|
188
|
+
>>> merge_dicts(dict1, dict2, recursive_condition=lambda v: isinstance(v, dict))
|
|
189
|
+
{'a': {'nested': [3, 4]}}
|
|
190
|
+
|
|
191
|
+
Using a custom mapping type (`defaultdict`):
|
|
192
|
+
>>> from collections import defaultdict
|
|
193
|
+
>>> merge_dicts(
|
|
194
|
+
... dict1, dict2, mapping_constructor=lambda items: defaultdict(int, items)
|
|
195
|
+
... )
|
|
196
|
+
defaultdict(<class 'int'>, {'a': defaultdict(<class 'int'>, {'nested': [3, 4]})})
|
|
197
|
+
"""
|
|
198
|
+
# Initialize merged mapping with an empty iterable for constructors requiring input
|
|
199
|
+
merged = mapping_constructor([])
|
|
200
|
+
|
|
201
|
+
for mapping in mappings:
|
|
202
|
+
for key, value in mapping.items():
|
|
203
|
+
if (
|
|
204
|
+
key in merged
|
|
205
|
+
and recursive_condition(value)
|
|
206
|
+
and recursive_condition(merged[key])
|
|
207
|
+
):
|
|
208
|
+
# Recursively merge nested mappings
|
|
209
|
+
merged[key] = merge_dicts(
|
|
210
|
+
merged[key],
|
|
211
|
+
value,
|
|
212
|
+
recursive_condition=recursive_condition,
|
|
213
|
+
conflict_resolver=conflict_resolver,
|
|
214
|
+
mapping_constructor=mapping_constructor,
|
|
215
|
+
)
|
|
216
|
+
elif key in merged:
|
|
217
|
+
# Resolve conflict using the provided resolver
|
|
218
|
+
merged[key] = conflict_resolver(merged[key], value)
|
|
219
|
+
else:
|
|
220
|
+
# Otherwise, add the value
|
|
221
|
+
merged[key] = value
|
|
222
|
+
|
|
223
|
+
return merged
|