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 CHANGED
@@ -1,3 +1,6 @@
1
1
  from .alias_dict import AliasDict as AliasDict
2
2
  from .exception import AliasError as AliasError
3
3
  from .exception import AliasValueError as AliasValueError
4
+
5
+ __version__ = "1.1.0"
6
+ __all__ = ["AliasDict", "AliasError", "AliasValueError"]
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
- """Custom Dict class supporting key-aliases pointing to shared values"""
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
- super().__init__(**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)
12
22
 
13
23
  def add_alias(self, key, *aliases):
14
- """Adds one or more aliases to specified key in the dictionary"""
15
- if key not in self.data.keys():
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
- for alias in aliases:
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
- """Removes one or more aliases"""
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
- """Removes all aliases"""
58
+ """Remove all aliases."""
32
59
  self._alias_dict.clear()
33
60
 
34
61
  def aliases(self):
35
- """Returns all aliases present in the dictionary"""
62
+ """Return all aliases."""
36
63
  return self._alias_dict.keys()
37
64
 
38
- def aliased_keys(self):
39
- """Returns a dictview of all keys with their corresponding aliases"""
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
- """Returns all keys"""
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
- """Returns all keys and aliases"""
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
- """Returns all values"""
94
+ """Return all values."""
55
95
  return self.data.values()
56
96
 
57
97
  def items(self):
58
- """Returns a dictview with all items (including alias/value tuples)"""
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
- """Returns the length of the original dictionary (without aliases)"""
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.keys())
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 AttributeError as e:
72
- raise KeyError(key) from e
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
- self._alias_dict = {k: v for k, v in self._alias_dict.items() if v != key}
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 set(self.keys())
133
+ return item in self.data or item in self._alias_dict
91
134
 
92
135
  def __iter__(self):
93
- for item in self.keys():
94
- yield item
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
- raise TypeError(f"{other} is not an AliasDict")
150
+ return NotImplemented
102
151
  return self.data == other.data and self._alias_dict == other._alias_dict
103
152
 
104
- def __hash__(self):
105
- return hash((self.data, self._alias_dict))
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
1
+ Metadata-Version: 2.4
2
2
  Name: aldict
3
- Version: 1.0.2
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
- Requires-Python: >=3.11
9
- Description-Content-Type: text/markdown
6
+ Author-Email: kaliv0 <kaloyan.ivanov88@gmail.com>
10
7
  License-File: LICENSE
11
- Provides-Extra: dev
12
- Requires-Dist: pytest>=8.3.4; extra == "dev"
13
- Requires-Dist: ruff>=0.8.3; extra == "dev"
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
  [![tests](https://img.shields.io/github/actions/workflow/status/kaliv0/aldict/ci.yml)](https://github.com/kaliv0/aldict/actions/workflows/ci.yml)
25
- ![Python 3.x](https://img.shields.io/badge/python-^3.11-blue?style=flat-square&logo=Python&logoColor=white)
20
+ ![Python 3.x](https://img.shields.io/badge/python-^3.10-blue?style=flat-square&logo=Python&logoColor=white)
26
21
  [![PyPI](https://img.shields.io/pypi/v/aldict.svg)](https://pypi.org/project/aldict/)
27
22
  [![License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](https://github.com/kaliv0/aldict/blob/main/LICENSE)
28
23
  [![Downloads](https://static.pepy.tech/badge/aldict)](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
- assert ad["a"] == ad["aa"] == 1
42
- assert ad["b"] == ad["bb"] == ad["Bbb"] == 2
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 parameters)
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
- - aliased_keys
82
+ - keys_with_aliases
76
83
  <br>(read <i>keys</i> with corresponding <i>alias(es)</i>)
77
84
  ```python
78
- assert dict(ad.aliased_keys()) == {'a': ['aa', 'ab', 'A'], 'b': ['bb', 'B']}
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
- ```shell
92
- dict_keys(['x', 'y', 'Xx', 'Yy', 'xyz'])
93
- dict_values([10, 20])
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,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: pdm-backend (2.4.6)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+
3
+ [gui_scripts]
4
+
@@ -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