nested-mapping 0.12.0__tar.gz → 0.13.0__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.
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/CHANGELOG.md +9 -0
- {nested_mapping-0.12.0/src/nested_mapping.egg-info → nested_mapping-0.13.0}/PKG-INFO +1 -1
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/__version__.py +3 -3
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/tools/map.py +54 -20
- nested_mapping-0.13.0/src/nested_mapping/tools/match.py +282 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0/src/nested_mapping.egg-info}/PKG-INFO +1 -1
- nested_mapping-0.13.0/src/nested_mapping.egg-info/scm_version.json +8 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/tests/test_map.py +45 -0
- nested_mapping-0.13.0/tests/test_match.py +908 -0
- nested_mapping-0.12.0/src/nested_mapping/tools/match.py +0 -128
- nested_mapping-0.12.0/src/nested_mapping.egg-info/scm_version.json +0 -8
- nested_mapping-0.12.0/tests/test_match.py +0 -73
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/.flake8 +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/.gitignore +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/.gitlab-ci.yml +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/CONTRIBUTING.md +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/LICENSE +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/README.md +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/conftest.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/pyproject.toml +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/pytest.ini +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/setup.cfg +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/__init__.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/class_wrapper.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/ipython.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/nested_mapping.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/nested_mapping_access.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/tools/__init__.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/tools/filter.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/tools/reorder_key.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/typing.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping/visitor.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping.egg-info/SOURCES.txt +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping.egg-info/dependency_links.txt +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping.egg-info/requires.txt +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping.egg-info/scm_file_list.json +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/src/nested_mapping.egg-info/top_level.txt +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/tests/test_filter.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/tests/test_nested_mapping.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/tests/test_nested_mapping_update.py +0 -0
- {nested_mapping-0.12.0 → nested_mapping-0.13.0}/tests/test_visitor.py +0 -0
|
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.13.0] - 2026-11-03
|
|
9
|
+
|
|
10
|
+
- feature: enable `remap_keys` and `make_reorder_fcn` to insert extra levels of nesting.
|
|
11
|
+
Should be enabled explicitly via `allow_insert_items`.
|
|
12
|
+
|
|
13
|
+
## [0.12.1] - 2026-10-29
|
|
14
|
+
|
|
15
|
+
- feature: extra protection for `match_keys` to avoid keys being skipped. Can be disabled.
|
|
16
|
+
|
|
8
17
|
## [0.12.0] - 2026-10-01
|
|
9
18
|
|
|
10
19
|
- add `skip_fcn` argument for `walk*` methods to skip items from iteration.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.13.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 13, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g4a18ec946'
|
|
@@ -52,11 +52,12 @@ def remap_items(
|
|
|
52
52
|
| tuple[tuple[str, ...], tuple[str, ...]]
|
|
53
53
|
| list[int]
|
|
54
54
|
| list[list[str]]
|
|
55
|
-
| Mapping[str, list[str] | tuple[str]]
|
|
55
|
+
| Mapping[str, list[str] | tuple[str, ...]]
|
|
56
56
|
| None
|
|
57
57
|
) = None,
|
|
58
58
|
skip_indices_source: Sequence[KeyLike | set] | None = None,
|
|
59
59
|
skip_indices_target: Sequence[KeyLike | set] | None = None,
|
|
60
|
+
allow_insert_items: bool = False,
|
|
60
61
|
fcn: Callable[[Any], Any] | None = None,
|
|
61
62
|
verbose: bool = False,
|
|
62
63
|
) -> NestedMapping:
|
|
@@ -67,7 +68,7 @@ def remap_items(
|
|
|
67
68
|
|
|
68
69
|
skip_source = _make_skip_fcn(skip_indices_source)
|
|
69
70
|
skip_target = _make_skip_fcn(skip_indices_target)
|
|
70
|
-
reorder = make_reorder_function(reorder_indices)
|
|
71
|
+
reorder = make_reorder_function(reorder_indices, allow_insert_items=allow_insert_items)
|
|
71
72
|
|
|
72
73
|
if fcn is None:
|
|
73
74
|
fcn = lambda o: o
|
|
@@ -79,15 +80,11 @@ def remap_items(
|
|
|
79
80
|
for key, value in source.walkitems():
|
|
80
81
|
if skip_source(key):
|
|
81
82
|
continue
|
|
82
|
-
for newkey in product(
|
|
83
|
-
*tuple(rename_indices.get(skey, (skey,)) for skey in key)
|
|
84
|
-
):
|
|
83
|
+
for newkey in product(*tuple(rename_indices.get(skey, (skey,)) for skey in key)):
|
|
85
84
|
newkey_ordered = reorder(newkey)
|
|
86
85
|
if skip_target(newkey_ordered):
|
|
87
86
|
if verbose:
|
|
88
|
-
print(
|
|
89
|
-
f"remap: skip {'.'.join(key)} → {'.'.join(newkey_ordered)}"
|
|
90
|
-
)
|
|
87
|
+
print(f"remap: skip {'.'.join(key)} → {'.'.join(newkey_ordered)}")
|
|
91
88
|
continue
|
|
92
89
|
if verbose:
|
|
93
90
|
print(
|
|
@@ -96,6 +93,9 @@ def remap_items(
|
|
|
96
93
|
target[newkey_ordered] = fcn(value)
|
|
97
94
|
else:
|
|
98
95
|
for key, value in source.walkitems():
|
|
96
|
+
if skip_source(key):
|
|
97
|
+
print(f"remap: skip {'.'.join(key)}")
|
|
98
|
+
continue
|
|
99
99
|
newkey_ordered = reorder(key)
|
|
100
100
|
if skip_target(newkey_ordered):
|
|
101
101
|
if verbose:
|
|
@@ -128,39 +128,73 @@ def make_reorder_function(
|
|
|
128
128
|
| tuple[tuple[str, ...], tuple[str, ...]]
|
|
129
129
|
| Sequence[int]
|
|
130
130
|
| Sequence[Sequence[str]]
|
|
131
|
-
| Mapping[str, list[str] | tuple[str]]
|
|
131
|
+
| Mapping[str, list[str] | tuple[str, ...]]
|
|
132
132
|
| None
|
|
133
133
|
),
|
|
134
134
|
*,
|
|
135
135
|
allow_skip_items: bool = False,
|
|
136
|
+
allow_insert_items: bool = False,
|
|
136
137
|
) -> Callable:
|
|
138
|
+
order_from_extended: list[None | str] = []
|
|
137
139
|
match reorder_indices:
|
|
138
140
|
case [int(), *_]:
|
|
139
|
-
index_order = reorder_indices
|
|
141
|
+
index_order = list(reorder_indices)
|
|
140
142
|
len_from = len(index_order)
|
|
143
|
+
|
|
144
|
+
match_length_from = not allow_skip_items
|
|
145
|
+
|
|
146
|
+
if allow_insert_items:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"make_reorder_function: inserting items is forbidden with tuple[int] arguments"
|
|
149
|
+
)
|
|
141
150
|
case [[str(), *_] as order_from, [str(), *_] as order_to] | {
|
|
142
151
|
"from": [str(), *_] as order_from,
|
|
143
152
|
"to": [str(), *_] as order_to,
|
|
144
153
|
}:
|
|
154
|
+
match_length_from = True
|
|
155
|
+
|
|
145
156
|
len_from = len(order_from)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
if allow_insert_items:
|
|
158
|
+
order_from_extended: list[None | str] = [None] * len(order_from)
|
|
159
|
+
index_order: list[None | int] = [None] * len(order_to)
|
|
160
|
+
not_processed_indices = set(range(len(order_from)))
|
|
161
|
+
for i, name in enumerate(order_to):
|
|
162
|
+
try:
|
|
163
|
+
index = order_from.index(name)
|
|
164
|
+
not_processed_indices.remove(index)
|
|
165
|
+
except ValueError:
|
|
166
|
+
index = len(order_from_extended)
|
|
167
|
+
order_from_extended.append(name)
|
|
168
|
+
index_order[i] = index
|
|
169
|
+
|
|
170
|
+
if not_processed_indices and not allow_skip_items:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Inconsistent order definitions (can not skip) {order_from}→{order_to}"
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
try:
|
|
176
|
+
index_order = list(order_from.index(item) for item in order_to)
|
|
177
|
+
except ValueError:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Inconsistent order definitions (can neigher skip no insert) {order_from}→{order_to}"
|
|
180
|
+
)
|
|
181
|
+
|
|
152
182
|
case None:
|
|
153
183
|
len_from = None
|
|
184
|
+
match_length_from = False
|
|
154
185
|
allow_skip_items = True
|
|
155
186
|
return lambda key: key
|
|
156
187
|
case _:
|
|
157
188
|
raise ValueError(f"Invalid order specification: {reorder_indices}")
|
|
158
189
|
|
|
159
190
|
def reorder_indices_fcn(key: Sequence):
|
|
160
|
-
if
|
|
161
|
-
raise ValueError(
|
|
162
|
-
|
|
191
|
+
if match_length_from and len(key) != len_from:
|
|
192
|
+
raise ValueError(f"inconsistent key length: {len(key)} vs required {len_from}")
|
|
193
|
+
if allow_insert_items:
|
|
194
|
+
return key.__class__(
|
|
195
|
+
key[idx] if idx < len(key) else order_from_extended[idx] for idx in index_order
|
|
163
196
|
)
|
|
164
|
-
|
|
197
|
+
else:
|
|
198
|
+
return key.__class__(key[idx] for idx in index_order)
|
|
165
199
|
|
|
166
200
|
return reorder_indices_fcn
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from typing import Any, TYPE_CHECKING, TypeVar
|
|
5
|
+
|
|
6
|
+
from ordered_set import OrderedSet
|
|
7
|
+
|
|
8
|
+
from ..typing import Key, KeyLike, TupleKey, properkey, setkey
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
12
|
+
|
|
13
|
+
SetType = TypeVar("SetType", set[str], OrderedSet[str])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_keys_consistent_ordered(
|
|
17
|
+
set_key_left: OrderedSet[str], set_key_right: OrderedSet[str]
|
|
18
|
+
) -> bool:
|
|
19
|
+
return set_key_left == set_key_right
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def set_keys_consistent_unordered(set_key_left: SetType, set_key_right: SetType) -> bool:
|
|
23
|
+
return not set_key_left.symmetric_difference(set_key_right)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_keys_consistent_symmetric(set_key_left: SetType, set_key_right: SetType) -> bool:
|
|
27
|
+
return set_key_left.issubset(set_key_right) or set_key_right.issubset(set_key_left)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set_keys_consistent_left_in_right(set_key_left: SetType, set_key_right: SetType) -> bool:
|
|
31
|
+
return set_key_left.issubset(set_key_right)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def set_keys_consistent_right_in_left(set_key_left: SetType, set_key_right: SetType) -> bool:
|
|
35
|
+
return set_key_right.issubset(set_key_left)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def match_keys_to_key(
|
|
39
|
+
keys_left: Sequence[KeyLike],
|
|
40
|
+
key_right_tuple: TupleKey,
|
|
41
|
+
key_right_set: SetType,
|
|
42
|
+
keys_consistent_fcn: Callable[[SetType, SetType], bool],
|
|
43
|
+
fcn: Callable[[TupleKey, TupleKey], Any],
|
|
44
|
+
*,
|
|
45
|
+
fcn_on_reject: Callable[[TupleKey, TupleKey], Any] | None = None,
|
|
46
|
+
collect_skipped_right_keys: bool = False,
|
|
47
|
+
collect_skipped_left_keys: bool = False,
|
|
48
|
+
skipped_left_keys: set | None = None,
|
|
49
|
+
processed_left_keys: set | None = None,
|
|
50
|
+
processed_right_keys: set | None = None,
|
|
51
|
+
skipped_right_keys: set | None = None,
|
|
52
|
+
require_no_duplicate_right_matches: bool = False,
|
|
53
|
+
) -> bool:
|
|
54
|
+
assert not collect_skipped_right_keys or (
|
|
55
|
+
processed_right_keys is not None and skipped_right_keys is not None
|
|
56
|
+
)
|
|
57
|
+
assert not collect_skipped_left_keys or (
|
|
58
|
+
processed_left_keys is not None and skipped_left_keys is not None
|
|
59
|
+
)
|
|
60
|
+
right_key_processed = False
|
|
61
|
+
key_left_match = None
|
|
62
|
+
for key_left in keys_left:
|
|
63
|
+
key_left_tuple = properkey(key_left)
|
|
64
|
+
key_left_set = OrderedSet(key_left_tuple)
|
|
65
|
+
|
|
66
|
+
if not keys_consistent_fcn(key_left_set, key_right_set): # pyright: ignore [reportArgumentType]
|
|
67
|
+
if (
|
|
68
|
+
collect_skipped_left_keys
|
|
69
|
+
and key_left_tuple
|
|
70
|
+
not in processed_left_keys # pyright: ignore [reportOperatorIssue]
|
|
71
|
+
):
|
|
72
|
+
skipped_left_keys.add( # pyright: ignore [reportOptionalMemberAccess]
|
|
73
|
+
key_left_tuple
|
|
74
|
+
)
|
|
75
|
+
if (
|
|
76
|
+
collect_skipped_right_keys
|
|
77
|
+
and key_right_tuple
|
|
78
|
+
not in processed_right_keys # pyright: ignore [reportOperatorIssue]
|
|
79
|
+
):
|
|
80
|
+
skipped_right_keys.add( # pyright: ignore [reportOptionalMemberAccess]
|
|
81
|
+
key_right_tuple
|
|
82
|
+
)
|
|
83
|
+
if fcn_on_reject:
|
|
84
|
+
fcn_on_reject(key_left_tuple, key_right_tuple)
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
fcn(key_left_tuple, key_right_tuple)
|
|
88
|
+
if require_no_duplicate_right_matches and right_key_processed:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"match_keys: right key {key_right_tuple!s} was matched more than once to "
|
|
91
|
+
f"{key_left_match!s}; {key_left_tuple!s}..."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
right_key_processed = True
|
|
95
|
+
key_left_match = key_left_tuple
|
|
96
|
+
|
|
97
|
+
if collect_skipped_left_keys:
|
|
98
|
+
with suppress(KeyError):
|
|
99
|
+
skipped_left_keys.remove(
|
|
100
|
+
key_left_tuple
|
|
101
|
+
) # pyright: ignore [reportOptionalMemberAccess]
|
|
102
|
+
processed_left_keys.add(key_left_tuple) # pyright: ignore [reportOptionalMemberAccess]
|
|
103
|
+
|
|
104
|
+
if collect_skipped_right_keys:
|
|
105
|
+
with suppress(KeyError):
|
|
106
|
+
skipped_right_keys.remove(
|
|
107
|
+
key_right_tuple
|
|
108
|
+
) # pyright: ignore [reportOptionalMemberAccess]
|
|
109
|
+
processed_right_keys.add(
|
|
110
|
+
key_right_tuple
|
|
111
|
+
) # pyright: ignore [reportOptionalMemberAccess]
|
|
112
|
+
|
|
113
|
+
return right_key_processed
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def zip_match_keys_to_key(
|
|
117
|
+
keys_left: Sequence[KeyLike],
|
|
118
|
+
key_right_tuple: TupleKey,
|
|
119
|
+
key_right_set: SetType,
|
|
120
|
+
keys_consistent_fcn: Callable[[SetType, SetType], bool],
|
|
121
|
+
):
|
|
122
|
+
for key_left in keys_left:
|
|
123
|
+
key_left_tuple = properkey(key_left)
|
|
124
|
+
key_left_set = OrderedSet(key_left_tuple)
|
|
125
|
+
if keys_consistent_fcn(key_left_set, key_right_set): # pyright: ignore [reportArgumentType]
|
|
126
|
+
yield key_left_tuple, key_right_tuple
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def zip_match_keys_to_keys(
|
|
130
|
+
keys_left: Sequence[KeyLike],
|
|
131
|
+
keys_right: Sequence[KeyLike],
|
|
132
|
+
keys_consistent_fcn: Callable[[SetType, SetType], bool],
|
|
133
|
+
):
|
|
134
|
+
for key_right in keys_right:
|
|
135
|
+
key_right_tuple = properkey(key_right)
|
|
136
|
+
key_right_set = OrderedSet(key_right_tuple)
|
|
137
|
+
|
|
138
|
+
yield from zip_match_keys_to_key(
|
|
139
|
+
keys_left, key_right_tuple, key_right_set, keys_consistent_fcn # pyright: ignore [reportArgumentType]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def match_keys(
|
|
144
|
+
keys_left_seq: Sequence[Sequence[KeyLike]],
|
|
145
|
+
keys_right: Iterable[KeyLike],
|
|
146
|
+
fcn: Callable[[int, TupleKey, TupleKey], Any],
|
|
147
|
+
*,
|
|
148
|
+
fcn_outer_before: Callable[[TupleKey], Any] | None = None,
|
|
149
|
+
fcn_outer_after: Callable[[TupleKey], Any] | None = None,
|
|
150
|
+
fcn_on_reject: Callable[[int, TupleKey, TupleKey], Any] | None = None,
|
|
151
|
+
left_in_right: bool = True,
|
|
152
|
+
right_in_left: bool = True,
|
|
153
|
+
require_all_right_keys_processed: bool = True,
|
|
154
|
+
require_all_left_keys_processed: bool = True,
|
|
155
|
+
require_all_left_keys_match_right: bool = True,
|
|
156
|
+
require_no_duplicate_right_matches: bool = False,
|
|
157
|
+
unmatched_right_keys_should_contain: Sequence[KeyLike] | None = None,
|
|
158
|
+
skippable_left_keys_should_contain: Sequence[KeyLike] | None = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
if isinstance(keys_right, str):
|
|
161
|
+
raise RuntimeError("`keys_right` should be iterable, but not string")
|
|
162
|
+
if not keys_left_seq:
|
|
163
|
+
raise RuntimeError("Left sequence is empty")
|
|
164
|
+
|
|
165
|
+
if left_in_right and right_in_left:
|
|
166
|
+
keys_consistent_fcn = set_keys_consistent_symmetric
|
|
167
|
+
elif left_in_right:
|
|
168
|
+
keys_consistent_fcn = set_keys_consistent_left_in_right
|
|
169
|
+
elif right_in_left:
|
|
170
|
+
keys_consistent_fcn = set_keys_consistent_right_in_left
|
|
171
|
+
else:
|
|
172
|
+
raise RuntimeError(
|
|
173
|
+
"At least on of the flags should be true: right_in_left or left_in_right"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
skipped_right_keys = []
|
|
177
|
+
processed_right_keys = set()
|
|
178
|
+
processed_left_keys = set()
|
|
179
|
+
skipped_left_keys = set()
|
|
180
|
+
skipped_right_keys_for_left = []
|
|
181
|
+
processed_right_keys_for_left = []
|
|
182
|
+
|
|
183
|
+
collect_skipped_left_keys = bool(
|
|
184
|
+
require_all_left_keys_processed or skippable_left_keys_should_contain is not None
|
|
185
|
+
)
|
|
186
|
+
collect_skipped_right_keys = bool(
|
|
187
|
+
require_all_left_keys_match_right or unmatched_right_keys_should_contain is not None
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
i_left = -1
|
|
191
|
+
for key_right in keys_right:
|
|
192
|
+
key_right_tuple = properkey(key_right)
|
|
193
|
+
key_right_set = OrderedSet(key_right_tuple)
|
|
194
|
+
if fcn_outer_before is not None:
|
|
195
|
+
fcn_outer_before(key_right_tuple)
|
|
196
|
+
|
|
197
|
+
right_key_processed = False
|
|
198
|
+
for i_left, keys_left in enumerate(keys_left_seq):
|
|
199
|
+
if i_left >= len(skipped_right_keys_for_left):
|
|
200
|
+
processed_right_keys_for_left.append(processed_right_keys_i := set())
|
|
201
|
+
skipped_right_keys_for_left.append(skipped_right_keys_i := set())
|
|
202
|
+
else:
|
|
203
|
+
processed_right_keys_i = processed_right_keys_for_left[i_left]
|
|
204
|
+
skipped_right_keys_i = skipped_right_keys_for_left[i_left]
|
|
205
|
+
|
|
206
|
+
right_key_processed = match_keys_to_key(
|
|
207
|
+
keys_left,
|
|
208
|
+
key_right_tuple,
|
|
209
|
+
key_right_set,
|
|
210
|
+
keys_consistent_fcn,
|
|
211
|
+
lambda *args: fcn(i_left, *args),
|
|
212
|
+
fcn_on_reject=(
|
|
213
|
+
(lambda *args: fcn_on_reject(i_left, *args))
|
|
214
|
+
if fcn_on_reject is not None
|
|
215
|
+
else None
|
|
216
|
+
),
|
|
217
|
+
collect_skipped_left_keys=collect_skipped_left_keys,
|
|
218
|
+
collect_skipped_right_keys=collect_skipped_right_keys,
|
|
219
|
+
skipped_left_keys=skipped_left_keys,
|
|
220
|
+
processed_left_keys=processed_left_keys,
|
|
221
|
+
skipped_right_keys=skipped_right_keys_i,
|
|
222
|
+
processed_right_keys=processed_right_keys_i,
|
|
223
|
+
require_no_duplicate_right_matches=require_no_duplicate_right_matches,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if fcn_outer_after is not None:
|
|
227
|
+
fcn_outer_after(key_right_tuple)
|
|
228
|
+
|
|
229
|
+
if right_key_processed:
|
|
230
|
+
if require_all_left_keys_match_right:
|
|
231
|
+
processed_right_keys.add(key_right_tuple)
|
|
232
|
+
elif require_all_right_keys_processed:
|
|
233
|
+
skipped_right_keys.append(key_right_tuple)
|
|
234
|
+
|
|
235
|
+
for i, (skipped_left_keys_i, processed_left_keys_i) in enumerate(
|
|
236
|
+
zip(skipped_right_keys_for_left, processed_right_keys_for_left)
|
|
237
|
+
):
|
|
238
|
+
if processed_left_keys_i != processed_right_keys:
|
|
239
|
+
if require_all_left_keys_match_right:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"match_keys: left sequence {i} did not provide keys {skipped_left_keys_i!s}"
|
|
242
|
+
)
|
|
243
|
+
_check_for_unexpectedly_skipped_keys(
|
|
244
|
+
skipped_left_keys_i,
|
|
245
|
+
unmatched_right_keys_should_contain,
|
|
246
|
+
kind=f"missing left ({i})",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if skipped_left_keys:
|
|
250
|
+
if require_all_left_keys_processed:
|
|
251
|
+
raise ValueError(f"match_keys: there were unprocessed left keys {skipped_left_keys!s}")
|
|
252
|
+
_check_for_unexpectedly_skipped_keys(
|
|
253
|
+
skipped_left_keys, unmatched_right_keys_should_contain, kind="skipped left"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if require_all_right_keys_processed and skipped_right_keys:
|
|
257
|
+
raise ValueError(f"match_keys: there were unprocessed right keys {skipped_right_keys!s}")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _check_for_unexpectedly_skipped_keys(
|
|
261
|
+
skipped_keys: Sequence[Key] | set[Key], should_contain: Sequence[KeyLike] | None, kind: str
|
|
262
|
+
):
|
|
263
|
+
if unexpectedly_skipped_keys := _get_unexpectedly_skipped_keys(skipped_keys, should_contain):
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f"match_keys: there were {kind} keys {skipped_keys!s}. "
|
|
266
|
+
f"{unexpectedly_skipped_keys} should not have been skipped."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _get_unexpectedly_skipped_keys(
|
|
271
|
+
skipped_keys: Sequence[Key] | set[Key], should_contain: Sequence[KeyLike] | None
|
|
272
|
+
) -> Sequence[Key]:
|
|
273
|
+
if not should_contain:
|
|
274
|
+
return []
|
|
275
|
+
|
|
276
|
+
setkeys = tuple(setkey(keypart) for keypart in should_contain)
|
|
277
|
+
incorrectly_skipped_keys = []
|
|
278
|
+
for skipped_key in skipped_keys:
|
|
279
|
+
if not any(setkey.issubset(skipped_key) for setkey in setkeys):
|
|
280
|
+
incorrectly_skipped_keys.append(skipped_key)
|
|
281
|
+
|
|
282
|
+
return incorrectly_skipped_keys
|
|
@@ -17,6 +17,7 @@ def test_remap_items_01():
|
|
|
17
17
|
d_renamed_reordered_skipped_source_match = NestedMapping()
|
|
18
18
|
d_renamed_reordered_skipped_target_match = NestedMapping()
|
|
19
19
|
d_renamed_reordered_skipped_both_match = NestedMapping()
|
|
20
|
+
d_renamed_reordered_skipped_both_extra_match = NestedMapping()
|
|
20
21
|
|
|
21
22
|
rename_indices = {
|
|
22
23
|
"z": ("z1", "z2"),
|
|
@@ -24,6 +25,8 @@ def test_remap_items_01():
|
|
|
24
25
|
}
|
|
25
26
|
reorder_indices = [2, 1, 0]
|
|
26
27
|
reorder_indices_names = (("a", "b", "c"), ("c", "b", "a"))
|
|
28
|
+
reorder_indices_names_extra = (("a", "b", "c"), ("c", "extra", "b", "a"))
|
|
29
|
+
reorder_indices_dict = {"from": ("a", "b", "c"), "to": ("c", "b", "a")}
|
|
27
30
|
skip_indices_source = (("1", "z"), ("b",))
|
|
28
31
|
skip_indices_target = (("1", "z2"), ("a",))
|
|
29
32
|
|
|
@@ -47,6 +50,9 @@ def test_remap_items_01():
|
|
|
47
50
|
d_renamed_reordered_skipped_both_match["z1", i2new, i1] = c
|
|
48
51
|
d_renamed_reordered_skipped_both_match["z2", i2new, i1] = c
|
|
49
52
|
|
|
53
|
+
d_renamed_reordered_skipped_both_extra_match["z1", "extra", i2new, i1] = c
|
|
54
|
+
d_renamed_reordered_skipped_both_extra_match["z2", "extra", i2new, i1] = c
|
|
55
|
+
|
|
50
56
|
d_renamed_reordered_skipped_source_match["z1", i2new, i1] = c
|
|
51
57
|
d_renamed_reordered_skipped_source_match["z2", i2new, i1] = c
|
|
52
58
|
|
|
@@ -63,6 +69,7 @@ def test_remap_items_01():
|
|
|
63
69
|
d_renamed_reordered_skipped_source_match[i3, i2new, i1] = c
|
|
64
70
|
if i2new != "a":
|
|
65
71
|
d_renamed_reordered_skipped_both_match[i3, i2new, i1] = c
|
|
72
|
+
d_renamed_reordered_skipped_both_extra_match[i3, "extra", i2new, i1] = c
|
|
66
73
|
|
|
67
74
|
d_new = remap_items(d_main, reorder_indices=reorder_indices)
|
|
68
75
|
assert d_reordered_match == d_new
|
|
@@ -73,6 +80,9 @@ def test_remap_items_01():
|
|
|
73
80
|
d_new = remap_items(d_main, reorder_indices=reorder_indices_names)
|
|
74
81
|
assert d_reordered_match == d_new
|
|
75
82
|
|
|
83
|
+
d_new = remap_items(d_main, reorder_indices=reorder_indices_dict)
|
|
84
|
+
assert d_reordered_match == d_new
|
|
85
|
+
|
|
76
86
|
d_new = remap_items(
|
|
77
87
|
d_main,
|
|
78
88
|
reorder_indices=reorder_indices,
|
|
@@ -122,6 +132,25 @@ def test_remap_items_01():
|
|
|
122
132
|
skip_indices_source=skip_indices_source,
|
|
123
133
|
skip_indices_target=skip_indices_target,
|
|
124
134
|
)
|
|
135
|
+
assert d_renamed_reordered_skipped_both_match == d_new
|
|
136
|
+
|
|
137
|
+
with raises(ValueError):
|
|
138
|
+
d_new = remap_items(
|
|
139
|
+
d_main,
|
|
140
|
+
rename_indices=rename_indices,
|
|
141
|
+
reorder_indices=reorder_indices_names_extra,
|
|
142
|
+
skip_indices_source=skip_indices_source,
|
|
143
|
+
skip_indices_target=skip_indices_target,
|
|
144
|
+
)
|
|
145
|
+
d_new = remap_items(
|
|
146
|
+
d_main,
|
|
147
|
+
rename_indices=rename_indices,
|
|
148
|
+
reorder_indices=reorder_indices_names_extra,
|
|
149
|
+
skip_indices_source=skip_indices_source,
|
|
150
|
+
skip_indices_target=skip_indices_target,
|
|
151
|
+
allow_insert_items=True,
|
|
152
|
+
)
|
|
153
|
+
assert d_renamed_reordered_skipped_both_extra_match == d_new
|
|
125
154
|
|
|
126
155
|
with raises(ValueError):
|
|
127
156
|
remap_items(d_main, reorder_indices=[["a", "b", "c"], ["c", "a", "B"]])
|
|
@@ -227,3 +256,19 @@ def test_reorder_function():
|
|
|
227
256
|
|
|
228
257
|
with raises(ValueError):
|
|
229
258
|
make_reorder_function([list("abc"), list("cad")])
|
|
259
|
+
|
|
260
|
+
with raises(ValueError):
|
|
261
|
+
fcn6 = make_reorder_function([list("abc"), list("cdab")])
|
|
262
|
+
|
|
263
|
+
fcn6 = make_reorder_function([list("abc"), list("cdab")], allow_insert_items=True)
|
|
264
|
+
data6 = [3, "d", 1, 2]
|
|
265
|
+
assert data6 == fcn6(data_from)
|
|
266
|
+
|
|
267
|
+
with raises(ValueError):
|
|
268
|
+
fcn6 = make_reorder_function([list("abc"), list("cdb")], allow_insert_items=True)
|
|
269
|
+
|
|
270
|
+
fcn6 = make_reorder_function(
|
|
271
|
+
[list("abc"), list("cdb")], allow_insert_items=True, allow_skip_items=True
|
|
272
|
+
)
|
|
273
|
+
data6 = [3, "d", 2]
|
|
274
|
+
assert data6 == fcn6(data_from)
|