18
18
19
19
import warnings
20
20
import inspect
21
+ import logging
21
22
import typing
22
23
23
24
import pytest
@@ -60,4 +61,248 @@ def test_is_struct_type():
60
61
assert not _common ._is_struct_type (typing .List [typing .Dict [str , int ]])
61
62
assert not _common ._is_struct_type (typing .List [typing .Dict [int , typing .Any ]])
62
63
assert not _common ._is_struct_type (typing .List [str ])
63
- assert not _common ._is_struct_type (typing .Dict [str , typing .Any ])
64
+ assert not _common ._is_struct_type (typing .Dict [str , typing .Any ])
65
+
66
+
67
+
68
+ @pytest .mark .parametrize (
69
+ "test_id, initial_target, update_dict, expected_target" ,
70
+ [
71
+ (
72
+ "simple_update" ,
73
+ {"a" : 1 , "b" : 2 },
74
+ {"b" : 3 , "c" : 4 },
75
+ {"a" : 1 , "b" : 3 , "c" : 4 },
76
+ ),
77
+ (
78
+ "nested_update" ,
79
+ {"a" : 1 , "b" : {"x" : 10 , "y" : 20 }},
80
+ {"b" : {"y" : 30 , "z" : 40 }, "c" : 3 },
81
+ {"a" : 1 , "b" : {"x" : 10 , "y" : 30 , "z" : 40 }, "c" : 3 },
82
+ ),
83
+ (
84
+ "add_new_nested_dict" ,
85
+ {"a" : 1 },
86
+ {"b" : {"x" : 10 , "y" : 20 }},
87
+ {"a" : 1 , "b" : {"x" : 10 , "y" : 20 }},
88
+ ),
89
+ (
90
+ "empty_target" ,
91
+ {},
92
+ {"a" : 1 , "b" : {"x" : 10 }},
93
+ {"a" : 1 , "b" : {"x" : 10 }},
94
+ ),
95
+ (
96
+ "empty_update" ,
97
+ {"a" : 1 , "b" : {"x" : 10 }},
98
+ {},
99
+ {"a" : 1 , "b" : {"x" : 10 }},
100
+ ),
101
+ (
102
+ "overwrite_non_dict_with_dict" ,
103
+ {"a" : 1 , "b" : 2 },
104
+ {"b" : {"x" : 10 }},
105
+ {"a" : 1 , "b" : {"x" : 10 }},
106
+ ),
107
+ (
108
+ "overwrite_dict_with_non_dict" ,
109
+ {"a" : 1 , "b" : {"x" : 10 }},
110
+ {"b" : 2 },
111
+ {"a" : 1 , "b" : 2 },
112
+ ),
113
+ (
114
+ "deeper_nesting" ,
115
+ {"a" : {"b" : {"c" : 1 , "d" : 2 }, "e" : 3 }},
116
+ {"a" : {"b" : {"d" : 4 , "f" : 5 }, "g" : 6 }, "h" : 7 },
117
+ {"a" : {"b" : {"c" : 1 , "d" : 4 , "f" : 5 }, "e" : 3 , "g" : 6 }, "h" : 7 },
118
+ ),
119
+ (
120
+ "different_value_types" ,
121
+ {"key1" : "string_val" , "key2" : {"nested_int" : 100 }},
122
+ {"key1" : 123 , "key2" : {"nested_list" : [1 , 2 , 3 ]}, "key3" : True },
123
+ {
124
+ "key1" : 123 ,
125
+ "key2" : {"nested_int" : 100 , "nested_list" : [1 , 2 , 3 ]},
126
+ "key3" : True ,
127
+ },
128
+ ),
129
+ (
130
+ "update_with_empty_nested_dict" , # Existing nested dict in target should not be cleared
131
+ {"a" : {"b" : 1 }},
132
+ {"a" : {}},
133
+ {"a" : {"b" : 1 }},
134
+ ),
135
+ (
136
+ "target_with_empty_nested_dict" ,
137
+ {"a" : {}},
138
+ {"a" : {"b" : 1 }},
139
+ {"a" : {"b" : 1 }},
140
+ ),
141
+ (
142
+ "key_case_alignment_check" ,
143
+ {"first_name" : "John" , "contact_info" : {"email_address" : "[email protected] " }},
144
+ {"firstName" : "Jane" , "contact_info" : {"email_address" : "[email protected] " , "phone_number" : "123" }},
145
+ {"first_name" : "Jane" , "contact_info" : {"email_address" : "[email protected] " , "phone_number" : "123" }},
146
+ )
147
+ ],
148
+ )
149
+ def test_recursive_dict_update (
150
+ test_id : str , initial_target : dict , update_dict : dict , expected_target : dict
151
+ ):
152
+ _common .recursive_dict_update (initial_target , update_dict )
153
+ assert initial_target == expected_target
154
+
155
+
156
+ @pytest .mark .parametrize (
157
+ "test_id, initial_target, update_dict, expected_target, expect_warning, expected_log_message_part" ,
158
+ [
159
+ (
160
+ "type_match_int" ,
161
+ {"a" : 1 },
162
+ {"a" : 2 },
163
+ {"a" : 2 },
164
+ False ,
165
+ "" ,
166
+ ),
167
+ (
168
+ "type_match_dict" ,
169
+ {"a" : {"b" : 1 }},
170
+ {"a" : {"b" : 2 }},
171
+ {"a" : {"b" : 2 }},
172
+ False ,
173
+ "" ,
174
+ ),
175
+ (
176
+ "type_mismatch_int_to_str" ,
177
+ {"a" : 1 },
178
+ {"a" : "hello" },
179
+ {"a" : "hello" },
180
+ True ,
181
+ "Type mismatch for key 'a'. Existing type: <class 'int'>, new type: <class 'str'>. Overwriting." ,
182
+ ),
183
+ (
184
+ "type_mismatch_dict_to_int" ,
185
+ {"a" : {"b" : 1 }},
186
+ {"a" : 100 },
187
+ {"a" : 100 },
188
+ True ,
189
+ "Type mismatch for key 'a'. Existing type: <class 'dict'>, new type: <class 'int'>. Overwriting." ,
190
+ ),
191
+ (
192
+ "type_mismatch_int_to_dict" ,
193
+ {"a" : 100 },
194
+ {"a" : {"b" : 1 }},
195
+ {"a" : {"b" : 1 }},
196
+ True ,
197
+ "Type mismatch for key 'a'. Existing type: <class 'int'>, new type: <class 'dict'>. Overwriting." ,
198
+ ),
199
+ ("add_new_key" , {"a" : 1 }, {"b" : "new" }, {"a" : 1 , "b" : "new" }, False , "" ),
200
+ ],
201
+ )
202
+ def test_recursive_dict_update_type_warnings (test_id , initial_target , update_dict , expected_target , expect_warning , expected_log_message_part , caplog ):
203
+ _common .recursive_dict_update (initial_target , update_dict )
204
+ assert initial_target == expected_target
205
+ if expect_warning :
206
+ assert len (caplog .records ) == 1
207
+ assert caplog .records [0 ].levelname == "WARNING"
208
+ assert expected_log_message_part in caplog .records [0 ].message
209
+ else :
210
+ for record in caplog .records :
211
+ if record .levelname == "WARNING" and expected_log_message_part in record .message :
212
+ pytest .fail (f"Unexpected warning logged for { test_id } : { record .message } " )
213
+
214
+
215
+ @pytest .mark .parametrize (
216
+ "test_id, target_dict, update_dict, expected_aligned_dict" ,
217
+ [
218
+ (
219
+ "simple_snake_to_camel" ,
220
+ {"first_name" : "John" , "last_name" : "Doe" },
221
+ {"firstName" : "Jane" , "lastName" : "Doe" },
222
+ {"first_name" : "Jane" , "last_name" : "Doe" },
223
+ ),
224
+ (
225
+ "simple_camel_to_snake" ,
226
+ {"firstName" : "John" , "lastName" : "Doe" },
227
+ {"first_name" : "Jane" , "last_name" : "Doe" },
228
+ {"firstName" : "Jane" , "lastName" : "Doe" },
229
+ ),
230
+ (
231
+ "nested_dict_alignment" ,
232
+ {"user_info" : {"contact_details" : {"email_address" : "" }}},
233
+ {"userInfo" : {"contactDetails" : {"emailAddress" : "[email protected] " }}},
234
+ {"user_info" : {"contact_details" : {"email_address" : "[email protected] " }}},
235
+ ),
236
+ (
237
+ "list_of_dicts_alignment" ,
238
+ {"users_list" : [{"user_id" : 0 , "user_name" : "" }]},
239
+ {"usersList" : [{"userId" : 1 , "userName" : "Alice" }]},
240
+ {"users_list" : [{"userId" : 1 , "userName" : "Alice" }]},
241
+ ),
242
+ (
243
+ "list_of_dicts_alignment_mixed_case_in_update" ,
244
+ {"users_list" : [{"user_id" : 0 , "user_name" : "" }]},
245
+ {"usersList" : [{"user_id" : 1 , "UserName" : "Alice" }]},
246
+ {"users_list" : [{"user_id" : 1 , "UserName" : "Alice" }]},
247
+ ),
248
+ (
249
+ "list_of_dicts_different_lengths_update_longer" ,
250
+ {"items_data" : [{"item_id" : 0 }]},
251
+ {"itemsData" : [{"itemId" : 1 }, {"item_id" : 2 , "itemName" : "Extra" }]},
252
+ {"items_data" : [{"itemId" : 1 }, {"item_id" : 2 , "itemName" : "Extra" }]},
253
+ ),
254
+ (
255
+ "list_of_dicts_different_lengths_target_longer" ,
256
+ {"items_data" : [{"item_id" : 0 , "item_name" : "" }, {"item_id" : 1 }]},
257
+ {"itemsData" : [{"itemId" : 10 }]},
258
+ {"items_data" : [{"itemId" : 10 }]},
259
+ ),
260
+ (
261
+ "no_matching_keys_preserves_update_case" ,
262
+ {"key_one" : 1 },
263
+ {"KEY_TWO" : 2 , "keyThree" : 3 },
264
+ {"KEY_TWO" : 2 , "keyThree" : 3 },
265
+ ),
266
+ (
267
+ "mixed_match_and_no_match" ,
268
+ {"first_name" : "John" , "age_years" : 30 },
269
+ {"firstName" : "Jane" , "AGE_YEARS" : 28 , "occupation_title" : "Engineer" },
270
+ {"first_name" : "Jane" , "age_years" : 28 , "occupation_title" : "Engineer" },
271
+ ),
272
+ (
273
+ "empty_target_dict" ,
274
+ {},
275
+ {"new_key" : "new_value" , "anotherKey" : "anotherValue" },
276
+ {"new_key" : "new_value" , "anotherKey" : "anotherValue" },
277
+ ),
278
+ (
279
+ "empty_update_dict" ,
280
+ {"existing_key" : "value" },
281
+ {},
282
+ {},
283
+ ),
284
+ (
285
+ "target_has_non_dict_value_for_nested_key" ,
286
+ {"config_settings" : 123 },
287
+ {"configSettings" : {"themeName" : "dark" }},
288
+ {"config_settings" : {"themeName" : "dark" }}, # Overwrites as per recursive_dict_update logic
289
+ ),
290
+ (
291
+ "update_has_non_dict_value_for_nested_key" ,
292
+ {"config_settings" : {"theme_name" : "light" }},
293
+ {"configSettings" : "dark_theme_string" },
294
+ {"config_settings" : "dark_theme_string" }, # Overwrites
295
+ ),
296
+ (
297
+ "deeply_nested_with_lists" ,
298
+ {"level_one" : {"list_items" : [{"item_name" : "" , "item_value" : 0 }]}},
299
+ {"levelOne" : {"listItems" : [{"itemName" : "Test" , "itemValue" : 100 }, {"itemName" : "Test2" , "itemValue" : 200 }]}},
300
+ {"level_one" : {"list_items" : [{"itemName" : "Test" , "itemValue" : 100 }, {"itemName" : "Test2" , "itemValue" : 200 }]}},
301
+ ),
302
+ ],
303
+ )
304
+ def test_align_key_case (
305
+ test_id : str , target_dict : dict , update_dict : dict , expected_aligned_dict : dict
306
+ ):
307
+ aligned_dict = _common .align_key_case (target_dict , update_dict )
308
+ assert aligned_dict == expected_aligned_dict , f"Test failed for: { test_id } "
0 commit comments