aldict 1.0.2__py3-none-any.whl → 1.1.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 +3 -0
- aldict/alias_dict.py +105 -30
- {aldict-1.0.2.dist-info → aldict-1.1.0.dist-info}/METADATA +78 -23
- aldict-1.1.0.dist-info/RECORD +8 -0
- {aldict-1.0.2.dist-info → aldict-1.1.0.dist-info}/WHEEL +1 -2
- aldict-1.1.0.dist-info/entry_points.txt +4 -0
- aldict-1.0.2.dist-info/RECORD +0 -8
- aldict-1.0.2.dist-info/top_level.txt +0 -1
- {aldict-1.0.2.dist-info → aldict-1.1.0.dist-info/licenses}/LICENSE +0 -0
aldict/__init__.py
CHANGED
aldict/alias_dict.py
CHANGED
|
@@ -1,75 +1,118 @@
|
|
|
1
1
|
from collections import UserDict, defaultdict
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from itertools import chain
|
|
2
4
|
|
|
3
5
|
from aldict.exception import AliasError, AliasValueError
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class AliasDict(UserDict):
|
|
7
|
-
"""
|
|
9
|
+
"""Dict with key-aliases pointing to shared values."""
|
|
8
10
|
|
|
9
|
-
def __init__(self, dict_):
|
|
11
|
+
def __init__(self, dict_=None, /, aliases=None):
|
|
10
12
|
self._alias_dict = {}
|
|
11
|
-
|
|
13
|
+
if isinstance(dict_, AliasDict):
|
|
14
|
+
super().__init__(dict_.data)
|
|
15
|
+
self._alias_dict = dict(dict_._alias_dict)
|
|
16
|
+
else:
|
|
17
|
+
super().__init__(dict_)
|
|
18
|
+
|
|
19
|
+
if aliases:
|
|
20
|
+
for key, alias_list in aliases.items():
|
|
21
|
+
self.add_alias(key, alias_list)
|
|
12
22
|
|
|
13
23
|
def add_alias(self, key, *aliases):
|
|
14
|
-
"""
|
|
15
|
-
if key not in self.data
|
|
24
|
+
"""Add one or more aliases to a key. Accepts *args or a list/tuple."""
|
|
25
|
+
if key not in self.data:
|
|
16
26
|
raise KeyError(key)
|
|
17
|
-
|
|
27
|
+
|
|
28
|
+
for alias in self._unpack(aliases):
|
|
18
29
|
if alias == key:
|
|
19
30
|
raise AliasValueError(f"Key and corresponding alias cannot be equal: '{key}'")
|
|
31
|
+
if alias in self.data:
|
|
32
|
+
raise AliasValueError(f"Alias '{alias}' already exists as a key in the dictionary")
|
|
20
33
|
self._alias_dict[alias] = key
|
|
21
34
|
|
|
22
35
|
def remove_alias(self, *aliases):
|
|
23
|
-
"""
|
|
24
|
-
for alias in aliases:
|
|
36
|
+
"""Remove one or more aliases. Accepts *args or a list/tuple."""
|
|
37
|
+
for alias in self._unpack(aliases):
|
|
25
38
|
try:
|
|
26
39
|
self._alias_dict.__delitem__(alias)
|
|
27
40
|
except KeyError as e:
|
|
28
|
-
raise AliasError(alias) from e
|
|
41
|
+
raise AliasError(f"Alias '{alias}' not found") from e
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _unpack(args):
|
|
45
|
+
return args[0] if len(args) == 1 and isinstance(args[0], (list, tuple)) else args
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def fromkeys(cls, iterable, value=None, aliases=None):
|
|
49
|
+
"""Create an AliasDict from an iterable of keys with optional aliases."""
|
|
50
|
+
return cls(dict.fromkeys(iterable, value), aliases=aliases)
|
|
51
|
+
|
|
52
|
+
def clear(self):
|
|
53
|
+
"""Clear all data and aliases."""
|
|
54
|
+
super().clear()
|
|
55
|
+
self._alias_dict.clear()
|
|
29
56
|
|
|
30
57
|
def clear_aliases(self):
|
|
31
|
-
"""
|
|
58
|
+
"""Remove all aliases."""
|
|
32
59
|
self._alias_dict.clear()
|
|
33
60
|
|
|
34
61
|
def aliases(self):
|
|
35
|
-
"""
|
|
62
|
+
"""Return all aliases."""
|
|
36
63
|
return self._alias_dict.keys()
|
|
37
64
|
|
|
38
|
-
def
|
|
39
|
-
"""
|
|
65
|
+
def is_alias(self, key):
|
|
66
|
+
"""Return True if the key is an alias, False otherwise."""
|
|
67
|
+
return key in self._alias_dict
|
|
68
|
+
|
|
69
|
+
def has_aliases(self, key):
|
|
70
|
+
"""Return True if the key has any aliases, False otherwise."""
|
|
71
|
+
return key in self._alias_dict.values()
|
|
72
|
+
|
|
73
|
+
def keys_with_aliases(self):
|
|
74
|
+
"""Return keys with their aliases."""
|
|
40
75
|
result = defaultdict(list)
|
|
41
76
|
for alias, key in self._alias_dict.items():
|
|
42
77
|
result[key].append(alias)
|
|
43
78
|
return result.items()
|
|
44
79
|
|
|
45
80
|
def origin_keys(self):
|
|
46
|
-
"""
|
|
81
|
+
"""Return original keys (without aliases)."""
|
|
47
82
|
return self.data.keys()
|
|
48
83
|
|
|
84
|
+
def origin_key(self, alias):
|
|
85
|
+
"""Return the original key for an alias, or None if not an alias."""
|
|
86
|
+
return self._alias_dict.get(alias)
|
|
87
|
+
|
|
49
88
|
def keys(self):
|
|
50
|
-
"""
|
|
89
|
+
"""Return all keys and aliases."""
|
|
51
90
|
return dict(**self.data, **self._alias_dict).keys()
|
|
91
|
+
# NB: could be optimized as 'return iter(self)' but we won't be able to call e.g. len(alias_dict.keys())
|
|
52
92
|
|
|
53
93
|
def values(self):
|
|
54
|
-
"""
|
|
94
|
+
"""Return all values."""
|
|
55
95
|
return self.data.values()
|
|
56
96
|
|
|
57
97
|
def items(self):
|
|
58
|
-
"""
|
|
98
|
+
"""Return all items (including alias/value pairs)."""
|
|
59
99
|
return dict(**self.data, **{k: self.data[v] for k, v in self._alias_dict.items()}).items()
|
|
100
|
+
# NB: could be optimized as
|
|
101
|
+
# 'return chain(self.data.items(), ((k, self.data[v]) for k, v in self._alias_dict.items()))'
|
|
102
|
+
# (same as .keys() above)
|
|
60
103
|
|
|
61
104
|
def origin_len(self):
|
|
62
|
-
"""
|
|
105
|
+
"""Return count of original keys (without aliases)."""
|
|
63
106
|
return len(self.data)
|
|
64
107
|
|
|
65
108
|
def __len__(self):
|
|
66
|
-
return len(self.
|
|
109
|
+
return len(self.data) + len(self._alias_dict)
|
|
67
110
|
|
|
68
111
|
def __missing__(self, key):
|
|
69
112
|
try:
|
|
70
113
|
return super().__getitem__(self._alias_dict[key])
|
|
71
|
-
except
|
|
72
|
-
raise KeyError(key) from
|
|
114
|
+
except KeyError:
|
|
115
|
+
raise KeyError(key) from None
|
|
73
116
|
|
|
74
117
|
def __setitem__(self, key, value):
|
|
75
118
|
try:
|
|
@@ -81,25 +124,57 @@ class AliasDict(UserDict):
|
|
|
81
124
|
def __delitem__(self, key):
|
|
82
125
|
try:
|
|
83
126
|
self.data.__delitem__(key)
|
|
84
|
-
|
|
127
|
+
for alias in [k for k, v in self._alias_dict.items() if v == key]:
|
|
128
|
+
del self._alias_dict[alias]
|
|
85
129
|
except KeyError:
|
|
86
|
-
# in case we try to delete alias via pop() or del
|
|
87
130
|
return self.remove_alias(key)
|
|
88
131
|
|
|
89
132
|
def __contains__(self, item):
|
|
90
|
-
return item in
|
|
133
|
+
return item in self.data or item in self._alias_dict
|
|
91
134
|
|
|
92
135
|
def __iter__(self):
|
|
93
|
-
|
|
94
|
-
|
|
136
|
+
return chain(self.data, self._alias_dict)
|
|
137
|
+
|
|
138
|
+
def __reversed__(self):
|
|
139
|
+
return chain(reversed(self._alias_dict), reversed(self.data))
|
|
140
|
+
|
|
141
|
+
def copy(self):
|
|
142
|
+
"""Return a shallow copy of the AliasDict."""
|
|
143
|
+
return type(self)(self)
|
|
95
144
|
|
|
96
145
|
def __repr__(self):
|
|
97
|
-
return f"AliasDict({self.items()})"
|
|
146
|
+
return f"AliasDict({dict(self.items())})"
|
|
98
147
|
|
|
99
148
|
def __eq__(self, other):
|
|
100
149
|
if not isinstance(other, AliasDict):
|
|
101
|
-
|
|
150
|
+
return NotImplemented
|
|
102
151
|
return self.data == other.data and self._alias_dict == other._alias_dict
|
|
103
152
|
|
|
104
|
-
def
|
|
105
|
-
|
|
153
|
+
def __or__(self, other):
|
|
154
|
+
if not isinstance(other, Mapping):
|
|
155
|
+
return NotImplemented
|
|
156
|
+
new = self.copy()
|
|
157
|
+
if isinstance(other, AliasDict):
|
|
158
|
+
new.update(other.data)
|
|
159
|
+
new._alias_dict.update(other._alias_dict)
|
|
160
|
+
else:
|
|
161
|
+
new.update(other)
|
|
162
|
+
return new
|
|
163
|
+
|
|
164
|
+
def __ror__(self, other):
|
|
165
|
+
if not isinstance(other, Mapping):
|
|
166
|
+
return NotImplemented
|
|
167
|
+
new = AliasDict(other)
|
|
168
|
+
new.update(self.data)
|
|
169
|
+
new._alias_dict.update(self._alias_dict)
|
|
170
|
+
return new
|
|
171
|
+
|
|
172
|
+
def __ior__(self, other):
|
|
173
|
+
if isinstance(other, AliasDict):
|
|
174
|
+
self.update(other.data)
|
|
175
|
+
self._alias_dict.update(other._alias_dict)
|
|
176
|
+
else:
|
|
177
|
+
self.update(other)
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
__hash__ = None
|
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: aldict
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Multi-key dictionary, supports adding and manipulating key-aliases pointing to shared values
|
|
5
|
-
Author-email: Kaloyan Ivanov <kaloyan.ivanov88@gmail.com>
|
|
6
|
-
Project-URL: repository, https://github.com/kaliv0/aldict
|
|
7
5
|
Keywords: multi-key dictionary,multidict,alias-dict
|
|
8
|
-
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
6
|
+
Author-Email: kaliv0 <kaloyan.ivanov88@gmail.com>
|
|
10
7
|
License-File: LICENSE
|
|
11
|
-
|
|
12
|
-
Requires-
|
|
13
|
-
|
|
14
|
-
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
15
|
-
Requires-Dist: twine>=6.0.1; extra == "dev"
|
|
8
|
+
Project-URL: repository, https://github.com/kaliv0/aldict
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
16
11
|
|
|
17
12
|
<p align="center">
|
|
18
13
|
<img src="https://github.com/kaliv0/aldict/blob/main/assets/alter-ego.jpg?raw=true" width="250" alt="Alter Ego">
|
|
@@ -22,7 +17,7 @@ Requires-Dist: twine>=6.0.1; extra == "dev"
|
|
|
22
17
|
# Aldict
|
|
23
18
|
|
|
24
19
|
[](https://github.com/kaliv0/aldict/actions/workflows/ci.yml)
|
|
25
|
-

|
|
26
21
|
[](https://pypi.org/project/aldict/)
|
|
27
22
|
[](https://github.com/kaliv0/aldict/blob/main/LICENSE)
|
|
28
23
|
[](https://pepy.tech/projects/aldict)
|
|
@@ -32,20 +27,32 @@ Multi-key dictionary, supports adding and manipulating key-aliases pointing to s
|
|
|
32
27
|
---
|
|
33
28
|
## How to use
|
|
34
29
|
|
|
30
|
+
- initialize with aliases
|
|
31
|
+
<br>(one-liner with <i>aliases</i> dict mapping <i>key</i> to list of <i>aliases</i>)
|
|
32
|
+
```python
|
|
33
|
+
ad = AliasDict({"a": 1, "b": 2}, aliases={"a": ["aa", "aaa"], "b": ["bb"]})
|
|
34
|
+
assert ad["a"] == ad["aa"] == ad["aaa"] == 1
|
|
35
|
+
assert ad["b"] == ad["bb"] == 2
|
|
36
|
+
```
|
|
35
37
|
- add_alias
|
|
36
|
-
<br>(pass <i>key</i> as first parameter and <i>alias(es)</i> as variadic params)
|
|
38
|
+
<br>(pass <i>key</i> as first parameter and <i>alias(es)</i> as variadic params, list or tuple)
|
|
37
39
|
```python
|
|
38
40
|
ad = AliasDict({"a": 1, "b": 2})
|
|
39
41
|
ad.add_alias("a", "aa")
|
|
40
42
|
ad.add_alias("b", "bb", "Bbb")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
ad.add_alias("a", ["aaa", "aaaa"])
|
|
44
|
+
ad.add_alias("b", ("bbb",))
|
|
45
|
+
|
|
46
|
+
assert ad["a"] == ad["aa"] == ad["aaa"] == ad["aaaa"] == 1
|
|
47
|
+
assert ad["b"] == ad["bb"] == ad["Bbb"] == ad["bbb"] == 2
|
|
43
48
|
```
|
|
44
49
|
- remove_alias
|
|
45
|
-
<br>(pass <i>alias(es)</i> to be removed as variadic
|
|
50
|
+
<br>(pass <i>alias(es)</i> to be removed as variadic params, list or tuple)
|
|
46
51
|
```python
|
|
47
52
|
ad.remove_alias("aa")
|
|
48
53
|
ad.remove_alias("bb", "Bbb")
|
|
54
|
+
ad.remove_alias(["aaa", "aaaa"])
|
|
55
|
+
ad.remove_alias(("bbb",))
|
|
49
56
|
assert len(ad.aliases()) == 0
|
|
50
57
|
```
|
|
51
58
|
- clear_aliases
|
|
@@ -72,10 +79,10 @@ ad.add_alias("b", "bb", "B")
|
|
|
72
79
|
ad.add_alias("a", "ab", "A")
|
|
73
80
|
assert list(ad.aliases()) == ['aa', 'bb', 'B', 'ab', 'A']
|
|
74
81
|
```
|
|
75
|
-
-
|
|
82
|
+
- keys_with_aliases
|
|
76
83
|
<br>(read <i>keys</i> with corresponding <i>alias(es)</i>)
|
|
77
84
|
```python
|
|
78
|
-
assert dict(ad.
|
|
85
|
+
assert dict(ad.keys_with_aliases()) == {'a': ['aa', 'ab', 'A'], 'b': ['bb', 'B']}
|
|
79
86
|
```
|
|
80
87
|
- read dictviews
|
|
81
88
|
<br>(<i>dict.keys()</i> and <i>dict.items()</i> include <i>aliased</i> versions)
|
|
@@ -87,11 +94,10 @@ ad.add_alias("y", "Yy", "xyz")
|
|
|
87
94
|
ad.keys()
|
|
88
95
|
ad.values()
|
|
89
96
|
ad.items()
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
dict_items([('x', 10), ('y', 20), ('Xx', 10), ('Yy', 20), ('xyz', 20)])
|
|
97
|
+
|
|
98
|
+
# dict_keys(['x', 'y', 'Xx', 'Yy', 'xyz'])
|
|
99
|
+
# dict_values([10, 20])
|
|
100
|
+
# dict_items([('x', 10), ('y', 20), ('Xx', 10), ('Yy', 20), ('xyz', 20)])
|
|
95
101
|
```
|
|
96
102
|
- remove key and aliases
|
|
97
103
|
```python
|
|
@@ -112,3 +118,52 @@ assert list(ad.keys()) == ["a", "b", "aa"]
|
|
|
112
118
|
assert len(ad) == 3
|
|
113
119
|
assert ad.origin_len() == 2
|
|
114
120
|
```
|
|
121
|
+
- copy
|
|
122
|
+
```python
|
|
123
|
+
ad = AliasDict({"a": 1, "b": 2})
|
|
124
|
+
ad.add_alias("a", "aa")
|
|
125
|
+
ad_copy = ad.copy()
|
|
126
|
+
assert ad_copy == ad
|
|
127
|
+
assert ad_copy is not ad
|
|
128
|
+
```
|
|
129
|
+
- merge with | and |= operators
|
|
130
|
+
```python
|
|
131
|
+
ad1 = AliasDict({"a": 1}, aliases={"a": ["aa"]})
|
|
132
|
+
ad2 = AliasDict({"b": 2}, aliases={"b": ["bb"]})
|
|
133
|
+
|
|
134
|
+
merged = ad1 | ad2
|
|
135
|
+
assert merged["aa"] == 1
|
|
136
|
+
assert merged["bb"] == 2
|
|
137
|
+
|
|
138
|
+
ad1 |= {"c": 3}
|
|
139
|
+
assert ad1["c"] == 3
|
|
140
|
+
```
|
|
141
|
+
- fromkeys
|
|
142
|
+
```python
|
|
143
|
+
ad = AliasDict.fromkeys(["a", "b", "c"], 0, aliases={"a": ["aa"]})
|
|
144
|
+
assert ad["a"] == ad["aa"] == 0
|
|
145
|
+
```
|
|
146
|
+
- origin_key
|
|
147
|
+
<br>(get original <i>key</i> for an <i>alias</i>)
|
|
148
|
+
```python
|
|
149
|
+
ad = AliasDict({"a": 1, "b": 2})
|
|
150
|
+
ad.add_alias("a", "aa")
|
|
151
|
+
assert ad.origin_key("aa") == "a"
|
|
152
|
+
assert ad.origin_key("a") is None # not an alias
|
|
153
|
+
```
|
|
154
|
+
- is_alias
|
|
155
|
+
<br>(check if <i>key</i> is an <i>alias</i>)
|
|
156
|
+
```python
|
|
157
|
+
ad = AliasDict({"a": 1, "b": 2})
|
|
158
|
+
ad.add_alias("a", "aa")
|
|
159
|
+
assert ad.is_alias("aa") is True
|
|
160
|
+
assert ad.is_alias("a") is False
|
|
161
|
+
```
|
|
162
|
+
- has_aliases
|
|
163
|
+
<br>(check if <i>key</i> has any <i>aliases</i>)
|
|
164
|
+
```python
|
|
165
|
+
ad = AliasDict({"a": 1, "b": 2})
|
|
166
|
+
ad.add_alias("a", "aa")
|
|
167
|
+
assert ad.has_aliases("a") is True
|
|
168
|
+
assert ad.has_aliases("b") is False
|
|
169
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
aldict-1.1.0.dist-info/METADATA,sha256=AzRxupLUAta9ULmLVV1adUjoMswf7f7obMq7m_IRZlI,4882
|
|
2
|
+
aldict-1.1.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
aldict-1.1.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
aldict-1.1.0.dist-info/licenses/LICENSE,sha256=frOVyHZrx5o-fh5xC-kggT3MaLdp6yxV_YGpVXFHFSQ,1071
|
|
5
|
+
aldict/__init__.py,sha256=16jYDIi10e4UnbxjdFsssrt4dgUpsgb3h8cDD7U4EzQ,233
|
|
6
|
+
aldict/alias_dict.py,sha256=O4EGRp-VtW1Mb-Fr1-_VDkPof6G9UBYYhx5l2I8FKck,5934
|
|
7
|
+
aldict/exception.py,sha256=rFNmv9HuUOn2toPVYpVPQq6SOsl8nvwtJpGhPkK1puQ,83
|
|
8
|
+
aldict-1.1.0.dist-info/RECORD,,
|
aldict-1.0.2.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
aldict/__init__.py,sha256=0l5cJ5LuzCZhZfx-IOk2dEsq6nJQTShr6oGmufIa9i4,153
|
|
2
|
-
aldict/alias_dict.py,sha256=j92SVd2RN1pzrClYhxhtdRIWAZ2_eVml3P4jcK4R890,3304
|
|
3
|
-
aldict/exception.py,sha256=rFNmv9HuUOn2toPVYpVPQq6SOsl8nvwtJpGhPkK1puQ,83
|
|
4
|
-
aldict-1.0.2.dist-info/LICENSE,sha256=frOVyHZrx5o-fh5xC-kggT3MaLdp6yxV_YGpVXFHFSQ,1071
|
|
5
|
-
aldict-1.0.2.dist-info/METADATA,sha256=s-n_Y9mIdqTsWZvdlxkE0qH6YSbnN9DwCtw1TjmuM-g,3470
|
|
6
|
-
aldict-1.0.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
7
|
-
aldict-1.0.2.dist-info/top_level.txt,sha256=pXwIKxRsbW2_Lh1HM-4cG2xE44RBzMz07GaY2EONV5M,7
|
|
8
|
-
aldict-1.0.2.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
aldict
|
|
File without changes
|