aldict 1.1.2__tar.gz → 1.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aldict
3
- Version: 1.1.2
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": ["aa"]})
156
- ad2 = AliasDict({"b": 2}, aliases={"b": ["bb"]})
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": ["aa"]})
180
+ ad = AliasDict.fromkeys(["a", "b", "c"], 0, aliases={"a": "aa"})
168
181
  assert ad["a"] == ad["aa"] == 0
169
182
  ```
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://github.com/kaliv0/aldict/blob/main/assets/alter-ego.jpg?raw=true" width="250" alt="Alter Ego">
2
+ <img src="https://github.com/kaliv0/aldict/blob/main/.github/assets/alter-ego.jpg?raw=true" width="250" alt="Alter Ego">
3
3
  </p>
4
4
 
5
5
  ---
@@ -88,6 +88,19 @@ ad.items()
88
88
  # dict_values([10, 20])
89
89
  # dict_items([('x', 10), ('y', 20), ('Xx', 10), ('Yy', 20), ('xyz', 20)])
90
90
  ```
91
+ - iterkeys
92
+ <br>(lazy iterator over all <i>keys</i> and <i>aliases</i>)
93
+ ```python
94
+ ad = AliasDict({"x": 10, "y": 20})
95
+ ad.add_alias("x", "Xx")
96
+
97
+ assert list(ad.iterkeys()) == ['x', 'y', 'Xx']
98
+ ```
99
+ - iteritems
100
+ <br>(lazy iterator over all <i>items</i> including <i>alias/value</i> pairs)
101
+ ```python
102
+ assert list(ad.iteritems()) == [('x', 10), ('y', 20), ('Xx', 10)]
103
+ ```
91
104
  - remove key and aliases
92
105
  ```python
93
106
  ad.pop("y")
@@ -141,8 +154,8 @@ assert ad_copy is not ad
141
154
  ```
142
155
  - merge with | and |= operators
143
156
  ```python
144
- ad1 = AliasDict({"a": 1}, aliases={"a": ["aa"]})
145
- ad2 = AliasDict({"b": 2}, aliases={"b": ["bb"]})
157
+ ad1 = AliasDict({"a": 1}, aliases={"a": "aa"})
158
+ ad2 = AliasDict({"b": 2}, aliases={"b": "bb"})
146
159
 
147
160
  merged = ad1 | ad2
148
161
  assert merged["aa"] == 1
@@ -153,6 +166,6 @@ assert ad1["c"] == 3
153
166
  ```
154
167
  - fromkeys
155
168
  ```python
156
- ad = AliasDict.fromkeys(["a", "b", "c"], 0, aliases={"a": ["aa"]})
169
+ ad = AliasDict.fromkeys(["a", "b", "c"], 0, aliases={"a": "aa"})
157
170
  assert ad["a"] == ad["aa"] == 0
158
171
  ```
@@ -0,0 +1,8 @@
1
+ from .alias_dict import (
2
+ AliasDict as AliasDict,
3
+ AliasError as AliasError,
4
+ AliasValueError as AliasValueError,
5
+ )
6
+
7
+ __version__ = "1.2.1"
8
+ __all__ = ["AliasDict", "AliasError", "AliasValueError"]
@@ -0,0 +1,234 @@
1
+ from collections import UserDict
2
+ from collections.abc import Mapping
3
+ from itertools import chain
4
+
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
16
+
17
+
18
+ class AliasDict(UserDict):
19
+ """Dict with key-aliases pointing to shared values."""
20
+
21
+ def __init__(self, dict_=None, /, aliases=None):
22
+ self._lookup_map = {} # key -> set(aliases)
23
+ self._alias_map = {} # alias -> key
24
+
25
+ if isinstance(dict_, AliasDict):
26
+ super().__init__(dict_.data)
27
+ self._lookup_map = {k: v.copy() for k, v in dict_._lookup_map.items()}
28
+ self._alias_map = dict_._alias_map.copy()
29
+ else:
30
+ super().__init__(dict_)
31
+
32
+ if aliases:
33
+ for key, alias_list in aliases.items():
34
+ self.add_alias(key, alias_list)
35
+
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."""
39
+ if key not in self.data:
40
+ raise KeyError(key)
41
+
42
+ for alias in self._unpack(aliases):
43
+ if alias == key:
44
+ raise AliasValueError(f"Key and corresponding alias cannot be equal: '{key}'")
45
+ if alias in self.data:
46
+ raise AliasValueError(f"Alias '{alias}' already exists as a key in the dictionary")
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
58
+
59
+ def remove_alias(self, *aliases):
60
+ """Remove one or more aliases. Accepts *args or a list/tuple."""
61
+ for alias in self._unpack(aliases):
62
+ try:
63
+ key = self._alias_map.pop(alias)
64
+ except KeyError as e:
65
+ raise AliasError(f"Alias '{alias}' not found") from e
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
+
72
+ @staticmethod
73
+ def _unpack(args):
74
+ return args[0] if len(args) == 1 and isinstance(args[0], (list, tuple)) else args
75
+
76
+ @classmethod
77
+ def fromkeys(cls, iterable, value=None, aliases=None):
78
+ """Create an AliasDict from an iterable of keys with optional aliases."""
79
+ return cls(dict.fromkeys(iterable, value), aliases=aliases)
80
+
81
+ def clear(self):
82
+ """Clear all data and aliases."""
83
+ super().clear()
84
+ self._lookup_map.clear()
85
+ self._alias_map.clear()
86
+
87
+ def clear_aliases(self):
88
+ """Remove all aliases."""
89
+ self._lookup_map.clear()
90
+ self._alias_map.clear()
91
+
92
+ def aliases(self):
93
+ """Return all aliases."""
94
+ return self._alias_map.keys()
95
+
96
+ def is_alias(self, key):
97
+ """Return True if the key is an alias, False otherwise."""
98
+ return key in self._alias_map
99
+
100
+ def has_aliases(self, key):
101
+ """Return True if the key has any aliases, False otherwise."""
102
+ return key in self._lookup_map
103
+
104
+ def keys_with_aliases(self):
105
+ """Return keys with their aliases."""
106
+ return self._lookup_map.items()
107
+
108
+ def origin_keys(self):
109
+ """Return original keys (without aliases)."""
110
+ return self.data.keys()
111
+
112
+ def origin_key(self, alias):
113
+ """Return the original key for an alias, or None if not an alias."""
114
+ return self._alias_map.get(alias)
115
+
116
+ def keys(self):
117
+ """Return all keys and aliases."""
118
+ return (self.data | self._alias_map).keys()
119
+
120
+ def values(self):
121
+ """Return all values."""
122
+ return self.data.values()
123
+
124
+ def items(self):
125
+ """Return all items (including alias/value pairs)."""
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()))
135
+
136
+ def origin_len(self):
137
+ """Return count of original keys (without aliases)."""
138
+ return len(self.data)
139
+
140
+ def __len__(self):
141
+ return len(self.data) + len(self._alias_map)
142
+
143
+ def __missing__(self, key):
144
+ try:
145
+ return super().__getitem__(self._alias_map[key])
146
+ except KeyError:
147
+ raise KeyError(key) from None
148
+
149
+ def __setitem__(self, key, value):
150
+ try:
151
+ key = self._alias_map[key]
152
+ except KeyError:
153
+ pass
154
+ super().__setitem__(key, value)
155
+
156
+ def __delitem__(self, key):
157
+ try:
158
+ del self.data[key]
159
+ for alias in self._lookup_map.pop(key, ()):
160
+ del self._alias_map[alias]
161
+ except KeyError:
162
+ return self.remove_alias(key)
163
+
164
+ def __contains__(self, item):
165
+ return item in self.data or item in self._alias_map
166
+
167
+ def __iter__(self):
168
+ return chain(self.data, self._alias_map)
169
+
170
+ def __reversed__(self):
171
+ return chain(reversed(self._alias_map), reversed(self.data))
172
+
173
+ def copy(self):
174
+ """Return a shallow copy of the AliasDict."""
175
+ return type(self)(self)
176
+
177
+ def __repr__(self):
178
+ return f"AliasDict({dict(self.items())})"
179
+
180
+ def __eq__(self, other):
181
+ if not isinstance(other, AliasDict):
182
+ return NotImplemented
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
185
+
186
+ def __or__(self, other):
187
+ if not isinstance(other, Mapping):
188
+ return NotImplemented
189
+ new = self.copy()
190
+ if isinstance(other, AliasDict):
191
+ new.update(other.data)
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)
196
+ else:
197
+ new.update(other)
198
+ return new
199
+
200
+ def __ror__(self, other):
201
+ if not isinstance(other, Mapping):
202
+ return NotImplemented
203
+ new = type(self)(other)
204
+ new.update(self.data)
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)
209
+ return new
210
+
211
+ def __ior__(self, other):
212
+ if isinstance(other, AliasDict):
213
+ self._validate_merge_aliases(self, other)
214
+ self.update(other.data)
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)
218
+ else:
219
+ self.update(other)
220
+ return self
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
+
234
+ __hash__ = None
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aldict"
3
- version = "1.1.2"
3
+ version = "1.2.1"
4
4
  requires-python = ">=3.10"
5
5
  authors = [
6
6
  { name = "kaliv0", email = "kaloyan.ivanov88@gmail.com" },
@@ -1,6 +0,0 @@
1
- from .alias_dict import AliasDict as AliasDict
2
- from .exception import AliasError as AliasError
3
- from .exception import AliasValueError as AliasValueError
4
-
5
- __version__ = "1.1.1"
6
- __all__ = ["AliasDict", "AliasError", "AliasValueError"]
@@ -1,180 +0,0 @@
1
- from collections import UserDict, defaultdict
2
- from collections.abc import Mapping
3
- from itertools import chain
4
-
5
- from aldict.exception import AliasError, AliasValueError
6
-
7
-
8
- class AliasDict(UserDict):
9
- """Dict with key-aliases pointing to shared values."""
10
-
11
- def __init__(self, dict_=None, /, aliases=None):
12
- self._alias_dict = {}
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)
22
-
23
- def add_alias(self, key, *aliases):
24
- """Add one or more aliases to a key. Accepts *args or a list/tuple."""
25
- if key not in self.data:
26
- raise KeyError(key)
27
-
28
- for alias in self._unpack(aliases):
29
- if alias == key:
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")
33
- self._alias_dict[alias] = key
34
-
35
- def remove_alias(self, *aliases):
36
- """Remove one or more aliases. Accepts *args or a list/tuple."""
37
- for alias in self._unpack(aliases):
38
- try:
39
- self._alias_dict.__delitem__(alias)
40
- except KeyError as 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()
56
-
57
- def clear_aliases(self):
58
- """Remove all aliases."""
59
- self._alias_dict.clear()
60
-
61
- def aliases(self):
62
- """Return all aliases."""
63
- return self._alias_dict.keys()
64
-
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."""
75
- result = defaultdict(list)
76
- for alias, key in self._alias_dict.items():
77
- result[key].append(alias)
78
- return result.items()
79
-
80
- def origin_keys(self):
81
- """Return original keys (without aliases)."""
82
- return self.data.keys()
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
-
88
- def keys(self):
89
- """Return all keys and aliases."""
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())
92
-
93
- def values(self):
94
- """Return all values."""
95
- return self.data.values()
96
-
97
- def items(self):
98
- """Return all items (including alias/value pairs)."""
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)
103
-
104
- def origin_len(self):
105
- """Return count of original keys (without aliases)."""
106
- return len(self.data)
107
-
108
- def __len__(self):
109
- return len(self.data) + len(self._alias_dict)
110
-
111
- def __missing__(self, key):
112
- try:
113
- return super().__getitem__(self._alias_dict[key])
114
- except KeyError:
115
- raise KeyError(key) from None
116
-
117
- def __setitem__(self, key, value):
118
- try:
119
- key = self._alias_dict[key]
120
- except KeyError:
121
- pass
122
- super().__setitem__(key, value)
123
-
124
- def __delitem__(self, key):
125
- try:
126
- self.data.__delitem__(key)
127
- for alias in [k for k, v in self._alias_dict.items() if v == key]:
128
- del self._alias_dict[alias]
129
- except KeyError:
130
- return self.remove_alias(key)
131
-
132
- def __contains__(self, item):
133
- return item in self.data or item in self._alias_dict
134
-
135
- def __iter__(self):
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)
144
-
145
- def __repr__(self):
146
- return f"AliasDict({dict(self.items())})"
147
-
148
- def __eq__(self, other):
149
- if not isinstance(other, AliasDict):
150
- return NotImplemented
151
- return self.data == other.data and self._alias_dict == other._alias_dict
152
-
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,6 +0,0 @@
1
- class AliasError(KeyError):
2
- pass
3
-
4
-
5
- class AliasValueError(ValueError):
6
- pass
File without changes