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 CHANGED
@@ -1,6 +1,8 @@
1
- from .alias_dict import AliasDict as AliasDict
2
- from .exception import AliasError as AliasError
3
- from .exception import AliasValueError as AliasValueError
1
+ from .alias_dict import (
2
+ AliasDict as AliasDict,
3
+ AliasError as AliasError,
4
+ AliasValueError as AliasValueError,
5
+ )
4
6
 
5
- __version__ = "1.1.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, defaultdict
1
+ from collections import UserDict
2
2
  from collections.abc import Mapping
3
3
  from itertools import chain
4
4
 
5
- from aldict.exception import AliasError, AliasValueError
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._alias_dict = {}
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._alias_dict = dict(dict_._alias_dict)
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
- self._alias_dict[alias] = key
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._alias_dict.__delitem__(alias)
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._alias_dict.clear()
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._alias_dict.clear()
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._alias_dict.keys()
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._alias_dict
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._alias_dict.values()
102
+ return key in self._lookup_map
72
103
 
73
104
  def keys_with_aliases(self):
74
105
  """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()
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._alias_dict.get(alias)
114
+ return self._alias_map.get(alias)
87
115
 
88
116
  def keys(self):
89
117
  """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())
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 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)
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._alias_dict)
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._alias_dict[key])
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._alias_dict[key]
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.__delitem__(key)
127
- for alias in [k for k, v in self._alias_dict.items() if v == key]:
128
- del self._alias_dict[alias]
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._alias_dict
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._alias_dict)
168
+ return chain(self.data, self._alias_map)
137
169
 
138
170
  def __reversed__(self):
139
- return chain(reversed(self._alias_dict), reversed(self.data))
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
- return self.data == other.data and self._alias_dict == other._alias_dict
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
- new._alias_dict.update(other._alias_dict)
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 = AliasDict(other)
203
+ new = type(self)(other)
168
204
  new.update(self.data)
169
- new._alias_dict.update(self._alias_dict)
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._alias_dict.update(other._alias_dict)
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.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
  ```
@@ -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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.4.6)
2
+ Generator: pdm-backend (2.4.7)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
aldict/exception.py DELETED
@@ -1,6 +0,0 @@
1
- class AliasError(KeyError):
2
- pass
3
-
4
-
5
- class AliasValueError(ValueError):
6
- pass
@@ -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,,