aldict 1.1.2__py3-none-any.whl → 1.2.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.
- aldict/__init__.py +1 -1
- aldict/alias_dict.py +81 -37
- {aldict-1.1.2.dist-info → aldict-1.2.0.dist-info}/METADATA +17 -4
- aldict-1.2.0.dist-info/RECORD +8 -0
- {aldict-1.1.2.dist-info → aldict-1.2.0.dist-info}/WHEEL +1 -1
- aldict-1.1.2.dist-info/RECORD +0 -8
- {aldict-1.1.2.dist-info → aldict-1.2.0.dist-info}/entry_points.txt +0 -0
- {aldict-1.1.2.dist-info → aldict-1.2.0.dist-info}/licenses/LICENSE +0 -0
aldict/__init__.py
CHANGED
aldict/alias_dict.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from collections import UserDict
|
|
1
|
+
from collections import UserDict
|
|
2
2
|
from collections.abc import Mapping
|
|
3
3
|
from itertools import chain
|
|
4
4
|
|
|
@@ -9,10 +9,13 @@ class AliasDict(UserDict):
|
|
|
9
9
|
"""Dict with key-aliases pointing to shared values."""
|
|
10
10
|
|
|
11
11
|
def __init__(self, dict_=None, /, aliases=None):
|
|
12
|
-
self.
|
|
12
|
+
self._lookup_map = {} # key -> set(aliases)
|
|
13
|
+
self._alias_map = {} # alias -> key
|
|
14
|
+
|
|
13
15
|
if isinstance(dict_, AliasDict):
|
|
14
16
|
super().__init__(dict_.data)
|
|
15
|
-
self.
|
|
17
|
+
self._lookup_map = {k: v.copy() for k, v in dict_._lookup_map.items()}
|
|
18
|
+
self._alias_map = dict_._alias_map.copy()
|
|
16
19
|
else:
|
|
17
20
|
super().__init__(dict_)
|
|
18
21
|
|
|
@@ -20,8 +23,9 @@ class AliasDict(UserDict):
|
|
|
20
23
|
for key, alias_list in aliases.items():
|
|
21
24
|
self.add_alias(key, alias_list)
|
|
22
25
|
|
|
23
|
-
def add_alias(self, key, *aliases):
|
|
24
|
-
"""Add one or more aliases to a key. Accepts *args or a list/tuple.
|
|
26
|
+
def add_alias(self, key, *aliases, strict=False):
|
|
27
|
+
"""Add one or more aliases to a key. Accepts *args or a list/tuple.
|
|
28
|
+
If strict=True, raises AliasValueError when an alias is already assigned to a different key."""
|
|
25
29
|
if key not in self.data:
|
|
26
30
|
raise KeyError(key)
|
|
27
31
|
|
|
@@ -30,16 +34,31 @@ class AliasDict(UserDict):
|
|
|
30
34
|
raise AliasValueError(f"Key and corresponding alias cannot be equal: '{key}'")
|
|
31
35
|
if alias in self.data:
|
|
32
36
|
raise AliasValueError(f"Alias '{alias}' already exists as a key in the dictionary")
|
|
33
|
-
|
|
37
|
+
|
|
38
|
+
if (old_key := self._alias_map.get(alias)) is not None and old_key != key:
|
|
39
|
+
if strict:
|
|
40
|
+
raise AliasValueError(f"Alias '{alias}' already assigned to key '{old_key}'")
|
|
41
|
+
aliases_set = self._lookup_map[old_key]
|
|
42
|
+
aliases_set.discard(alias)
|
|
43
|
+
if not aliases_set:
|
|
44
|
+
del self._lookup_map[old_key]
|
|
45
|
+
|
|
46
|
+
self._lookup_map.setdefault(key, set()).add(alias)
|
|
47
|
+
self._alias_map[alias] = key
|
|
34
48
|
|
|
35
49
|
def remove_alias(self, *aliases):
|
|
36
50
|
"""Remove one or more aliases. Accepts *args or a list/tuple."""
|
|
37
51
|
for alias in self._unpack(aliases):
|
|
38
52
|
try:
|
|
39
|
-
self.
|
|
53
|
+
key = self._alias_map.pop(alias)
|
|
40
54
|
except KeyError as e:
|
|
41
55
|
raise AliasError(f"Alias '{alias}' not found") from e
|
|
42
56
|
|
|
57
|
+
aliases_set = self._lookup_map[key]
|
|
58
|
+
aliases_set.discard(alias)
|
|
59
|
+
if not aliases_set:
|
|
60
|
+
del self._lookup_map[key]
|
|
61
|
+
|
|
43
62
|
@staticmethod
|
|
44
63
|
def _unpack(args):
|
|
45
64
|
return args[0] if len(args) == 1 and isinstance(args[0], (list, tuple)) else args
|
|
@@ -52,30 +71,29 @@ class AliasDict(UserDict):
|
|
|
52
71
|
def clear(self):
|
|
53
72
|
"""Clear all data and aliases."""
|
|
54
73
|
super().clear()
|
|
55
|
-
self.
|
|
74
|
+
self._lookup_map.clear()
|
|
75
|
+
self._alias_map.clear()
|
|
56
76
|
|
|
57
77
|
def clear_aliases(self):
|
|
58
78
|
"""Remove all aliases."""
|
|
59
|
-
self.
|
|
79
|
+
self._lookup_map.clear()
|
|
80
|
+
self._alias_map.clear()
|
|
60
81
|
|
|
61
82
|
def aliases(self):
|
|
62
83
|
"""Return all aliases."""
|
|
63
|
-
return self.
|
|
84
|
+
return self._alias_map.keys()
|
|
64
85
|
|
|
65
86
|
def is_alias(self, key):
|
|
66
87
|
"""Return True if the key is an alias, False otherwise."""
|
|
67
|
-
return key in self.
|
|
88
|
+
return key in self._alias_map
|
|
68
89
|
|
|
69
90
|
def has_aliases(self, key):
|
|
70
91
|
"""Return True if the key has any aliases, False otherwise."""
|
|
71
|
-
return key in self.
|
|
92
|
+
return key in self._lookup_map
|
|
72
93
|
|
|
73
94
|
def keys_with_aliases(self):
|
|
74
95
|
"""Return keys with their aliases."""
|
|
75
|
-
|
|
76
|
-
for alias, key in self._alias_dict.items():
|
|
77
|
-
result[key].append(alias)
|
|
78
|
-
return result.items()
|
|
96
|
+
return self._lookup_map.items()
|
|
79
97
|
|
|
80
98
|
def origin_keys(self):
|
|
81
99
|
"""Return original keys (without aliases)."""
|
|
@@ -83,12 +101,11 @@ class AliasDict(UserDict):
|
|
|
83
101
|
|
|
84
102
|
def origin_key(self, alias):
|
|
85
103
|
"""Return the original key for an alias, or None if not an alias."""
|
|
86
|
-
return self.
|
|
104
|
+
return self._alias_map.get(alias)
|
|
87
105
|
|
|
88
106
|
def keys(self):
|
|
89
107
|
"""Return all keys and aliases."""
|
|
90
|
-
return
|
|
91
|
-
# NB: could be optimized as 'return iter(self)' but we won't be able to call e.g. len(alias_dict.keys())
|
|
108
|
+
return (self.data | self._alias_map).keys()
|
|
92
109
|
|
|
93
110
|
def values(self):
|
|
94
111
|
"""Return all values."""
|
|
@@ -96,47 +113,52 @@ class AliasDict(UserDict):
|
|
|
96
113
|
|
|
97
114
|
def items(self):
|
|
98
115
|
"""Return all items (including alias/value pairs)."""
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
return (self.data | {k: self.data[v] for k, v in self._alias_map.items()}).items()
|
|
117
|
+
|
|
118
|
+
def iterkeys(self):
|
|
119
|
+
"""Return a lazy iterator over all keys and aliases."""
|
|
120
|
+
return iter(self)
|
|
121
|
+
|
|
122
|
+
def iteritems(self):
|
|
123
|
+
"""Return a lazy iterator over all items (including alias/value pairs)."""
|
|
124
|
+
return chain(self.data.items(), ((k, self.data[v]) for k, v in self._alias_map.items()))
|
|
103
125
|
|
|
104
126
|
def origin_len(self):
|
|
105
127
|
"""Return count of original keys (without aliases)."""
|
|
106
128
|
return len(self.data)
|
|
107
129
|
|
|
108
130
|
def __len__(self):
|
|
109
|
-
return len(self.data) + len(self.
|
|
131
|
+
return len(self.data) + len(self._alias_map)
|
|
110
132
|
|
|
111
133
|
def __missing__(self, key):
|
|
112
134
|
try:
|
|
113
|
-
return super().__getitem__(self.
|
|
135
|
+
return super().__getitem__(self._alias_map[key])
|
|
114
136
|
except KeyError:
|
|
115
137
|
raise KeyError(key) from None
|
|
116
138
|
|
|
117
139
|
def __setitem__(self, key, value):
|
|
118
140
|
try:
|
|
119
|
-
key = self.
|
|
141
|
+
key = self._alias_map[key]
|
|
120
142
|
except KeyError:
|
|
121
143
|
pass
|
|
122
144
|
super().__setitem__(key, value)
|
|
123
145
|
|
|
124
146
|
def __delitem__(self, key):
|
|
125
147
|
try:
|
|
126
|
-
self.data
|
|
127
|
-
for alias in
|
|
128
|
-
del self.
|
|
148
|
+
del self.data[key]
|
|
149
|
+
for alias in self._lookup_map.pop(key, ()):
|
|
150
|
+
del self._alias_map[alias]
|
|
129
151
|
except KeyError:
|
|
130
152
|
return self.remove_alias(key)
|
|
131
153
|
|
|
132
154
|
def __contains__(self, item):
|
|
133
|
-
return item in self.data or item in self.
|
|
155
|
+
return item in self.data or item in self._alias_map
|
|
134
156
|
|
|
135
157
|
def __iter__(self):
|
|
136
|
-
return chain(self.data, self.
|
|
158
|
+
return chain(self.data, self._alias_map)
|
|
137
159
|
|
|
138
160
|
def __reversed__(self):
|
|
139
|
-
return chain(reversed(self.
|
|
161
|
+
return chain(reversed(self._alias_map), reversed(self.data))
|
|
140
162
|
|
|
141
163
|
def copy(self):
|
|
142
164
|
"""Return a shallow copy of the AliasDict."""
|
|
@@ -148,7 +170,8 @@ class AliasDict(UserDict):
|
|
|
148
170
|
def __eq__(self, other):
|
|
149
171
|
if not isinstance(other, AliasDict):
|
|
150
172
|
return NotImplemented
|
|
151
|
-
|
|
173
|
+
# _lookup_map is derived from _alias_map, so comparing it is redundant
|
|
174
|
+
return self.data == other.data and self._alias_map == other._alias_map
|
|
152
175
|
|
|
153
176
|
def __or__(self, other):
|
|
154
177
|
if not isinstance(other, Mapping):
|
|
@@ -156,7 +179,10 @@ class AliasDict(UserDict):
|
|
|
156
179
|
new = self.copy()
|
|
157
180
|
if isinstance(other, AliasDict):
|
|
158
181
|
new.update(other.data)
|
|
159
|
-
|
|
182
|
+
self._validate_merge_aliases(new, other)
|
|
183
|
+
new._alias_map.update(other._alias_map)
|
|
184
|
+
for k, v in other._lookup_map.items():
|
|
185
|
+
new._lookup_map.setdefault(k, set()).update(v)
|
|
160
186
|
else:
|
|
161
187
|
new.update(other)
|
|
162
188
|
return new
|
|
@@ -164,17 +190,35 @@ class AliasDict(UserDict):
|
|
|
164
190
|
def __ror__(self, other):
|
|
165
191
|
if not isinstance(other, Mapping):
|
|
166
192
|
return NotImplemented
|
|
167
|
-
new =
|
|
193
|
+
new = type(self)(other)
|
|
168
194
|
new.update(self.data)
|
|
169
|
-
|
|
195
|
+
self._validate_merge_aliases(new, self)
|
|
196
|
+
new._alias_map.update(self._alias_map)
|
|
197
|
+
for k, v in self._lookup_map.items():
|
|
198
|
+
new._lookup_map.setdefault(k, set()).update(v)
|
|
170
199
|
return new
|
|
171
200
|
|
|
172
201
|
def __ior__(self, other):
|
|
173
202
|
if isinstance(other, AliasDict):
|
|
203
|
+
self._validate_merge_aliases(self, other)
|
|
174
204
|
self.update(other.data)
|
|
175
|
-
self.
|
|
205
|
+
self._alias_map.update(other._alias_map)
|
|
206
|
+
for k, v in other._lookup_map.items():
|
|
207
|
+
self._lookup_map.setdefault(k, set()).update(v)
|
|
176
208
|
else:
|
|
177
209
|
self.update(other)
|
|
178
210
|
return self
|
|
179
211
|
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _validate_merge_aliases(target, other):
|
|
214
|
+
"""Check that other's aliases don't collide with target's keys and vice versa."""
|
|
215
|
+
for alias, key in other._alias_map.items(): # noqa
|
|
216
|
+
if alias in target.data:
|
|
217
|
+
raise AliasValueError(f"Alias '{alias}' already exists as a key in the dictionary")
|
|
218
|
+
if (existing := target._alias_map.get(alias)) is not None and existing != key: # noqa
|
|
219
|
+
raise AliasValueError(f"Alias '{alias}' already assigned to key '{existing}'")
|
|
220
|
+
for key in other.data:
|
|
221
|
+
if key in target._alias_map: # noqa
|
|
222
|
+
raise AliasValueError(f"Key '{key}' already exists as an alias in the dictionary")
|
|
223
|
+
|
|
180
224
|
__hash__ = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aldict
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Multi-key dictionary, supports adding and manipulating key-aliases pointing to shared values
|
|
5
5
|
Keywords: multi-key dictionary,multidict,alias-dict
|
|
6
6
|
Author-Email: kaliv0 <kaloyan.ivanov88@gmail.com>
|
|
@@ -99,6 +99,19 @@ ad.items()
|
|
|
99
99
|
# dict_values([10, 20])
|
|
100
100
|
# dict_items([('x', 10), ('y', 20), ('Xx', 10), ('Yy', 20), ('xyz', 20)])
|
|
101
101
|
```
|
|
102
|
+
- iterkeys
|
|
103
|
+
<br>(lazy iterator over all <i>keys</i> and <i>aliases</i>)
|
|
104
|
+
```python
|
|
105
|
+
ad = AliasDict({"x": 10, "y": 20})
|
|
106
|
+
ad.add_alias("x", "Xx")
|
|
107
|
+
|
|
108
|
+
assert list(ad.iterkeys()) == ['x', 'y', 'Xx']
|
|
109
|
+
```
|
|
110
|
+
- iteritems
|
|
111
|
+
<br>(lazy iterator over all <i>items</i> including <i>alias/value</i> pairs)
|
|
112
|
+
```python
|
|
113
|
+
assert list(ad.iteritems()) == [('x', 10), ('y', 20), ('Xx', 10)]
|
|
114
|
+
```
|
|
102
115
|
- remove key and aliases
|
|
103
116
|
```python
|
|
104
117
|
ad.pop("y")
|
|
@@ -152,8 +165,8 @@ assert ad_copy is not ad
|
|
|
152
165
|
```
|
|
153
166
|
- merge with | and |= operators
|
|
154
167
|
```python
|
|
155
|
-
ad1 = AliasDict({"a": 1}, aliases={"a":
|
|
156
|
-
ad2 = AliasDict({"b": 2}, aliases={"b":
|
|
168
|
+
ad1 = AliasDict({"a": 1}, aliases={"a": "aa"})
|
|
169
|
+
ad2 = AliasDict({"b": 2}, aliases={"b": "bb"})
|
|
157
170
|
|
|
158
171
|
merged = ad1 | ad2
|
|
159
172
|
assert merged["aa"] == 1
|
|
@@ -164,6 +177,6 @@ assert ad1["c"] == 3
|
|
|
164
177
|
```
|
|
165
178
|
- fromkeys
|
|
166
179
|
```python
|
|
167
|
-
ad = AliasDict.fromkeys(["a", "b", "c"], 0, aliases={"a":
|
|
180
|
+
ad = AliasDict.fromkeys(["a", "b", "c"], 0, aliases={"a": "aa"})
|
|
168
181
|
assert ad["a"] == ad["aa"] == 0
|
|
169
182
|
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
aldict-1.2.0.dist-info/METADATA,sha256=QNtG7xV9icevXw4BkrpNkcILHOUyu7Ch69NLNRI4B0M,5239
|
|
2
|
+
aldict-1.2.0.dist-info/WHEEL,sha256=Wb0ASbVj8JvWHpOiIpPi7ucfIgJeCi__PzivviEAQFc,90
|
|
3
|
+
aldict-1.2.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
aldict-1.2.0.dist-info/licenses/LICENSE,sha256=frOVyHZrx5o-fh5xC-kggT3MaLdp6yxV_YGpVXFHFSQ,1071
|
|
5
|
+
aldict/__init__.py,sha256=Fog4LzmMPlKzmFAuJ7YpYVSmSemClWfs_gA3qjWP_q8,233
|
|
6
|
+
aldict/alias_dict.py,sha256=JOC8-z08zhfEOSHyVoWg4dq1gX1MFs-bjFUBmeu7ydo,8012
|
|
7
|
+
aldict/exception.py,sha256=rFNmv9HuUOn2toPVYpVPQq6SOsl8nvwtJpGhPkK1puQ,83
|
|
8
|
+
aldict-1.2.0.dist-info/RECORD,,
|
aldict-1.1.2.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
aldict-1.1.2.dist-info/METADATA,sha256=XUOyCZAmI-ujmNr5ng5muf2-W_YFIpXek_8YbUyCT6g,4884
|
|
2
|
-
aldict-1.1.2.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
-
aldict-1.1.2.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
-
aldict-1.1.2.dist-info/licenses/LICENSE,sha256=frOVyHZrx5o-fh5xC-kggT3MaLdp6yxV_YGpVXFHFSQ,1071
|
|
5
|
-
aldict/__init__.py,sha256=6dHZU-HPKpqt6z5bX4-zWA0Vy7Wm2emhnmO4aEnaRXk,233
|
|
6
|
-
aldict/alias_dict.py,sha256=O4EGRp-VtW1Mb-Fr1-_VDkPof6G9UBYYhx5l2I8FKck,5934
|
|
7
|
-
aldict/exception.py,sha256=rFNmv9HuUOn2toPVYpVPQq6SOsl8nvwtJpGhPkK1puQ,83
|
|
8
|
-
aldict-1.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|