flatten-dict 0.5.0__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.
@@ -0,0 +1,7 @@
1
+ from importlib.metadata import version
2
+
3
+ from .flatten_dict import flatten, unflatten # noqa: F401
4
+
5
+ __all__ = ["flatten", "unflatten", "splitter"]
6
+
7
+ __version__ = version("flatten-dict")
@@ -0,0 +1,167 @@
1
+ import inspect
2
+ from collections.abc import Mapping
3
+
4
+ from .reducers import dot_reducer, path_reducer, tuple_reducer, underscore_reducer
5
+ from .splitters import dot_splitter, path_splitter, tuple_splitter, underscore_splitter
6
+
7
+ REDUCER_DICT = {
8
+ "tuple": tuple_reducer,
9
+ "path": path_reducer,
10
+ "dot": dot_reducer,
11
+ "underscore": underscore_reducer,
12
+ }
13
+
14
+ SPLITTER_DICT = {
15
+ "tuple": tuple_splitter,
16
+ "path": path_splitter,
17
+ "dot": dot_splitter,
18
+ "underscore": underscore_splitter,
19
+ }
20
+
21
+
22
+ def flatten(
23
+ d,
24
+ reducer="tuple",
25
+ inverse=False,
26
+ max_flatten_depth=None,
27
+ enumerate_types=(),
28
+ keep_empty_types=(),
29
+ ):
30
+ """Flatten `Mapping` object.
31
+
32
+ Parameters
33
+ ----------
34
+ d : dict-like object
35
+ The dict that will be flattened.
36
+ reducer : {'tuple', 'path', 'underscore', 'dot', Callable}
37
+ The key joining method. If a `Callable` is given, the `Callable` will be
38
+ used to reduce.
39
+ 'tuple': The resulting key will be tuple of the original keys.
40
+ 'path': Use `os.path.join` to join keys.
41
+ 'underscore': Use underscores to join keys.
42
+ 'dot': Use dots to join keys.
43
+ inverse : bool
44
+ Whether you want invert the resulting key and value.
45
+ max_flatten_depth : Optional[int]
46
+ Maximum depth to merge.
47
+ enumerate_types : Sequence[type]
48
+ Flatten these types using `enumerate`.
49
+ For example, if we set `enumerate_types` to ``(list,)``,
50
+ `list` indices become keys: ``{'a': ['b', 'c']}`` -> ``{('a', 0): 'b', ('a', 1): 'c'}``.
51
+ keep_empty_types : Sequence[type]
52
+ By default, ``flatten({1: 2, 3: {}})`` will give you ``{(1,): 2}``, that is, the key ``3``
53
+ will disappear.
54
+ This is also applied for the types in `enumerate_types`, that is,
55
+ ``flatten({1: 2, 3: []}, enumerate_types=(list,))`` will give you ``{(1,): 2}``.
56
+ If you want to keep those empty values, you can specify the types in `keep_empty_types`:
57
+
58
+ >>> flatten({1: 2, 3: {}}, keep_empty_types=(dict,))
59
+ {(1,): 2, (3,): {}}
60
+
61
+ Returns
62
+ -------
63
+ flat_dict : dict
64
+ """
65
+ enumerate_types = tuple(enumerate_types)
66
+ flattenable_types = (Mapping,) + enumerate_types
67
+ if not isinstance(d, flattenable_types):
68
+ raise ValueError(
69
+ "argument type %s is not in the flattenalbe types %s"
70
+ % (type(d), flattenable_types)
71
+ )
72
+
73
+ # check max_flatten_depth
74
+ if max_flatten_depth is not None and max_flatten_depth < 1:
75
+ raise ValueError("max_flatten_depth should not be less than 1.")
76
+
77
+ if isinstance(reducer, str):
78
+ reducer = REDUCER_DICT[reducer]
79
+ reducer_accepts_parent_obj = len(inspect.signature(reducer).parameters) == 3
80
+ flat_dict = {}
81
+
82
+ def _flatten(_d, depth, parent=None):
83
+ key_value_iterable = (
84
+ enumerate(_d) if isinstance(_d, enumerate_types) else _d.items()
85
+ )
86
+ has_item = False
87
+ for key, value in key_value_iterable:
88
+ has_item = True
89
+ if reducer_accepts_parent_obj:
90
+ flat_key = reducer(parent, key, _d)
91
+ else:
92
+ flat_key = reducer(parent, key)
93
+ if isinstance(value, flattenable_types) and (
94
+ max_flatten_depth is None or depth < max_flatten_depth
95
+ ):
96
+ # recursively build the result
97
+ has_child = _flatten(value, depth=depth + 1, parent=flat_key)
98
+ if has_child or not isinstance(value, keep_empty_types):
99
+ # ignore the key in this level because it already has child key
100
+ # or its value is empty
101
+ continue
102
+
103
+ # add an item to the result
104
+ if inverse:
105
+ flat_key, value = value, flat_key
106
+ if flat_key in flat_dict:
107
+ raise ValueError("duplicated key '{}'".format(flat_key))
108
+ flat_dict[flat_key] = value
109
+
110
+ return has_item
111
+
112
+ _flatten(d, depth=1)
113
+ return flat_dict
114
+
115
+
116
+ def nested_set_dict(d, keys, value):
117
+ """Set a value to a sequence of nested keys.
118
+
119
+ Parameters
120
+ ----------
121
+ d : Mapping
122
+ keys : Sequence[str]
123
+ value : Any
124
+ """
125
+ assert keys
126
+ key = keys[0]
127
+ if len(keys) == 1:
128
+ if key in d:
129
+ raise ValueError("duplicated key '{}'".format(key))
130
+ d[key] = value
131
+ return
132
+ d = d.setdefault(key, {})
133
+ nested_set_dict(d, keys[1:], value)
134
+
135
+
136
+ def unflatten(d, splitter="tuple", inverse=False):
137
+ """Unflatten dict-like object.
138
+
139
+ Parameters
140
+ ----------
141
+ d : dict-like object
142
+ The dict that will be unflattened.
143
+ splitter : {'tuple', 'path', 'underscore', 'dot', Callable}
144
+ The key splitting method. If a Callable is given, the Callable will be
145
+ used to split `d`.
146
+ 'tuple': Use each element in the tuple key as the key of the unflattened dict.
147
+ 'path': Use `pathlib.Path.parts` to split keys.
148
+ 'underscore': Use underscores to split keys.
149
+ 'dot': Use dots to split keys.
150
+ inverse : bool
151
+ Whether you want to invert the key and value before flattening.
152
+
153
+ Returns
154
+ -------
155
+ unflattened_dict : dict
156
+ """
157
+ if isinstance(splitter, str):
158
+ splitter = SPLITTER_DICT[splitter]
159
+
160
+ unflattened_dict = {}
161
+ for flat_key, value in d.items():
162
+ if inverse:
163
+ flat_key, value = value, flat_key
164
+ key_tuple = splitter(flat_key)
165
+ nested_set_dict(unflattened_dict, key_tuple, value)
166
+
167
+ return unflattened_dict
@@ -0,0 +1,11 @@
1
+ """`flatten_dict.reducer` is deprecated in favor of `flatten_dict.reducers`."""
2
+
3
+ import warnings
4
+
5
+ from .reducers import * # noqa: F401, F403
6
+
7
+ warnings.warn(
8
+ "`flatten_dict.reducer` is deprecated in favor of `flatten_dict.reducers`.",
9
+ FutureWarning,
10
+ stacklevel=2,
11
+ )
@@ -0,0 +1,46 @@
1
+ def tuple_reducer(k1, k2):
2
+ if k1 is None:
3
+ return (k2,)
4
+ return k1 + (k2,)
5
+
6
+
7
+ def path_reducer(k1, k2):
8
+ import os.path
9
+
10
+ if k1 is None:
11
+ return k2
12
+ return os.path.join(k1, k2)
13
+
14
+
15
+ def dot_reducer(k1, k2):
16
+ if k1 is None:
17
+ return k2
18
+ return "{}.{}".format(k1, k2)
19
+
20
+
21
+ def underscore_reducer(k1, k2):
22
+ if k1 is None:
23
+ return k2
24
+ return "{}_{}".format(k1, k2)
25
+
26
+
27
+ def make_reducer(delimiter):
28
+ """Create a reducer with a custom delimiter.
29
+
30
+ Parameters
31
+ ----------
32
+ delimiter : str
33
+ Delimiter to use to join keys.
34
+
35
+ Returns
36
+ -------
37
+ f : Callable
38
+ Callable that can be passed to `flatten()`'s `reducer` argument.
39
+ """
40
+
41
+ def f(k1, k2):
42
+ if k1 is None:
43
+ return k2
44
+ return "{}{}{}".format(k1, delimiter, k2)
45
+
46
+ return f
@@ -0,0 +1,11 @@
1
+ """`flatten_dict.splitter` is deprecated in favor of `flatten_dict.splitters`."""
2
+
3
+ import warnings
4
+
5
+ from .splitters import * # noqa: F401, F403
6
+
7
+ warnings.warn(
8
+ "`flatten_dict.splitter` is deprecated in favor of `flatten_dict.splitters`.",
9
+ FutureWarning,
10
+ stacklevel=2,
11
+ )
@@ -0,0 +1,40 @@
1
+ def tuple_splitter(flat_key):
2
+ return flat_key
3
+
4
+
5
+ def path_splitter(flat_key):
6
+ from pathlib import PurePath
7
+
8
+ keys = PurePath(flat_key).parts
9
+ return keys
10
+
11
+
12
+ def dot_splitter(flat_key):
13
+ keys = tuple(flat_key.split("."))
14
+ return keys
15
+
16
+
17
+ def underscore_splitter(flat_key):
18
+ keys = tuple(flat_key.split("_"))
19
+ return keys
20
+
21
+
22
+ def make_splitter(delimiter):
23
+ """Create a reducer with a custom delimiter.
24
+
25
+ Parameters
26
+ ----------
27
+ delimiter : str
28
+ Delimiter to use to split keys.
29
+
30
+ Returns
31
+ -------
32
+ f : Callable
33
+ Callable that can be passed to ``unflatten``'s ``splitter`` argument.
34
+ """
35
+
36
+ def f(flat_key):
37
+ keys = tuple(flat_key.split(delimiter))
38
+ return keys
39
+
40
+ return f
@@ -0,0 +1,474 @@
1
+ import json
2
+ import os.path
3
+ from types import GeneratorType
4
+
5
+ import pytest
6
+
7
+ from flatten_dict import flatten, unflatten
8
+ from flatten_dict.reducers import (
9
+ make_reducer,
10
+ path_reducer,
11
+ tuple_reducer,
12
+ underscore_reducer,
13
+ )
14
+ from flatten_dict.splitters import (
15
+ make_splitter,
16
+ path_splitter,
17
+ tuple_splitter,
18
+ underscore_splitter,
19
+ )
20
+
21
+
22
+ @pytest.fixture
23
+ def normal_dict():
24
+ return {
25
+ "a": "0",
26
+ "b": {
27
+ "a": "1.0",
28
+ "b": "1.1",
29
+ },
30
+ "c": {
31
+ "a": "2.0",
32
+ "b": {
33
+ "a": "2.1.0",
34
+ "b": "2.1.1",
35
+ },
36
+ },
37
+ }
38
+
39
+
40
+ @pytest.fixture
41
+ def flat_tuple_dict():
42
+ return {
43
+ ("a",): "0",
44
+ ("b", "a"): "1.0",
45
+ ("b", "b"): "1.1",
46
+ ("c", "a"): "2.0",
47
+ ("c", "b", "a"): "2.1.0",
48
+ ("c", "b", "b"): "2.1.1",
49
+ }
50
+
51
+
52
+ @pytest.fixture
53
+ def flat_tuple_dict_depth2():
54
+ return {
55
+ ("a",): "0",
56
+ ("b", "a"): "1.0",
57
+ ("b", "b"): "1.1",
58
+ ("c", "a"): "2.0",
59
+ ("c", "b"): {"a": "2.1.0", "b": "2.1.1"},
60
+ }
61
+
62
+
63
+ @pytest.fixture
64
+ def normal_dict_with_nested_lists():
65
+ return {
66
+ "aaa": "0",
67
+ "bbb": ["1.1", "1.2", "1.3"],
68
+ "ccc": [{"aaa": "2.1.1"}, {"aaa": "2.2.1"}],
69
+ "ddd": [["3.1.1", "3.1.2", "3.1.3"], ["3.2.1", "3.2.2", "3.2.3"]],
70
+ "eee": [
71
+ {"aaa": ["4.1.1", "4.1.2", "4.1.3"]},
72
+ {"bbb": ["4.2.1", "4.2.2", "4.2.3"]},
73
+ ],
74
+ }
75
+
76
+
77
+ @pytest.fixture
78
+ def flat_dict_with_nested_lists_with_list_syntax():
79
+ return {
80
+ "aaa": "0",
81
+ "bbb[0]": "1.1",
82
+ "bbb[1]": "1.2",
83
+ "bbb[2]": "1.3",
84
+ "ccc[0].aaa": "2.1.1",
85
+ "ccc[1].aaa": "2.2.1",
86
+ "ddd[0][0]": "3.1.1",
87
+ "ddd[0][1]": "3.1.2",
88
+ "ddd[0][2]": "3.1.3",
89
+ "ddd[1][0]": "3.2.1",
90
+ "ddd[1][1]": "3.2.2",
91
+ "ddd[1][2]": "3.2.3",
92
+ "eee[0].aaa[0]": "4.1.1",
93
+ "eee[0].aaa[1]": "4.1.2",
94
+ "eee[0].aaa[2]": "4.1.3",
95
+ "eee[1].bbb[0]": "4.2.1",
96
+ "eee[1].bbb[1]": "4.2.2",
97
+ "eee[1].bbb[2]": "4.2.3",
98
+ }
99
+
100
+
101
+ def get_flat_tuple_dict(flat_tuple_dict):
102
+ return flat_tuple_dict
103
+
104
+
105
+ def get_flat_path_dict(flat_tuple_dict):
106
+ return {os.path.join(*k): v for k, v in flat_tuple_dict.items()}
107
+
108
+
109
+ def get_flat_underscore_dict(flat_tuple_dict):
110
+ return {"_".join(k): v for k, v in flat_tuple_dict.items()}
111
+
112
+
113
+ @pytest.fixture
114
+ def inv_flat_tuple_dict(flat_tuple_dict):
115
+ return {v: k for k, v in flat_tuple_dict.items()}
116
+
117
+
118
+ def test_flatten_dict(normal_dict, flat_tuple_dict):
119
+ assert flatten(normal_dict) == flat_tuple_dict
120
+
121
+
122
+ def test_flatten_dict_invalid_depth_limit(normal_dict):
123
+ with pytest.raises(ValueError):
124
+ flatten(normal_dict, max_flatten_depth=0)
125
+
126
+
127
+ def test_flatten_dict_depth_limit_1(normal_dict):
128
+ flattened = flatten(normal_dict, max_flatten_depth=1)
129
+ values_before = sorted(
130
+ flattened.values(), key=lambda x: json.dumps(x, sort_keys=True)
131
+ )
132
+ values_after = sorted(
133
+ normal_dict.values(), key=lambda x: json.dumps(x, sort_keys=True)
134
+ )
135
+ assert values_before == values_after
136
+
137
+
138
+ def test_flatten_dict_depth_limit_2(normal_dict, flat_tuple_dict_depth2):
139
+ assert flatten(normal_dict, max_flatten_depth=2) == flat_tuple_dict_depth2
140
+
141
+
142
+ def test_flatten_dict_irrelevant_depth_limit(normal_dict, flat_tuple_dict):
143
+ assert flatten(normal_dict, max_flatten_depth=3) == flat_tuple_dict
144
+
145
+
146
+ def test_flatten_nonflattenable_type():
147
+ with pytest.raises(ValueError):
148
+ flatten([])
149
+
150
+
151
+ @pytest.mark.parametrize(
152
+ "reducer, expected_flat_dict_func",
153
+ [
154
+ ("tuple", get_flat_tuple_dict),
155
+ ("path", get_flat_path_dict),
156
+ ("underscore", get_flat_underscore_dict),
157
+ (tuple_reducer, get_flat_tuple_dict),
158
+ (path_reducer, get_flat_path_dict),
159
+ (underscore_reducer, get_flat_underscore_dict),
160
+ ],
161
+ )
162
+ def test_flatten_dict_with_reducer(
163
+ normal_dict, flat_tuple_dict, reducer, expected_flat_dict_func
164
+ ):
165
+ expected_flat_dict = expected_flat_dict_func(flat_tuple_dict)
166
+ assert flatten(normal_dict, reducer=reducer) == expected_flat_dict
167
+
168
+
169
+ def test_flatten_dict_inverse(normal_dict, inv_flat_tuple_dict):
170
+ assert flatten(normal_dict, inverse=True) == inv_flat_tuple_dict
171
+
172
+
173
+ def test_flatten_dict_inverse_with_duplicated_value(normal_dict):
174
+ dup_val_dict = normal_dict.copy()
175
+ dup_val_dict["a"] = "2.1.1"
176
+ with pytest.raises(ValueError):
177
+ flatten(dup_val_dict, inverse=True)
178
+
179
+
180
+ def test_flatten_dict_with_list_syntax(
181
+ normal_dict_with_nested_lists, flat_dict_with_nested_lists_with_list_syntax
182
+ ):
183
+ def _reducer(parent_path, key, parent_obj):
184
+ if parent_path is None:
185
+ return key
186
+ if isinstance(parent_obj, list):
187
+ return "{}[{}]".format(parent_path, key)
188
+ return "{}.{}".format(parent_path, key)
189
+
190
+ assert (
191
+ flatten(
192
+ normal_dict_with_nested_lists, reducer=_reducer, enumerate_types=(list,)
193
+ )
194
+ == flat_dict_with_nested_lists_with_list_syntax
195
+ )
196
+
197
+
198
+ def test_unflatten_dict(normal_dict, flat_tuple_dict):
199
+ assert unflatten(flat_tuple_dict) == normal_dict
200
+
201
+
202
+ def test_unflatten_dict_inverse(normal_dict, inv_flat_tuple_dict):
203
+ assert unflatten(inv_flat_tuple_dict, inverse=True) == normal_dict
204
+
205
+
206
+ @pytest.mark.parametrize(
207
+ "splitter, flat_dict_func",
208
+ [
209
+ ("tuple", get_flat_tuple_dict),
210
+ ("path", get_flat_path_dict),
211
+ ("underscore", get_flat_underscore_dict),
212
+ (tuple_splitter, get_flat_tuple_dict),
213
+ (path_splitter, get_flat_path_dict),
214
+ (underscore_splitter, get_flat_underscore_dict),
215
+ ],
216
+ )
217
+ def test_unflatten_dict_with_splitter(
218
+ normal_dict, flat_tuple_dict, splitter, flat_dict_func
219
+ ):
220
+ flat_dict = flat_dict_func(flat_tuple_dict)
221
+ assert unflatten(flat_dict, splitter=splitter) == normal_dict
222
+
223
+
224
+ def test_unflatten_dict_inverse_with_duplicated_value(
225
+ flat_tuple_dict, inv_flat_tuple_dict
226
+ ):
227
+ inv_flat_tuple_dict["2.1.1"] = ("c", "b", "a")
228
+ with pytest.raises(ValueError):
229
+ unflatten(inv_flat_tuple_dict, inverse=True)
230
+
231
+
232
+ @pytest.fixture
233
+ def dict_with_list():
234
+ return {
235
+ "a": "0",
236
+ "b": {
237
+ "a": "1.0",
238
+ "b": "1.1",
239
+ },
240
+ "c": {
241
+ "a": "2.0",
242
+ "b": {
243
+ "a": "2.1.0",
244
+ "b": ["2.1.1.0", "2.1.1.1"],
245
+ "c": [],
246
+ },
247
+ },
248
+ }
249
+
250
+
251
+ @pytest.fixture
252
+ def flat_tuple_dict_with_list():
253
+ return {
254
+ ("a",): "0",
255
+ ("b", "a"): "1.0",
256
+ ("b", "b"): "1.1",
257
+ ("c", "a"): "2.0",
258
+ ("c", "b", "a"): "2.1.0",
259
+ ("c", "b", "b"): ["2.1.1.0", "2.1.1.1"],
260
+ ("c", "b", "c"): [],
261
+ }
262
+
263
+
264
+ @pytest.fixture
265
+ def flat_tuple_dict_with_list_depth2():
266
+ return {
267
+ ("a",): "0",
268
+ ("b", "a"): "1.0",
269
+ ("b", "b"): "1.1",
270
+ ("c", "a"): "2.0",
271
+ ("c", "b"): {
272
+ "a": "2.1.0",
273
+ "b": ["2.1.1.0", "2.1.1.1"],
274
+ "c": [],
275
+ },
276
+ }
277
+
278
+
279
+ def test_flatten_dict_with_list(dict_with_list, flat_tuple_dict_with_list):
280
+ assert flatten(dict_with_list) == flat_tuple_dict_with_list
281
+
282
+
283
+ def test_flatten_dict_with_list_depth_limit_1(dict_with_list):
284
+ flattened = flatten(dict_with_list, max_flatten_depth=1)
285
+ before = sorted(dict_with_list.items(), key=lambda x: x[0])
286
+ after = sorted(flattened.items(), key=lambda x: x[0])
287
+ # flatten with the default reducer transforms keys to tuples
288
+ after = [(x[0][0], x[1]) for x in after]
289
+ assert before == after
290
+
291
+
292
+ def test_flatten_dict_with_list_depth_limit_2(
293
+ dict_with_list, flat_tuple_dict_with_list_depth2
294
+ ):
295
+ assert (
296
+ flatten(dict_with_list, max_flatten_depth=2) == flat_tuple_dict_with_list_depth2
297
+ )
298
+
299
+
300
+ def test_flatten_dict_with_list_irrelevant_depth_limit(
301
+ dict_with_list, flat_tuple_dict_with_list
302
+ ):
303
+ assert flatten(dict_with_list, max_flatten_depth=3) == flat_tuple_dict_with_list
304
+
305
+
306
+ @pytest.mark.parametrize(
307
+ "reducer, expected_flat_dict_func",
308
+ [
309
+ ("tuple", get_flat_tuple_dict),
310
+ ("path", get_flat_path_dict),
311
+ ("underscore", get_flat_underscore_dict),
312
+ (tuple_reducer, get_flat_tuple_dict),
313
+ (path_reducer, get_flat_path_dict),
314
+ (underscore_reducer, get_flat_underscore_dict),
315
+ ],
316
+ )
317
+ def test_flatten_dict_with_list_with_reducer(
318
+ dict_with_list, flat_tuple_dict_with_list, reducer, expected_flat_dict_func
319
+ ):
320
+ expected_flat_dict = expected_flat_dict_func(flat_tuple_dict_with_list)
321
+ assert flatten(dict_with_list, reducer=reducer) == expected_flat_dict
322
+
323
+
324
+ def test_unflatten_dict_with_list(dict_with_list, flat_tuple_dict_with_list):
325
+ assert unflatten(flat_tuple_dict_with_list) == dict_with_list
326
+
327
+
328
+ @pytest.mark.parametrize(
329
+ "splitter, flat_dict_func",
330
+ [
331
+ ("tuple", get_flat_tuple_dict),
332
+ ("path", get_flat_path_dict),
333
+ ("underscore", get_flat_underscore_dict),
334
+ (tuple_splitter, get_flat_tuple_dict),
335
+ (path_splitter, get_flat_path_dict),
336
+ (underscore_splitter, get_flat_underscore_dict),
337
+ ],
338
+ )
339
+ def test_unflatten_dict_with_list_with_splitter(
340
+ dict_with_list, flat_tuple_dict_with_list, splitter, flat_dict_func
341
+ ):
342
+ flat_dict = flat_dict_func(flat_tuple_dict_with_list)
343
+ assert unflatten(flat_dict, splitter=splitter) == dict_with_list
344
+
345
+
346
+ @pytest.fixture
347
+ def flat_tuple_dict_with_enumerated_list():
348
+ return {
349
+ ("a",): "0",
350
+ ("b", "a"): "1.0",
351
+ ("b", "b"): "1.1",
352
+ ("c", "a"): "2.0",
353
+ ("c", "b", "a"): "2.1.0",
354
+ ("c", "b", "b", 0): "2.1.1.0",
355
+ ("c", "b", "b", 1): "2.1.1.1",
356
+ }
357
+
358
+
359
+ def test_flatten_dict_with_list_with_enumerate_types(
360
+ dict_with_list, flat_tuple_dict_with_enumerated_list
361
+ ):
362
+ assert (
363
+ flatten(dict_with_list, enumerate_types=(list,))
364
+ == flat_tuple_dict_with_enumerated_list
365
+ )
366
+
367
+
368
+ def test_flatten_list():
369
+ assert flatten([1, 2], enumerate_types=(list,)) == {(0,): 1, (1,): 2}
370
+
371
+
372
+ @pytest.fixture
373
+ def dict_with_generator():
374
+ return {
375
+ "a": "0",
376
+ "b": {
377
+ "a": "1.0",
378
+ "b": "1.1",
379
+ },
380
+ "c": {
381
+ "a": "2.0",
382
+ "b": {
383
+ "a": "2.1.0",
384
+ "b": ("2.1.1.%d" % i for i in range(2)),
385
+ "c": (i for i in ()), # empty generator
386
+ },
387
+ },
388
+ }
389
+
390
+
391
+ def test_flatten_dict_with_generator_with_enumerate_types(
392
+ dict_with_generator, flat_tuple_dict_with_enumerated_list
393
+ ):
394
+ assert (
395
+ flatten(dict_with_generator, enumerate_types=(GeneratorType,))
396
+ == flat_tuple_dict_with_enumerated_list
397
+ )
398
+
399
+
400
+ @pytest.fixture
401
+ def dict_with_empty_dict():
402
+ return {
403
+ "a": "0",
404
+ "b": {
405
+ "a": "1.0",
406
+ "b": "1.1",
407
+ },
408
+ "c": {
409
+ "a": "2.0",
410
+ "b": {
411
+ "a": "2.1.0",
412
+ "b": "2.1.1",
413
+ "c": {},
414
+ },
415
+ },
416
+ }
417
+
418
+
419
+ @pytest.fixture
420
+ def flat_tuple_dict_with_empty_dict():
421
+ return {
422
+ ("a",): "0",
423
+ ("b", "a"): "1.0",
424
+ ("b", "b"): "1.1",
425
+ ("c", "a"): "2.0",
426
+ ("c", "b", "a"): "2.1.0",
427
+ ("c", "b", "b"): "2.1.1",
428
+ ("c", "b", "c"): {},
429
+ }
430
+
431
+
432
+ def test_flatten_dict_with_empty_dict(dict_with_empty_dict, flat_tuple_dict):
433
+ assert flatten(dict_with_empty_dict) == flat_tuple_dict
434
+
435
+
436
+ def test_flatten_dict_with_empty_dict_kept(
437
+ dict_with_empty_dict, flat_tuple_dict_with_empty_dict
438
+ ):
439
+ assert (
440
+ flatten(dict_with_empty_dict, keep_empty_types=(dict,))
441
+ == flat_tuple_dict_with_empty_dict
442
+ )
443
+
444
+
445
+ def test_flatten_dict_with_keep_empty_types(normal_dict, flat_tuple_dict):
446
+ assert flatten(normal_dict, keep_empty_types=(dict, str)) == flat_tuple_dict
447
+
448
+
449
+ @pytest.mark.parametrize(
450
+ "delimiter, delimiter_equivalent", [(".", "dot"), ("_", "underscore")]
451
+ )
452
+ def test_make_reducer(normal_dict, delimiter, delimiter_equivalent):
453
+ reducer = make_reducer(delimiter)
454
+ flattened_dict_using_make_reducer = flatten(normal_dict, reducer=reducer)
455
+ flattened_dict_using_equivalent_reducer = flatten(
456
+ normal_dict, reducer=delimiter_equivalent
457
+ )
458
+ assert flattened_dict_using_make_reducer == flattened_dict_using_equivalent_reducer
459
+
460
+
461
+ @pytest.mark.parametrize(
462
+ "delimiter, delimiter_equivalent", [(".", "dot"), ("_", "underscore")]
463
+ )
464
+ def test_make_splitter(normal_dict, delimiter, delimiter_equivalent):
465
+ splitter = make_splitter(delimiter)
466
+ flat_dict = flatten(normal_dict, delimiter_equivalent)
467
+ unflattened_dict_using_make_splitter = unflatten(flat_dict, splitter=splitter)
468
+ unflattened_dict_using_equivalent_splitter = unflatten(
469
+ flat_dict, splitter=delimiter_equivalent
470
+ )
471
+ assert (
472
+ unflattened_dict_using_make_splitter
473
+ == unflattened_dict_using_equivalent_splitter
474
+ )
@@ -0,0 +1,287 @@
1
+ Metadata-Version: 2.4
2
+ Name: flatten-dict
3
+ Version: 0.5.0
4
+ Summary: A flexible utility for flattening and unflattening dict-like objects in Python.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: Ian Lin
8
+ Author-email: you@example.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Utilities
18
+ Project-URL: Homepage, https://github.com/ianlini/flatten-dict
19
+ Project-URL: Repository, https://github.com/ianlini/flatten-dict
20
+ Description-Content-Type: text/x-rst
21
+
22
+ flatten-dict
23
+ ============
24
+ .. image:: https://github.com/ianlini/flatten-dict/actions/workflows/main.yml/badge.svg
25
+ :target: https://github.com/ianlini/flatten-dict/actions
26
+ .. image:: https://img.shields.io/pypi/v/flatten-dict.svg
27
+ :target: https://pypi.org/project/flatten-dict/
28
+ .. image:: https://img.shields.io/pypi/l/flatten-dict.svg
29
+ :target: https://github.com/ianlini/flatten-dict/blob/master/LICENSE
30
+ .. image:: https://img.shields.io/github/stars/ianlini/flatten-dict.svg?style=social
31
+ :target: https://github.com/ianlini/flatten-dict
32
+
33
+ A flexible utility for flattening and unflattening dict-like objects in Python.
34
+
35
+
36
+ Introduction
37
+ ------------
38
+ This package provides a function ``flatten()`` for flattening dict-like objects in Python (tested with 3.10-3.15).
39
+ It also provides some key joining methods (reducer), and you can choose the reducer you want or even implement your own reducer.
40
+ You can also invert the resulting flat dict using ``unflatten()``.
41
+
42
+ Installation
43
+ ------------
44
+
45
+ .. code-block:: bash
46
+
47
+ pip install flatten-dict
48
+
49
+ Documentation
50
+ -------------
51
+
52
+ Flatten
53
+ ```````
54
+
55
+ .. code-block:: python
56
+
57
+ def flatten(d, reducer='tuple', inverse=False, enumerate_types=(), keep_empty_types=()):
58
+ """Flatten `Mapping` object.
59
+
60
+ Parameters
61
+ ----------
62
+ d : dict-like object
63
+ The dict that will be flattened.
64
+ reducer : {'tuple', 'path', 'underscore', 'dot', Callable}
65
+ The key joining method. If a `Callable` is given, the `Callable` will be
66
+ used to reduce.
67
+ 'tuple': The resulting key will be tuple of the original keys.
68
+ 'path': Use `os.path.join` to join keys.
69
+ 'underscore': Use underscores to join keys.
70
+ 'dot': Use dots to join keys.
71
+ inverse : bool
72
+ Whether you want invert the resulting key and value.
73
+ max_flatten_depth : int
74
+ Maximum depth to merge.
75
+ enumerate_types : Sequence[type]
76
+ Flatten these types using `enumerate`.
77
+ For example, if we set `enumerate_types` to ``(list,)``,
78
+ `list` indices become keys: ``{'a': ['b', 'c']}`` -> ``{('a', 0): 'b', ('a', 1): 'c'}``.
79
+ keep_empty_types : Sequence[type]
80
+ By default, ``flatten({1: 2, 3: {}})`` will give you ``{(1,): 2}``, that is, the key ``3``
81
+ will disappear.
82
+ This is also applied for the types in `enumerate_types`, that is,
83
+ ``flatten({1: 2, 3: []}, enumerate_types=(list,))`` will give you ``{(1,): 2}``.
84
+ If you want to keep those empty values, you can specify the types in `keep_empty_types`:
85
+
86
+ >>> flatten({1: 2, 3: {}}, keep_empty_types=(dict,))
87
+ {(1,): 2, (3,): {}}
88
+
89
+ Returns
90
+ -------
91
+ flat_dict : dict
92
+ """
93
+
94
+ Examples
95
+ ::::::::
96
+
97
+ >>> from flatten_dict import flatten
98
+ >>> from pprint import pprint
99
+ >>> normal_dict = {
100
+ ... 'a': '0',
101
+ ... 'b': {
102
+ ... 'a': '1.0',
103
+ ... 'b': '1.1',
104
+ ... },
105
+ ... 'c': {
106
+ ... 'a': '2.0',
107
+ ... 'b': {
108
+ ... 'a': '2.1.0',
109
+ ... 'b': '2.1.1',
110
+ ... },
111
+ ... },
112
+ ... }
113
+ >>> pprint(flatten(normal_dict))
114
+ {('a',): '0',
115
+ ('b', 'a'): '1.0',
116
+ ('b', 'b'): '1.1',
117
+ ('c', 'a'): '2.0',
118
+ ('c', 'b', 'a'): '2.1.0',
119
+ ('c', 'b', 'b'): '2.1.1'}
120
+ >>> pprint(flatten(normal_dict, reducer='path'))
121
+ {'a': '0',
122
+ 'b/a': '1.0',
123
+ 'b/b': '1.1',
124
+ 'c/a': '2.0',
125
+ 'c/b/a': '2.1.0',
126
+ 'c/b/b': '2.1.1'}
127
+ >>> pprint(flatten(normal_dict, reducer='path', inverse=True))
128
+ {'0': 'a',
129
+ '1.0': 'b/a',
130
+ '1.1': 'b/b',
131
+ '2.0': 'c/a',
132
+ '2.1.0': 'c/b/a',
133
+ '2.1.1': 'c/b/b'}
134
+ >>> pprint(flatten(normal_dict, reducer='path', max_flatten_depth=2))
135
+ {'a': '0',
136
+ 'b/a': '1.0',
137
+ 'b/b': '1.1',
138
+ 'c/a': '2.0',
139
+ 'c/b': {'a': '2.1.0', 'b': '2.1.1'}}
140
+
141
+ The `reducer` parameter supports ``'tuple'``, ``'path'``, ``'underscore'``, ``'dot'`` and `Callable`. We can customize the reducer using a function:
142
+
143
+ >>> def underscore_reducer(k1, k2):
144
+ ... if k1 is None:
145
+ ... return k2
146
+ ... else:
147
+ ... return k1 + "_" + k2
148
+ ...
149
+ >>> pprint(flatten(normal_dict, reducer=underscore_reducer))
150
+ {'a': '0',
151
+ 'b_a': '1.0',
152
+ 'b_b': '1.1',
153
+ 'c_a': '2.0',
154
+ 'c_b_a': '2.1.0',
155
+ 'c_b_b': '2.1.1'}
156
+
157
+ There is also a factory function `make_reducer()` to help you create customized reducer. The function currently only supports customized delimiter:
158
+
159
+ >>> from flatten_dict.reducers import make_reducer
160
+ >>> pprint(flatten(normal_dict, reducer=make_reducer(delimiter='_')))
161
+ {'a': '0',
162
+ 'b_a': '1.0',
163
+ 'b_b': '1.1',
164
+ 'c_a': '2.0',
165
+ 'c_b_a': '2.1.0',
166
+ 'c_b_b': '2.1.1'}
167
+
168
+ If we have some iterable (e.g., `list`) in the `dict`, we will normally get this:
169
+
170
+ >>> flatten({'a': [1, 2, 3], 'b': 'c'})
171
+ {('a',): [1, 2, 3], ('b',): 'c'}
172
+
173
+ If we want to use its indices as keys, then we can use the parameter `enumerate_types`:
174
+
175
+ >>> flatten({'a': [1, 2, 3], 'b': 'c'}, enumerate_types=(list,))
176
+ {('a', 0): 1, ('a', 1): 2, ('a', 2): 3, ('b',): 'c'}
177
+
178
+ We can even flatten a `list` directly:
179
+
180
+ >>> flatten([1, 2, 3], enumerate_types=(list,))
181
+ {(0,): 1, (1,): 2, (2,): 3}
182
+
183
+ If there is an empty dict in the values, by default, it will disappear after flattened:
184
+
185
+ >>> flatten({1: 2, 3: {}})
186
+ {(1,): 2}
187
+
188
+ We can keep the empty dict in the result using ``keep_empty_types=(dict,)``:
189
+
190
+ >>> flatten({1: 2, 3: {}}, keep_empty_types=(dict,))
191
+ {(1,): 2, (3,): {}}
192
+
193
+ Unflatten
194
+ `````````
195
+
196
+ .. code-block:: python
197
+
198
+ def unflatten(d, splitter='tuple', inverse=False):
199
+ """Unflatten dict-like object.
200
+
201
+ Parameters
202
+ ----------
203
+ d : dict-like object
204
+ The dict that will be unflattened.
205
+ splitter : {'tuple', 'path', 'underscore', 'dot', Callable}
206
+ The key splitting method. If a Callable is given, the Callable will be
207
+ used to split `d`.
208
+ 'tuple': Use each element in the tuple key as the key of the unflattened dict.
209
+ 'path': Use `pathlib.Path.parts` to split keys.
210
+ 'underscore': Use underscores to split keys.
211
+ 'dot': Use dots to split keys.
212
+ inverse : bool
213
+ Whether you want to invert the key and value before flattening.
214
+
215
+ Returns
216
+ -------
217
+ unflattened_dict : dict
218
+ """
219
+
220
+ Examples
221
+ ::::::::
222
+
223
+ >>> from pprint import pprint
224
+ >>> from flatten_dict import unflatten
225
+ >>> flat_dict = {
226
+ ... ('a',): '0',
227
+ ... ('b', 'a'): '1.0',
228
+ ... ('b', 'b'): '1.1',
229
+ ... ('c', 'a'): '2.0',
230
+ ... ('c', 'b', 'a'): '2.1.0',
231
+ ... ('c', 'b', 'b'): '2.1.1',
232
+ ... }
233
+ >>> pprint(unflatten(flat_dict))
234
+ {'a': '0',
235
+ 'b': {'a': '1.0', 'b': '1.1'},
236
+ 'c': {'a': '2.0', 'b': {'a': '2.1.0', 'b': '2.1.1'}}}
237
+ >>> flat_dict = {
238
+ ... 'a': '0',
239
+ ... 'b/a': '1.0',
240
+ ... 'b/b': '1.1',
241
+ ... 'c/a': '2.0',
242
+ ... 'c/b/a': '2.1.0',
243
+ ... 'c/b/b': '2.1.1',
244
+ ... }
245
+ >>> pprint(unflatten(flat_dict, splitter='path'))
246
+ {'a': '0',
247
+ 'b': {'a': '1.0', 'b': '1.1'},
248
+ 'c': {'a': '2.0', 'b': {'a': '2.1.0', 'b': '2.1.1'}}}
249
+ >>> flat_dict = {
250
+ ... '0': 'a',
251
+ ... '1.0': 'b/a',
252
+ ... '1.1': 'b/b',
253
+ ... '2.0': 'c/a',
254
+ ... '2.1.0': 'c/b/a',
255
+ ... '2.1.1': 'c/b/b',
256
+ ... }
257
+ >>> pprint(unflatten(flat_dict, splitter='path', inverse=True))
258
+ {'a': '0',
259
+ 'b': {'a': '1.0', 'b': '1.1'},
260
+ 'c': {'a': '2.0', 'b': {'a': '2.1.0', 'b': '2.1.1'}}}
261
+
262
+ The `splitter` parameter supports ``'tuple'``, ``'path'``, ``'underscore'``, ``'dot'`` and `Callable`. We can customize the reducer using a function:
263
+
264
+ >>> def underscore_splitter(flat_key):
265
+ ... return flat_key.split("_")
266
+ ...
267
+ >>> flat_dict = {
268
+ ... 'a': '0',
269
+ ... 'b_a': '1.0',
270
+ ... 'b_b': '1.1',
271
+ ... 'c_a': '2.0',
272
+ ... 'c_b_a': '2.1.0',
273
+ ... 'c_b_b': '2.1.1',
274
+ ... }
275
+ >>> pprint(unflatten(flat_dict, splitter=underscore_splitter))
276
+ {'a': '0',
277
+ 'b': {'a': '1.0', 'b': '1.1'},
278
+ 'c': {'a': '2.0', 'b': {'a': '2.1.0', 'b': '2.1.1'}}}
279
+
280
+ There is also a factory function `make_splitter()` to help you create customized splitter. The function currently only supports customized delimiter:
281
+
282
+ >>> from flatten_dict.splitters import make_splitter
283
+ >>> pprint(unflatten(flat_dict, splitter=make_splitter(delimiter='_')))
284
+ {'a': '0',
285
+ 'b': {'a': '1.0', 'b': '1.1'},
286
+ 'c': {'a': '2.0', 'b': {'a': '2.1.0', 'b': '2.1.1'}}}
287
+
@@ -0,0 +1,11 @@
1
+ flatten_dict/__init__.py,sha256=f38pB1Z4Q7s5v0jqIq6hVAQIOMbDnADIbd2_WEDW7jw,186
2
+ flatten_dict/flatten_dict.py,sha256=IMGHq2bE1RWTZPqBzIVskOy8PpKUB7jAg20wh7APG30,5513
3
+ flatten_dict/reducer.py,sha256=irHqGQlgcr807TPE_-DsY9286TsDDQ_RKJL4BfwRpI8,278
4
+ flatten_dict/reducers.py,sha256=6_XtTqv9xkFamDmpmQA48gesJFcuBOyAjTOJgCxf3kQ,831
5
+ flatten_dict/splitter.py,sha256=PlEcWhu4eAQIDYoffSHznx3UTdlrV8hHQ9qT5RQnAjA,283
6
+ flatten_dict/splitters.py,sha256=Dt3ufj00m9iV7S7ptEYk76b1SLwvglnxEKHZkzCHW4c,746
7
+ flatten_dict/tests/flatten_dict_test.py,sha256=9KzgRh7c0MZUGvQaYnunNm-PBSKRk7N0A2662nc2iP4,12845
8
+ flatten_dict-0.5.0.dist-info/METADATA,sha256=_VxmnEThCFv8rnHyG6wlbOkfCX77YYa_7tF6kpJthiU,8926
9
+ flatten_dict-0.5.0.dist-info/WHEEL,sha256=Vz2fHgx6HFtSwhs8KvkHLqH5Ea4w1_rner5uNVGCeIE,88
10
+ flatten_dict-0.5.0.dist-info/licenses/LICENSE,sha256=2mzVYhXROtXqbir2WKWhyRQlE9f2JEAeFnkpyqxc_-c,1064
11
+ flatten_dict-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.3.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Ian Lin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.