aldict 1.1.2__py3-none-any.whl → 1.2.1__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 +6 -4
- aldict/alias_dict.py +92 -38
- {aldict-1.1.2.dist-info → aldict-1.2.1.dist-info}/METADATA +18 -5
- aldict-1.2.1.dist-info/RECORD +7 -0
- {aldict-1.1.2.dist-info → aldict-1.2.1.dist-info}/WHEEL +1 -1
- aldict/exception.py +0 -6
- aldict-1.1.2.dist-info/RECORD +0 -8
- {aldict-1.1.2.dist-info → aldict-1.2.1.dist-info}/entry_points.txt +0 -0
- {aldict-1.1.2.dist-info → aldict-1.2.1.dist-info}/licenses/LICENSE +0 -0
aldict/__init__.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
from .alias_dict import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from .alias_dict import (
|
|
2
|
+
AliasDict as AliasDict,
|
|
3
|
+
AliasError as AliasError,
|
|
4
|
+
AliasValueError as AliasValueError,
|
|
5
|
+
)
|
|
4
6
|
|
|
5
|
-
__version__ = "1.
|
|
7
|
+
__version__ = "1.2.1"
|
|
6
8
|
__all__ = ["AliasDict", "AliasError", "AliasValueError"]
|
aldict/alias_dict.py
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
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
|
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
class AliasError(KeyError):
|
|
7
|
+
"""Key alias not found"""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AliasValueError(ValueError):
|
|
13
|
+
"""Inappropriate alias value"""
|
|
14
|
+
|
|
15
|
+
pass
|
|
6
16
|
|
|
7
17
|
|
|
8
18
|
class AliasDict(UserDict):
|
|
9
19
|
"""Dict with key-aliases pointing to shared values."""
|
|
10
20
|
|
|
11
21
|
def __init__(self, dict_=None, /, aliases=None):
|
|
12
|
-
self.
|
|
22
|
+
self._lookup_map = {} # key -> set(aliases)
|
|
23
|
+
self._alias_map = {} # alias -> key
|
|
24
|
+
|
|
13
25
|
if isinstance(dict_, AliasDict):
|
|
14
26
|
super().__init__(dict_.data)
|
|
15
|
-
self.
|
|
27
|
+
self._lookup_map = {k: v.copy() for k, v in dict_._lookup_map.items()}
|
|
28
|
+
self._alias_map = dict_._alias_map.copy()
|
|
16
29
|
else:
|
|
17
30
|
super().__init__(dict_)
|
|
18
31
|
|
|
@@ -20,8 +33,9 @@ class AliasDict(UserDict):
|
|
|
20
33
|
for key, alias_list in aliases.items():
|
|
21
34
|
self.add_alias(key, alias_list)
|
|
22
35
|
|
|
23
|
-
def add_alias(self, key, *aliases):
|
|
24
|
-
"""Add one or more aliases to a key. Accepts *args or a list/tuple.
|
|
36
|
+
def add_alias(self, key, *aliases, strict=False):
|
|
37
|
+
"""Add one or more aliases to a key. Accepts *args or a list/tuple.
|
|
38
|
+
If strict=True, raises AliasValueError when an alias is already assigned to a different key."""
|
|
25
39
|
if key not in self.data:
|
|
26
40
|
raise KeyError(key)
|
|
27
41
|
|
|
@@ -30,16 +44,31 @@ class AliasDict(UserDict):
|
|
|
30
44
|
raise AliasValueError(f"Key and corresponding alias cannot be equal: '{key}'")
|
|
31
45
|
if alias in self.data:
|
|
32
46
|
raise AliasValueError(f"Alias '{alias}' already exists as a key in the dictionary")
|
|
33
|
-
|
|
47
|
+
|
|
48
|
+
if (old_key := self._alias_map.get(alias)) is not None and old_key != key:
|
|
49
|
+
if strict:
|
|
50
|
+
raise AliasValueError(f"Alias '{alias}' already assigned to key '{old_key}'")
|
|
51
|
+
aliases_set = self._lookup_map[old_key]
|
|
52
|
+
aliases_set.discard(alias)
|
|
53
|
+
if not aliases_set:
|
|
54
|
+
del self._lookup_map[old_key]
|
|
55
|
+
|
|
56
|
+
self._lookup_map.setdefault(key, set()).add(alias)
|
|
57
|
+
self._alias_map[alias] = key
|
|
34
58
|
|
|
35
59
|
def remove_alias(self, *aliases):
|
|
36
60
|
"""Remove one or more aliases. Accepts *args or a list/tuple."""
|
|
37
61
|
for alias in self._unpack(aliases):
|
|
38
62
|
try:
|
|
39
|
-
self.
|
|
63
|
+
key = self._alias_map.pop(alias)
|
|
40
64
|
except KeyError as e:
|
|
41
65
|
raise AliasError(f"Alias '{alias}' not found") from e
|
|
42
66
|
|
|
67
|
+
aliases_set = self._lookup_map[key]
|
|
68
|
+
aliases_set.discard(alias)
|
|
69
|
+
if not aliases_set:
|
|
70
|
+
del self._lookup_map[key]
|
|
71
|
+
|
|
43
72
|
@staticmethod
|
|
44
73
|
def _unpack(args):
|
|
45
74
|
return args[0] if len(args) == 1 and isinstance(args[0], (list, tuple)) else args
|
|
@@ -52,30 +81,29 @@ class AliasDict(UserDict):
|
|
|
52
81
|
def clear(self):
|
|
53
82
|
"""Clear all data and aliases."""
|
|
54
83
|
super().clear()
|
|
55
|
-
self.
|
|
84
|
+
self._lookup_map.clear()
|
|
85
|
+
self._alias_map.clear()
|
|
56
86
|
|
|
57
87
|
def clear_aliases(self):
|
|
58
88
|
"""Remove all aliases."""
|
|
59
|
-
self.
|
|
89
|
+
self._lookup_map.clear()
|
|
90
|
+
self._alias_map.clear()
|
|
60
91
|
|
|
61
92
|
def aliases(self):
|
|
62
93
|
"""Return all aliases."""
|
|
63
|
-
return self.
|
|
94
|
+
return self._alias_map.keys()
|
|
64
95
|
|
|
65
96
|
def is_alias(self, key):
|
|
66
97
|
"""Return True if the key is an alias, False otherwise."""
|
|
67
|
-
return key in self.
|
|
98
|
+
return key in self._alias_map
|
|
68
99
|
|
|
69
100
|
def has_aliases(self, key):
|
|
70
101
|
"""Return True if the key has any aliases, False otherwise."""
|
|
71
|
-
return key in self.
|
|
102
|
+
return key in self._lookup_map
|
|
72
103
|
|
|
73
104
|
def keys_with_aliases(self):
|
|
74
105
|
"""Return keys with their aliases."""
|
|
75
|
-
|
|
76
|
-
for alias, key in self._alias_dict.items():
|
|
77
|
-
result[key].append(alias)
|
|
78
|
-
return result.items()
|
|
106
|
+
return self._lookup_map.items()
|
|
79
107
|
|
|
80
108
|
def origin_keys(self):
|
|
81
109
|
"""Return original keys (without aliases)."""
|
|
@@ -83,12 +111,11 @@ class AliasDict(UserDict):
|
|
|
83
111
|
|
|
84
112
|
def origin_key(self, alias):
|
|
85
113
|
"""Return the original key for an alias, or None if not an alias."""
|
|
86
|
-
return self.
|
|
114
|
+
return self._alias_map.get(alias)
|
|
87
115
|
|
|
88
116
|
def keys(self):
|
|
89
117
|
"""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())
|
|
118
|
+
return (self.data | self._alias_map).keys()
|
|
92
119
|
|
|
93
120
|
def values(self):
|
|
94
121
|
"""Return all values."""
|
|
@@ -96,47 +123,52 @@ class AliasDict(UserDict):
|
|
|
96
123
|
|
|
97
124
|
def items(self):
|
|
98
125
|
"""Return all items (including alias/value pairs)."""
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
return (self.data | {k: self.data[v] for k, v in self._alias_map.items()}).items()
|
|
127
|
+
|
|
128
|
+
def iterkeys(self):
|
|
129
|
+
"""Return a lazy iterator over all keys and aliases."""
|
|
130
|
+
return iter(self)
|
|
131
|
+
|
|
132
|
+
def iteritems(self):
|
|
133
|
+
"""Return a lazy iterator over all items (including alias/value pairs)."""
|
|
134
|
+
return chain(self.data.items(), ((k, self.data[v]) for k, v in self._alias_map.items()))
|
|
103
135
|
|
|
104
136
|
def origin_len(self):
|
|
105
137
|
"""Return count of original keys (without aliases)."""
|
|
106
138
|
return len(self.data)
|
|
107
139
|
|
|
108
140
|
def __len__(self):
|
|
109
|
-
return len(self.data) + len(self.
|
|
141
|
+
return len(self.data) + len(self._alias_map)
|
|
110
142
|
|
|
111
143
|
def __missing__(self, key):
|
|
112
144
|
try:
|
|
113
|
-
return super().__getitem__(self.
|
|
145
|
+
return super().__getitem__(self._alias_map[key])
|
|
114
146
|
except KeyError:
|
|
115
147
|
raise KeyError(key) from None
|
|
116
148
|
|
|
117
149
|
def __setitem__(self, key, value):
|
|
118
150
|
try:
|
|
119
|
-
key = self.
|
|
151
|
+
key = self._alias_map[key]
|
|
120
152
|
except KeyError:
|
|
121
153
|
pass
|
|
122
154
|
super().__setitem__(key, value)
|
|
123
155
|
|
|
124
156
|
def __delitem__(self, key):
|
|
125
157
|
try:
|
|
126
|
-
self.data
|
|
127
|
-
for alias in
|
|
128
|
-
del self.
|
|
158
|
+
del self.data[key]
|
|
159
|
+
for alias in self._lookup_map.pop(key, ()):
|
|
160
|
+
del self._alias_map[alias]
|
|
129
161
|
except KeyError:
|
|
130
162
|
return self.remove_alias(key)
|
|
131
163
|
|
|
132
164
|
def __contains__(self, item):
|
|
133
|
-
return item in self.data or item in self.
|
|
165
|
+
return item in self.data or item in self._alias_map
|
|
134
166
|
|
|
135
167
|
def __iter__(self):
|
|
136
|
-
return chain(self.data, self.
|
|
168
|
+
return chain(self.data, self._alias_map)
|
|
137
169
|
|
|
138
170
|
def __reversed__(self):
|
|
139
|
-
return chain(reversed(self.
|
|
171
|
+
return chain(reversed(self._alias_map), reversed(self.data))
|
|
140
172
|
|
|
141
173
|
def copy(self):
|
|
142
174
|
"""Return a shallow copy of the AliasDict."""
|
|
@@ -148,7 +180,8 @@ class AliasDict(UserDict):
|
|
|
148
180
|
def __eq__(self, other):
|
|
149
181
|
if not isinstance(other, AliasDict):
|
|
150
182
|
return NotImplemented
|
|
151
|
-
|
|
183
|
+
# _lookup_map is derived from _alias_map, so comparing it is redundant
|
|
184
|
+
return self.data == other.data and self._alias_map == other._alias_map
|
|
152
185
|
|
|
153
186
|
def __or__(self, other):
|
|
154
187
|
if not isinstance(other, Mapping):
|
|
@@ -156,7 +189,10 @@ class AliasDict(UserDict):
|
|
|
156
189
|
new = self.copy()
|
|
157
190
|
if isinstance(other, AliasDict):
|
|
158
191
|
new.update(other.data)
|
|
159
|
-
|
|
192
|
+
self._validate_merge_aliases(new, other)
|
|
193
|
+
new._alias_map.update(other._alias_map)
|
|
194
|
+
for k, v in other._lookup_map.items():
|
|
195
|
+
new._lookup_map.setdefault(k, set()).update(v)
|
|
160
196
|
else:
|
|
161
197
|
new.update(other)
|
|
162
198
|
return new
|
|
@@ -164,17 +200,35 @@ class AliasDict(UserDict):
|
|
|
164
200
|
def __ror__(self, other):
|
|
165
201
|
if not isinstance(other, Mapping):
|
|
166
202
|
return NotImplemented
|
|
167
|
-
new =
|
|
203
|
+
new = type(self)(other)
|
|
168
204
|
new.update(self.data)
|
|
169
|
-
|
|
205
|
+
self._validate_merge_aliases(new, self)
|
|
206
|
+
new._alias_map.update(self._alias_map)
|
|
207
|
+
for k, v in self._lookup_map.items():
|
|
208
|
+
new._lookup_map.setdefault(k, set()).update(v)
|
|
170
209
|
return new
|
|
171
210
|
|
|
172
211
|
def __ior__(self, other):
|
|
173
212
|
if isinstance(other, AliasDict):
|
|
213
|
+
self._validate_merge_aliases(self, other)
|
|
174
214
|
self.update(other.data)
|
|
175
|
-
self.
|
|
215
|
+
self._alias_map.update(other._alias_map)
|
|
216
|
+
for k, v in other._lookup_map.items():
|
|
217
|
+
self._lookup_map.setdefault(k, set()).update(v)
|
|
176
218
|
else:
|
|
177
219
|
self.update(other)
|
|
178
220
|
return self
|
|
179
221
|
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _validate_merge_aliases(target, other):
|
|
224
|
+
"""Check that other's aliases don't collide with target's keys and vice versa."""
|
|
225
|
+
for alias, key in other._alias_map.items(): # noqa
|
|
226
|
+
if alias in target.data:
|
|
227
|
+
raise AliasValueError(f"Alias '{alias}' already exists as a key in the dictionary")
|
|
228
|
+
if (existing := target._alias_map.get(alias)) is not None and existing != key: # noqa
|
|
229
|
+
raise AliasValueError(f"Alias '{alias}' already assigned to key '{existing}'")
|
|
230
|
+
for key in other.data:
|
|
231
|
+
if key in target._alias_map: # noqa
|
|
232
|
+
raise AliasValueError(f"Key '{key}' already exists as an alias in the dictionary")
|
|
233
|
+
|
|
180
234
|
__hash__ = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aldict
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
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>
|
|
@@ -10,7 +10,7 @@ Requires-Python: >=3.10
|
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<img src="https://github.com/kaliv0/aldict/blob/main/assets/alter-ego.jpg?raw=true" width="250" alt="Alter Ego">
|
|
13
|
+
<img src="https://github.com/kaliv0/aldict/blob/main/.github/assets/alter-ego.jpg?raw=true" width="250" alt="Alter Ego">
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
---
|
|
@@ -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,7 @@
|
|
|
1
|
+
aldict-1.2.1.dist-info/METADATA,sha256=EhnQOvS5TDWK14MVRelIRNyspGyzRkxKbr_WyF-L3VM,5247
|
|
2
|
+
aldict-1.2.1.dist-info/WHEEL,sha256=Wb0ASbVj8JvWHpOiIpPi7ucfIgJeCi__PzivviEAQFc,90
|
|
3
|
+
aldict-1.2.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
aldict-1.2.1.dist-info/licenses/LICENSE,sha256=frOVyHZrx5o-fh5xC-kggT3MaLdp6yxV_YGpVXFHFSQ,1071
|
|
5
|
+
aldict/__init__.py,sha256=4TSCecvDUKGKQHAYyws0jytjQiYiHRh9LA62L6kC-HI,206
|
|
6
|
+
aldict/alias_dict.py,sha256=DbGPulzlREVod-tMOnVY06Peezk6zsNZOx7aZ3YTuWU,8107
|
|
7
|
+
aldict-1.2.1.dist-info/RECORD,,
|
aldict/exception.py
DELETED
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
|