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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lkj
3
- Version: 0.1.34
3
+ Version: 0.1.36
4
4
  Summary: A dump of homeless useful utils
5
5
  Home-page: https://github.com/thorwhalen/lkj
6
6
  Author: Thor Whalen
@@ -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 truncate_dict_values, inclusive_subdict, exclusive_subdict
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
- truncate_string_with_marker, # Truncate a string to a maximum length, inserting a marker in the middle.
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,
@@ -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