aldict 1.1.1__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 CHANGED
@@ -2,5 +2,5 @@ from .alias_dict import AliasDict as AliasDict
2
2
  from .exception import AliasError as AliasError
3
3
  from .exception import AliasValueError as AliasValueError
4
4
 
5
- __version__ = "1.1.1"
5
+ __version__ = "1.2.0"
6
6
  __all__ = ["AliasDict", "AliasError", "AliasValueError"]
aldict/alias_dict.py CHANGED
@@ -1,4 +1,4 @@
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
 
@@ -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._alias_dict = {}
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._alias_dict = dict(dict_._alias_dict)
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
- self._alias_dict[alias] = key
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._alias_dict.__delitem__(alias)
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._alias_dict.clear()
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._alias_dict.clear()
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._alias_dict.keys()
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._alias_dict
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._alias_dict.values()
92
+ return key in self._lookup_map
72
93
 
73
94
  def keys_with_aliases(self):
74
95
  """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()
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._alias_dict.get(alias)
104
+ return self._alias_map.get(alias)
87
105
 
88
106
  def keys(self):
89
107
  """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())
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 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)
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._alias_dict)
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._alias_dict[key])
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._alias_dict[key]
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.__delitem__(key)
127
- for alias in [k for k, v in self._alias_dict.items() if v == key]:
128
- del self._alias_dict[alias]
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._alias_dict
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._alias_dict)
158
+ return chain(self.data, self._alias_map)
137
159
 
138
160
  def __reversed__(self):
139
- return chain(reversed(self._alias_dict), reversed(self.data))
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
- return self.data == other.data and self._alias_dict == other._alias_dict
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
- new._alias_dict.update(other._alias_dict)
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 = AliasDict(other)
193
+ new = type(self)(other)
168
194
  new.update(self.data)
169
- new._alias_dict.update(self._alias_dict)
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._alias_dict.update(other._alias_dict)
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.1.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>
@@ -28,9 +28,9 @@ Multi-key dictionary, supports adding and manipulating key-aliases pointing to s
28
28
  ## How to use
29
29
 
30
30
  - initialize with aliases
31
- <br>(one-liner with <i>aliases</i> dict mapping <i>key</i> to list of <i>aliases</i>)
31
+ <br>(one-liner with <i>aliases</i> dict mapping <i>original key</i> to <i>alias keys</i>)
32
32
  ```python
33
- ad = AliasDict({"a": 1, "b": 2}, aliases={"a": ["aa", "aaa"], "b": ["bb"]})
33
+ ad = AliasDict({"a": 1, "b": 2}, aliases={"a": ["aa", "aaa"], "b": "bb"})
34
34
  assert ad["a"] == ad["aa"] == ad["aaa"] == 1
35
35
  assert ad["b"] == ad["bb"] == 2
36
36
  ```
@@ -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,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,,
@@ -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
@@ -1,8 +0,0 @@
1
- aldict-1.1.1.dist-info/METADATA,sha256=nRJAZ0Ra2gbzejZufFJxyp8HAftTQ5I63zJYrdP7UIk,4882
2
- aldict-1.1.1.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
- aldict-1.1.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
- aldict-1.1.1.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.1.dist-info/RECORD,,