@@ -298,7 +298,7 @@ def test_metadata_conversion(self):
298298 assert roundtrip_metadata ['complex_list' ][0 ]['name' ] == 'item1'
299299
300300 def test_metadata_with_custom_objects (self ):
301- """Test metadata conversion with custom objects that need str() fallback ."""
301+ """Test metadata conversion with custom objects using preprocessing utility ."""
302302
303303 class CustomObject :
304304 def __str__ (self ):
@@ -313,8 +313,11 @@ def __repr__(self):
313313 'nested_custom' : {'obj' : CustomObject (), 'normal' : 'value' },
314314 }
315315
316+ # Use preprocessing utility to make it serializable
317+ serializable_metadata = proto_utils .make_dict_serializable (metadata )
318+
316319 # Convert to proto
317- proto_metadata = proto_utils .ToProto .metadata (metadata )
320+ proto_metadata = proto_utils .ToProto .metadata (serializable_metadata )
318321 assert proto_metadata is not None
319322
320323 # Convert back to Python
@@ -339,7 +342,7 @@ def test_metadata_edge_cases(self):
339342 'false' : False ,
340343 'empty_string' : '' ,
341344 'unicode_string' : 'string test' ,
342- 'large_number ' : 9999999999999999 ,
345+ 'safe_number ' : 9007199254740991 , # JavaScript MAX_SAFE_INTEGER
343346 'negative_number' : - 42 ,
344347 'float_precision' : 0.123456789 ,
345348 'numeric_string' : '12345' ,
@@ -356,7 +359,156 @@ def test_metadata_edge_cases(self):
356359 assert roundtrip_metadata ['false' ] is False
357360 assert roundtrip_metadata ['empty_string' ] == ''
358361 assert roundtrip_metadata ['unicode_string' ] == 'string test'
359- assert roundtrip_metadata ['large_number ' ] == 9999999999999999
362+ assert roundtrip_metadata ['safe_number ' ] == 9007199254740991
360363 assert roundtrip_metadata ['negative_number' ] == - 42
361364 assert abs (roundtrip_metadata ['float_precision' ] - 0.123456789 ) < 1e-10
362365 assert roundtrip_metadata ['numeric_string' ] == '12345'
366+
367+ def test_make_dict_serializable (self ):
368+ """Test the make_dict_serializable utility function."""
369+
370+ class CustomObject :
371+ def __str__ (self ):
372+ return 'custom_str'
373+
374+ test_data = {
375+ 'string' : 'hello' ,
376+ 'int' : 42 ,
377+ 'float' : 3.14 ,
378+ 'bool' : True ,
379+ 'none' : None ,
380+ 'custom' : CustomObject (),
381+ 'list' : [1 , 'two' , CustomObject ()],
382+ 'tuple' : (1 , 2 , CustomObject ()),
383+ 'nested' : {'inner_custom' : CustomObject (), 'inner_normal' : 'value' },
384+ }
385+
386+ result = proto_utils .make_dict_serializable (test_data )
387+
388+ # Basic types should be unchanged
389+ assert result ['string' ] == 'hello'
390+ assert result ['int' ] == 42
391+ assert result ['float' ] == 3.14
392+ assert result ['bool' ] is True
393+ assert result ['none' ] is None
394+
395+ # Custom objects should be converted to strings
396+ assert result ['custom' ] == 'custom_str'
397+ assert result ['list' ] == [1 , 'two' , 'custom_str' ]
398+ assert result ['tuple' ] == [1 , 2 , 'custom_str' ] # tuples become lists
399+ assert result ['nested' ]['inner_custom' ] == 'custom_str'
400+ assert result ['nested' ]['inner_normal' ] == 'value'
401+
402+ def test_normalize_large_integers_to_strings (self ):
403+ """Test the normalize_large_integers_to_strings utility function."""
404+
405+ test_data = {
406+ 'small_int' : 42 ,
407+ 'large_int' : 9999999999999999999 , # > 15 digits
408+ 'negative_large' : - 9999999999999999999 ,
409+ 'float' : 3.14 ,
410+ 'string' : 'hello' ,
411+ 'list' : [123 , 9999999999999999999 , 'text' ],
412+ 'nested' : {'inner_large' : 9999999999999999999 , 'inner_small' : 100 },
413+ }
414+
415+ result = proto_utils .normalize_large_integers_to_strings (test_data )
416+
417+ # Small integers should remain as integers
418+ assert result ['small_int' ] == 42
419+ assert isinstance (result ['small_int' ], int )
420+
421+ # Large integers should be converted to strings
422+ assert result ['large_int' ] == '9999999999999999999'
423+ assert isinstance (result ['large_int' ], str )
424+ assert result ['negative_large' ] == '-9999999999999999999'
425+ assert isinstance (result ['negative_large' ], str )
426+
427+ # Other types should be unchanged
428+ assert result ['float' ] == 3.14
429+ assert result ['string' ] == 'hello'
430+
431+ # Lists should be processed recursively
432+ assert result ['list' ] == [123 , '9999999999999999999' , 'text' ]
433+
434+ # Nested dicts should be processed recursively
435+ assert result ['nested' ]['inner_large' ] == '9999999999999999999'
436+ assert result ['nested' ]['inner_small' ] == 100
437+
438+ def test_parse_string_integers_in_dict (self ):
439+ """Test the parse_string_integers_in_dict utility function."""
440+
441+ test_data = {
442+ 'regular_string' : 'hello' ,
443+ 'numeric_string_small' : '123' , # small, should stay as string
444+ 'numeric_string_large' : '9999999999999999999' , # > 15 digits, should become int
445+ 'negative_large_string' : '-9999999999999999999' ,
446+ 'float_string' : '3.14' , # not all digits, should stay as string
447+ 'mixed_string' : '123abc' , # not all digits, should stay as string
448+ 'int' : 42 ,
449+ 'list' : ['hello' , '9999999999999999999' , '123' ],
450+ 'nested' : {
451+ 'inner_large_string' : '9999999999999999999' ,
452+ 'inner_regular' : 'value' ,
453+ },
454+ }
455+
456+ result = proto_utils .parse_string_integers_in_dict (test_data )
457+
458+ # Regular strings should remain unchanged
459+ assert result ['regular_string' ] == 'hello'
460+ assert (
461+ result ['numeric_string_small' ] == '123'
462+ ) # too small, stays string
463+ assert result ['float_string' ] == '3.14' # not all digits
464+ assert result ['mixed_string' ] == '123abc' # not all digits
465+
466+ # Large numeric strings should be converted to integers
467+ assert result ['numeric_string_large' ] == 9999999999999999999
468+ assert isinstance (result ['numeric_string_large' ], int )
469+ assert result ['negative_large_string' ] == - 9999999999999999999
470+ assert isinstance (result ['negative_large_string' ], int )
471+
472+ # Other types should be unchanged
473+ assert result ['int' ] == 42
474+
475+ # Lists should be processed recursively
476+ assert result ['list' ] == ['hello' , 9999999999999999999 , '123' ]
477+
478+ # Nested dicts should be processed recursively
479+ assert result ['nested' ]['inner_large_string' ] == 9999999999999999999
480+ assert result ['nested' ]['inner_regular' ] == 'value'
481+
482+ def test_large_integer_roundtrip_with_utilities (self ):
483+ """Test large integer handling with preprocessing and post-processing utilities."""
484+
485+ original_data = {
486+ 'large_int' : 9999999999999999999 ,
487+ 'small_int' : 42 ,
488+ 'nested' : {'another_large' : 12345678901234567890 , 'normal' : 'text' },
489+ }
490+
491+ # Step 1: Preprocess to convert large integers to strings
492+ preprocessed = proto_utils .normalize_large_integers_to_strings (
493+ original_data
494+ )
495+
496+ # Step 2: Convert to proto
497+ proto_metadata = proto_utils .ToProto .metadata (preprocessed )
498+ assert proto_metadata is not None
499+
500+ # Step 3: Convert back from proto
501+ dict_from_proto = proto_utils .FromProto .metadata (proto_metadata )
502+
503+ # Step 4: Post-process to convert large integer strings back to integers
504+ final_result = proto_utils .parse_string_integers_in_dict (
505+ dict_from_proto
506+ )
507+
508+ # Verify roundtrip preserved the original data
509+ assert final_result ['large_int' ] == 9999999999999999999
510+ assert isinstance (final_result ['large_int' ], int )
511+ assert final_result ['small_int' ] == 42
512+ assert final_result ['nested' ]['another_large' ] == 12345678901234567890
513+ assert isinstance (final_result ['nested' ]['another_large' ], int )
514+ assert final_result ['nested' ]['normal' ] == 'text'
0 commit comments