lkj 0.1.33__tar.gz → 0.1.35__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.33 → lkj-0.1.35}/PKG-INFO +1 -1
- {lkj-0.1.33 → lkj-0.1.35}/lkj/__init__.py +10 -3
- lkj-0.1.35/lkj/dicts.py +223 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj/strings.py +78 -20
- {lkj-0.1.33 → lkj-0.1.35}/lkj.egg-info/PKG-INFO +1 -1
- {lkj-0.1.33 → lkj-0.1.35}/setup.cfg +1 -1
- lkj-0.1.33/lkj/dicts.py +0 -106
- {lkj-0.1.33 → lkj-0.1.35}/LICENSE +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/README.md +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj/chunking.py +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj/filesys.py +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj/funcs.py +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj/importing.py +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj/iterables.py +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj/loggers.py +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj/misc.py +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj.egg-info/SOURCES.txt +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj.egg-info/dependency_links.txt +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj.egg-info/not-zip-safe +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/lkj.egg-info/top_level.txt +0 -0
- {lkj-0.1.33 → lkj-0.1.35}/setup.py +0 -0
|
@@ -8,18 +8,25 @@ 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 (
|
|
14
19
|
indent_lines, # Indent all lines of a string
|
|
15
20
|
most_common_indent, # Get the most common indent of a multiline string
|
|
16
21
|
regex_based_substitution,
|
|
17
|
-
|
|
22
|
+
truncate_string, # Truncate a string to a maximum length, inserting a marker in the middle.
|
|
23
|
+
truncate_lines, # Truncate a multiline string to a maximum number of lines
|
|
18
24
|
unique_affixes, # Get unique prefixes or suffixes of a list of strings
|
|
19
25
|
camel_to_snake, # Convert CamelCase to snake_case
|
|
20
26
|
snake_to_camel, # Convert snake_case to CamelCase
|
|
21
27
|
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
|
|
28
|
+
fields_of_string_formats, # Extract field names from an iterable of string formats,
|
|
29
|
+
truncate_string_with_marker, # Deprecated: Backcompatibility alias
|
|
23
30
|
)
|
|
24
31
|
from lkj.loggers import (
|
|
25
32
|
print_with_timestamp,
|
lkj-0.1.35/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
|
+
>>> inclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'})
|
|
22
|
+
{'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
|
|
@@ -136,9 +136,7 @@ def snake_to_camel(snake_string):
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
# Note: Vendored in i2.multi_objects and dol.util
|
|
139
|
-
def
|
|
140
|
-
s, *, left_limit=15, right_limit=15, middle_marker="..."
|
|
141
|
-
):
|
|
139
|
+
def truncate_string(s: str, *, left_limit=15, right_limit=15, middle_marker="..."):
|
|
142
140
|
"""
|
|
143
141
|
Truncate a string to a maximum length, inserting a marker in the middle.
|
|
144
142
|
|
|
@@ -148,23 +146,23 @@ def truncate_string_with_marker(
|
|
|
148
146
|
If the string is shorter than the sum of the left_limit and right_limit,
|
|
149
147
|
the string is returned as is.
|
|
150
148
|
|
|
151
|
-
>>>
|
|
149
|
+
>>> truncate_string('1234567890')
|
|
152
150
|
'1234567890'
|
|
153
151
|
|
|
154
152
|
But if the string is longer than the sum of the limits, it is truncated:
|
|
155
153
|
|
|
156
|
-
>>>
|
|
154
|
+
>>> truncate_string('1234567890', left_limit=3, right_limit=3)
|
|
157
155
|
'123...890'
|
|
158
|
-
>>>
|
|
156
|
+
>>> truncate_string('1234567890', left_limit=3, right_limit=0)
|
|
159
157
|
'123...'
|
|
160
|
-
>>>
|
|
158
|
+
>>> truncate_string('1234567890', left_limit=0, right_limit=3)
|
|
161
159
|
'...890'
|
|
162
160
|
|
|
163
161
|
If you're using a specific parametrization of the function often, you can
|
|
164
162
|
create a partial function with the desired parameters:
|
|
165
163
|
|
|
166
164
|
>>> from functools import partial
|
|
167
|
-
>>> truncate_string = partial(
|
|
165
|
+
>>> truncate_string = partial(truncate_string, left_limit=2, right_limit=2, middle_marker='---')
|
|
168
166
|
>>> truncate_string('1234567890')
|
|
169
167
|
'12---90'
|
|
170
168
|
>>> truncate_string('supercalifragilisticexpialidocious')
|
|
@@ -181,6 +179,54 @@ def truncate_string_with_marker(
|
|
|
181
179
|
return s[:left_limit] + middle_marker + s[-right_limit:]
|
|
182
180
|
|
|
183
181
|
|
|
182
|
+
truncate_string_with_marker = truncate_string # backwards compatibility alias
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def truncate_lines(
|
|
186
|
+
s: str, top_limit: int = None, bottom_limit: int = None, middle_marker: str = "..."
|
|
187
|
+
) -> str:
|
|
188
|
+
"""
|
|
189
|
+
Truncates a string by limiting the number of lines from the top and bottom.
|
|
190
|
+
If the total number of lines is greater than top_limit + bottom_limit,
|
|
191
|
+
it keeps the first `top_limit` lines, keeps the last `bottom_limit` lines,
|
|
192
|
+
and replaces the omitted middle portion with a single line containing
|
|
193
|
+
`middle_marker`.
|
|
194
|
+
|
|
195
|
+
If top_limit or bottom_limit is None, it is treated as 0.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> text = '''Line1
|
|
199
|
+
... Line2
|
|
200
|
+
... Line3
|
|
201
|
+
... Line4
|
|
202
|
+
... Line5
|
|
203
|
+
... Line6'''
|
|
204
|
+
|
|
205
|
+
>>> print(truncate_lines(text, top_limit=2, bottom_limit=2))
|
|
206
|
+
Line1
|
|
207
|
+
Line2
|
|
208
|
+
...
|
|
209
|
+
Line5
|
|
210
|
+
Line6
|
|
211
|
+
"""
|
|
212
|
+
# Interpret None as zero for convenience
|
|
213
|
+
top = top_limit if top_limit is not None else 0
|
|
214
|
+
bottom = bottom_limit if bottom_limit is not None else 0
|
|
215
|
+
|
|
216
|
+
# Split on line boundaries (retaining any trailing newlines in each piece)
|
|
217
|
+
lines = s.splitlines(True)
|
|
218
|
+
total_lines = len(lines)
|
|
219
|
+
|
|
220
|
+
# If no need to truncate, return as is
|
|
221
|
+
if total_lines <= top + bottom:
|
|
222
|
+
return s
|
|
223
|
+
|
|
224
|
+
# Otherwise, keep the top lines, keep the bottom lines,
|
|
225
|
+
# and insert a single marker line in the middle
|
|
226
|
+
truncated = lines[:top] + [middle_marker + "\n"] + lines[-bottom:]
|
|
227
|
+
return "".join(truncated)
|
|
228
|
+
|
|
229
|
+
|
|
184
230
|
# TODO: Generalize so that it can be used with regex keys (not escaped)
|
|
185
231
|
def regex_based_substitution(replacements: dict, regex=None, s: str = None):
|
|
186
232
|
"""
|
|
@@ -201,29 +247,41 @@ def regex_based_substitution(replacements: dict, regex=None, s: str = None):
|
|
|
201
247
|
'I like orange and grapes.'
|
|
202
248
|
|
|
203
249
|
You have access to the ``replacements`` and ``regex`` attributes of the
|
|
204
|
-
``substitute`` function
|
|
250
|
+
``substitute`` function. See how the replacements dict has been ordered by
|
|
251
|
+
descending length of keys. This is to ensure that longer keys are replaced
|
|
252
|
+
before shorter keys, avoiding partial replacements.
|
|
205
253
|
|
|
206
254
|
>>> substitute.replacements
|
|
207
|
-
{'
|
|
255
|
+
{'banana': 'grape', 'apple': 'orange'}
|
|
208
256
|
|
|
209
257
|
"""
|
|
210
258
|
import re
|
|
211
259
|
from functools import partial
|
|
212
260
|
|
|
213
261
|
if regex is None and s is None:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
regex
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
262
|
+
# Sort keys by length while maintaining value alignment
|
|
263
|
+
sorted_replacements = sorted(
|
|
264
|
+
replacements.items(), key=lambda x: len(x[0]), reverse=True
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Create regex pattern from sorted keys (without escaping to allow regex)
|
|
268
|
+
sorted_keys = [pair[0] for pair in sorted_replacements]
|
|
269
|
+
sorted_values = [pair[1] for pair in sorted_replacements]
|
|
270
|
+
regex = re.compile("|".join(sorted_keys))
|
|
271
|
+
|
|
272
|
+
# Prepare the substitution function with aligned replacements
|
|
273
|
+
aligned_replacements = dict(zip(sorted_keys, sorted_values))
|
|
274
|
+
substitute = partial(regex_based_substitution, aligned_replacements, regex)
|
|
275
|
+
substitute.replacements = aligned_replacements
|
|
223
276
|
substitute.regex = regex
|
|
224
277
|
return substitute
|
|
225
|
-
|
|
278
|
+
elif s is not None:
|
|
279
|
+
# Perform substitution using the compiled regex and aligned replacements
|
|
226
280
|
return regex.sub(lambda m: replacements[m.group(0)], s)
|
|
281
|
+
else:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
"Invalid usage: provide either `s` or let the function construct itself."
|
|
284
|
+
)
|
|
227
285
|
|
|
228
286
|
|
|
229
287
|
from typing import Callable, Iterable, Sequence
|
lkj-0.1.33/lkj/dicts.py
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
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
|
-
>>> inclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'})
|
|
22
|
-
{'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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|