Skip to content

Commit cc0ed0e

Browse files
committed
added casted dict class for easy automatic casting
1 parent 271a105 commit cc0ed0e

File tree

1 file changed

+179
-0
lines changed

1 file changed

+179
-0
lines changed

python_utils/containers.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
from __future__ import annotations
2+
3+
import abc
4+
from typing import Any
5+
from typing import Generator
6+
7+
from . import types
8+
9+
KT = types.TypeVar('KT')
10+
VT = types.TypeVar('VT')
11+
DT = types.Dict[KT, VT]
12+
KT_cast = types.Optional[types.Callable[[Any], KT]]
13+
VT_cast = types.Optional[types.Callable[[Any], VT]]
14+
15+
# Using types.Union instead of | since Python 3.7 doesn't fully support it
16+
DictUpdateArgs = types.Union[
17+
types.Mapping,
18+
types.Iterable[types.Union[types.Tuple[Any, Any], types.Mapping]],
19+
]
20+
21+
22+
class CastedDictBase(types.Dict[KT, VT], abc.ABC):
23+
_key_cast: KT_cast
24+
_value_cast: VT_cast
25+
26+
def __init__(
27+
self,
28+
key_cast: KT_cast = None,
29+
value_cast: VT_cast = None,
30+
*args,
31+
**kwargs
32+
) -> None:
33+
self._value_cast = value_cast
34+
self._key_cast = key_cast
35+
self.update(*args, **kwargs)
36+
37+
def update(
38+
self,
39+
*args: DictUpdateArgs,
40+
**kwargs
41+
) -> None:
42+
if args:
43+
kwargs.update(*args)
44+
45+
if kwargs:
46+
for key, value in kwargs.items():
47+
self[key] = value
48+
49+
def __setitem__(self, key: Any, value: Any) -> None:
50+
if self._key_cast is not None:
51+
key = self._key_cast(key)
52+
53+
return super().__setitem__(key, value)
54+
55+
56+
class CastedDict(CastedDictBase):
57+
'''
58+
Custom dictionary that casts keys and values to the specified typing.
59+
60+
Note that you can specify the types for mypy and type hinting with:
61+
CastedDict[int, int](int, int)
62+
63+
>>> d = CastedDict(int, int)
64+
>>> d[1] = 2
65+
>>> d['3'] = '4'
66+
>>> d.update({'5': '6'})
67+
>>> d.update([('7', '8')])
68+
>>> d
69+
{1: 2, 3: 4, 5: 6, 7: 8}
70+
>>> list(d.keys())
71+
[1, 3, 5, 7]
72+
>>> list(d)
73+
[1, 3, 5, 7]
74+
>>> list(d.values())
75+
[2, 4, 6, 8]
76+
>>> list(d.items())
77+
[(1, 2), (3, 4), (5, 6), (7, 8)]
78+
>>> d[3]
79+
4
80+
81+
# Casts are optional and can be disabled by passing None as the cast
82+
>>> d = CastedDict()
83+
>>> d[1] = 2
84+
>>> d['3'] = '4'
85+
>>> d.update({'5': '6'})
86+
>>> d.update([('7', '8')])
87+
>>> d
88+
{1: 2, '3': '4', '5': '6', '7': '8'}
89+
'''
90+
91+
def __setitem__(self, key, value):
92+
if self._value_cast is not None:
93+
value = self._value_cast(value)
94+
95+
super().__setitem__(key, value)
96+
97+
98+
class LazyCastedDict(CastedDictBase):
99+
'''
100+
Custom dictionary that casts keys and lazily casts values to the specified
101+
typing. Note that the values are cast only when they are accessed and
102+
are not cached between executions.
103+
104+
Note that you can specify the types for mypy and type hinting with:
105+
LazyCastedDict[int, int](int, int)
106+
107+
>>> d = LazyCastedDict(int, int)
108+
>>> d[1] = 2
109+
>>> d['3'] = '4'
110+
>>> d.update({'5': '6'})
111+
>>> d.update([('7', '8')])
112+
>>> d
113+
{1: 2, 3: '4', 5: '6', 7: '8'}
114+
>>> list(d.keys())
115+
[1, 3, 5, 7]
116+
>>> list(d)
117+
[1, 3, 5, 7]
118+
>>> list(d.values())
119+
[2, 4, 6, 8]
120+
>>> list(d.items())
121+
[(1, 2), (3, 4), (5, 6), (7, 8)]
122+
>>> d[3]
123+
4
124+
125+
# Casts are optional and can be disabled by passing None as the cast
126+
>>> d = LazyCastedDict()
127+
>>> d[1] = 2
128+
>>> d['3'] = '4'
129+
>>> d.update({'5': '6'})
130+
>>> d.update([('7', '8')])
131+
>>> d
132+
{1: 2, '3': '4', '5': '6', '7': '8'}
133+
>>> list(d.keys())
134+
[1, '3', '5', '7']
135+
>>> list(d.values())
136+
[2, '4', '6', '8']
137+
138+
>>> list(d.items())
139+
[(1, 2), ('3', '4'), ('5', '6'), ('7', '8')]
140+
>>> d['3']
141+
'4'
142+
'''
143+
144+
def __setitem__(self, key, value):
145+
if self._key_cast is not None:
146+
key = self._key_cast(key)
147+
148+
super().__setitem__(key, value)
149+
150+
def __getitem__(self, key) -> VT:
151+
if self._key_cast is not None:
152+
key = self._key_cast(key)
153+
154+
value = super().__getitem__(key)
155+
156+
if self._value_cast is not None:
157+
value = self._value_cast(value)
158+
159+
return value
160+
161+
def items(self) -> Generator[tuple[KT, VT], None, None]: # type: ignore
162+
if self._value_cast is None:
163+
yield from super().items()
164+
else:
165+
for key, value in super().items():
166+
yield key, self._value_cast(value)
167+
168+
def values(self) -> Generator[VT, None, None]: # type: ignore
169+
if self._value_cast is None:
170+
yield from super().values()
171+
else:
172+
for value in super().values():
173+
yield self._value_cast(value)
174+
175+
176+
if __name__ == '__main__':
177+
import doctest
178+
179+
doctest.testmod()

0 commit comments

Comments
 (0)