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
« 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# ============================================================================ #
9# ---------------------------------------------------------------------------- #
10# #
11# Overview ####
12# #
13# ---------------------------------------------------------------------------- #
16# ---------------------------------------------------------------------------- #
17# Description ####
18# ---------------------------------------------------------------------------- #
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"""
29# ---------------------------------------------------------------------------- #
30# #
31# Setup ####
32# #
33# ---------------------------------------------------------------------------- #
36# ---------------------------------------------------------------------------- #
37# Imports ####
38# ---------------------------------------------------------------------------- #
41# ## Python Third Party Imports ----
42from typeguard import typechecked
44# ## Local First Party Imports ----
45from toolbox_python.collection_types import dict_any, dict_str_any, str_list
48# ---------------------------------------------------------------------------- #
49# Exports ####
50# ---------------------------------------------------------------------------- #
52__all__: str_list = ["dict_reverse_keys_and_values"]
55# ---------------------------------------------------------------------------- #
56# #
57# Functions ####
58# #
59# ---------------------------------------------------------------------------- #
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.
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.
73 Params:
74 dictionary (dict_any):
75 The input `#!py dict` that you'd like to have the `keys` and `values` switched.
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.
81 Returns:
82 output_dict (dict_str_int):
83 The updated `#!py dict`.
85 ???+ example "Examples"
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 ```
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>
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>
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>
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