Coverage for src/toolbox_python/dictionaries.py: 100%

18 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-24 10:34 +0000

1# ============================================================================ # 

2# # 

3# Title : Dictionaries # 

4# Purpose : Manipulate and enhance dictionaries. # 

5# # 

6# ============================================================================ # 

7 

8 

9# ---------------------------------------------------------------------------- # 

10# # 

11# Overview #### 

12# # 

13# ---------------------------------------------------------------------------- # 

14 

15 

16# ---------------------------------------------------------------------------- # 

17# Description #### 

18# ---------------------------------------------------------------------------- # 

19 

20 

21""" 

22!!! note "Summary" 

23 The `dictionaries` module is used how to manipulate and enhance Python dictionaries. 

24!!! abstract "Details" 

25 Note that functions in this module will only take-in and manipulate existing `#!py dict` objects, and also output `#!py dict` objects. It will not sub-class the base `#!py dict` object, or create new '`#!py dict`-like' objects. It will always maintain pure python types at it's core. 

26""" 

27 

28 

29# ---------------------------------------------------------------------------- # 

30# # 

31# Setup #### 

32# # 

33# ---------------------------------------------------------------------------- # 

34 

35 

36# ---------------------------------------------------------------------------- # 

37# Imports #### 

38# ---------------------------------------------------------------------------- # 

39 

40 

41# ## Python Third Party Imports ---- 

42from typeguard import typechecked 

43 

44# ## Local First Party Imports ---- 

45from toolbox_python.collection_types import dict_any, dict_str_any, str_list 

46 

47 

48# ---------------------------------------------------------------------------- # 

49# Exports #### 

50# ---------------------------------------------------------------------------- # 

51 

52__all__: str_list = ["dict_reverse_keys_and_values"] 

53 

54 

55# ---------------------------------------------------------------------------- # 

56# # 

57# Functions #### 

58# # 

59# ---------------------------------------------------------------------------- # 

60 

61 

62@typechecked 

63def dict_reverse_keys_and_values( 

64 dictionary: dict_any, 

65) -> dict_str_any: 

66 """ 

67 !!! note "Summary" 

68 Take the `key` and `values` of a dictionary, and reverse them. 

69 

70 ???+ info "Details" 

71 This process is simple enough if the `values` are atomic types, like `#!py str`, `#!py int`, or `#!py float` types. But it is a little more tricky when the `values` are more complex types, like `#!py list` or `#!py dict`; here we need to use some recursion. 

72 

73 Params: 

74 dictionary (dict_any): 

75 The input `#!py dict` that you'd like to have the `keys` and `values` switched. 

76 

77 Raises: 

78 TypeError: If any of the inputs parsed to the parameters of this function are not the correct type. Uses the [`@typeguard.typechecked`](https://typeguard.readthedocs.io/en/stable/api.html#typeguard.typechecked) decorator. 

79 KeyError: When there are duplicate `values` being coerced to `keys` in the new dictionary. Raised because a Python `#!py dict` cannot have duplicate keys of the same value. 

80 

81 Returns: 

82 output_dict (dict_str_int): 

83 The updated `#!py dict`. 

84 

85 ???+ example "Examples" 

86 

87 ```{.py .python linenums="1" title="Set up"} 

88 >>> # Imports 

89 >>> from toolbox_python.dictionaries import dict_reverse_keys_and_values 

90 >>> 

91 >>> # Basic dictionary 

92 >>> dict_basic = { 

93 ... "a": 1, 

94 ... "b": 2, 

95 ... "c": 3, 

96 ... } 

97 >>> 

98 >>> # Dictionary with iterables 

99 >>> dict_iterables = { 

100 ... "a": ["1", "2", "3"], 

101 ... "b": [4, 5, 6], 

102 ... "c": ("7", "8", "9"), 

103 ... "d": (10, 11, 12), 

104 ... } 

105 >>> 

106 >>> # Dictionary with iterables and duplicates 

107 >>> dict_iterables_with_duplicates = { 

108 ... "a": [1, 2, 3], 

109 ... "b": [4, 2, 5], 

110 ... } 

111 >>> 

112 >>> # Dictionary with sub-dictionaries 

113 >>> dict_with_dicts = { 

114 ... "a": { 

115 ... "aa": 11, 

116 ... "bb": 22, 

117 ... "cc": 33, 

118 ... }, 

119 ... "b": { 

120 ... "dd": [1, 2, 3], 

121 ... "ee": ("4", "5", "6"), 

122 ... }, 

123 ... } 

124 ``` 

125 

126 ```{.py .python linenums="1" title="Example 1: Reverse one-for-one"} 

127 >>> print(dict_reverse_keys_and_values(dict_basic)) 

128 ``` 

129 <div class="result" markdown> 

130 ```{.sh .shell title="Terminal"} 

131 { 

132 "1": "a", 

133 "2": "b", 

134 "3": "c", 

135 } 

136 ``` 

137 !!! success "Conclusion: Successful conversion." 

138 !!! observation "Notice here that the original values were type `#!py int`, but here they have been converted to `#!py str`. This is because `#!py dict` keys should ideally only be `#!py str` type." 

139 </div> 

140 

141 ```{.py .python linenums="1" title="Example 2: Reverse dictionary containing iterables in `values`"} 

142 >>> print(dict_reverse_keys_and_values(dict_iterables)) 

143 ``` 

144 <div class="result" markdown> 

145 ```{.sh .shell title="Terminal"} 

146 { 

147 "1": "a", 

148 "2": "a", 

149 "3": "a", 

150 "4": "b", 

151 "5": "b", 

152 "6": "b", 

153 "7": "c", 

154 "8": "c", 

155 "9": "c", 

156 "10": "d", 

157 "11": "d", 

158 "12": "d", 

159 } 

160 ``` 

161 !!! success "Conclusion: Successful conversion." 

162 !!! observation "Notice here how it has 'flattened' the iterables in the `values` in to individual keys, and assigned the original `key` to multiple keys. They keys have again been coerced to `#!py str` type." 

163 </div> 

164 

165 ```{.py .python linenums="1" title="Example 3: Dictionary with iterables, raise error when `key` already exists"} 

166 >>> print(dict_reverse_keys_and_values(dict_iterables_with_duplicates)) 

167 ``` 

168 <div class="result" markdown> 

169 ```{.sh .shell title="Terminal"} 

170 KeyError: Key already existing. 

171 Cannot update `output_dict` with new elements: {2: 'b'} 

172 Because the key is already existing for: {'2': 'a'} 

173 Full `output_dict` so far: 

174 {'1': 'a', '2': 'a', '3': 'a', '4': 'b'} 

175 ``` 

176 !!! failure "Conclusion: Failed conversion." 

177 !!! observation "Here, in the second element of the dictionary (`#!py "b"`), there is a duplicate value `#!py 2` which is already existing in the first element of the dictionary (`#!py "a"`). So, we would expect to see an error.<br>Remember, a Python `#!py dict` object _cannot_ contain duplicate keys. They must always be unique." 

178 </div> 

179 

180 ```{.py .python linenums="1" title="Example 4: Dictionary with embedded dictionaries"} 

181 >>> print(dict_reverse_keys_and_values(dict_with_dicts)) 

182 ``` 

183 <div class="result" markdown> 

184 ```{.sh .shell title="Terminal"} 

185 { 

186 "1": "a", 

187 "2": "a", 

188 "3": "a", 

189 "4": "b", 

190 "5": "b", 

191 "6": "b", 

192 "7": "c", 

193 "8": "c", 

194 "9": "c", 

195 "10": "d", 

196 "11": "d", 

197 "12": "d", 

198 } 

199 ``` 

200 !!! success "Conclusion: Successful conversion." 

201 !!! observation "Here, the process would be to run a recursive process when it recognises that any `value` is a `#!py dict` object. So long as there are no duplicate values in any of the contained `#!py dict`'s, the resulting output will be a big, flat dictionary." 

202 </div> 

203 """ 

204 output_dict: dict_str_any = dict() 

205 for key, value in dictionary.items(): 

206 if isinstance(value, (str, int, float)): 

207 output_dict[str(value)] = key 

208 elif isinstance(value, (tuple, list)): 

209 for elem in value: 

210 if str(elem) in output_dict.keys(): 

211 raise KeyError( 

212 f"Key already existing.\n" 

213 f"Cannot update `output_dict` with new elements: { {elem: key} }\n" 

214 f"Because the key is already existing for: { {new_key: new_value for (new_key, new_value) in output_dict.items() if new_key==str(elem)} }\n" 

215 f"Full `output_dict` so far:\n{output_dict}" 

216 ) 

217 output_dict[str(elem)] = key 

218 elif isinstance(value, dict): 

219 interim_dict: dict_str_any = dict_reverse_keys_and_values(value) 

220 output_dict = { 

221 **output_dict, 

222 **interim_dict, 

223 } 

224 return output_dict