@@ -460,6 +460,88 @@ def test_read_quoting(self):
460460 self ._read_test (['1\\ .5,\\ .5,"\\ .5"' ], [[1.5 , 0.5 , ".5" ]],
461461 quoting = csv .QUOTE_STRINGS , escapechar = '\\ ' )
462462
463+ def test_read_converter (self ):
464+ def converter (index , field ):
465+ calls .append ((index , field ))
466+ return types [index ](field )
467+
468+ types = [str , int , complex ]
469+ calls = []
470+ self ._read_test (['spam,42,1j' ], [['spam' , 42 , 1j ]],
471+ quoting = csv .QUOTE_NONNUMERIC , converter = converter )
472+ self .assertEqual (calls , [(0 , 'spam' ), (1 , '42' ), (2 , '1j' )])
473+
474+ # The index is the position in the record and is reset for each record.
475+ types = [int ] * 3
476+ calls = []
477+ self ._read_test (['1,2,3' , '4,5' , '6' ], [[1 , 2 , 3 ], [4 , 5 ], [6 ]],
478+ quoting = csv .QUOTE_STRINGS , converter = converter )
479+ self .assertEqual ([index for index , field in calls ],
480+ [0 , 1 , 2 , 0 , 1 , 0 ])
481+
482+ # Quoted and empty fields are not converted.
483+ types = [str ] * 3
484+ calls = []
485+ self ._read_test (['"spam",,42' ], [['spam' , '' , '42' ]],
486+ quoting = csv .QUOTE_NONNUMERIC , converter = converter )
487+ self .assertEqual (calls , [(2 , '42' )])
488+
489+ # Other quoting modes do not convert at all.
490+ self ._read_test (['1,2' ], [['1' , '2' ]],
491+ converter = lambda index , field : int (field ))
492+ self ._read_test (['1,2' ], [['1' , '2' ]],
493+ quoting = csv .QUOTE_ALL ,
494+ converter = lambda index , field : int (field ))
495+
496+ # None means the default conversion.
497+ self ._read_test (['1,2' ], [[1.0 , 2.0 ]],
498+ quoting = csv .QUOTE_NONNUMERIC , converter = None )
499+
500+ def test_read_converter_errors (self ):
501+ with self .assertRaisesRegex (TypeError , 'must be callable or None' ):
502+ csv .reader ([], converter = 'int' )
503+ with self .assertRaises (ZeroDivisionError ):
504+ self ._read_test (['1,2' ], [], quoting = csv .QUOTE_NONNUMERIC ,
505+ converter = lambda index , field : 1 / 0 )
506+ # A one-argument callable does not fit.
507+ with self .assertRaises (TypeError ):
508+ self ._read_test (['1,2' ], [], quoting = csv .QUOTE_NONNUMERIC ,
509+ converter = float )
510+
511+ def test_write_formatter (self ):
512+ def formatter (index , value ):
513+ calls .append ((index , value ))
514+ return format (value , '.2f' ) if index == 2 else str (value )
515+
516+ calls = []
517+ self ._write_test (['a' , 1 , 0.0 , 3.14159 ], 'a,1,0.00,3.14159' ,
518+ formatter = formatter )
519+ self .assertEqual (calls , [(1 , 1 ), (2 , 0.0 ), (3 , 3.14159 )])
520+
521+ # Strings and None are not passed to the formatter.
522+ calls = []
523+ self ._write_test ([0 , 'a' , None , 3 ], '<0>,a,,<3>' ,
524+ formatter = lambda index , value :
525+ calls .append (value ) or f'<{ index } >' )
526+ self .assertEqual (calls , [0 , 3 ])
527+
528+ # Quoting is decided by the original value, not by the result.
529+ self ._write_test ([1.5 , 'a' ], '1.50,"a"' , quoting = csv .QUOTE_NONNUMERIC ,
530+ formatter = lambda index , value : format (value , '.2f' ))
531+
532+ # None means str().
533+ self ._write_test ([1 , 2 ], '1,2' , formatter = None )
534+
535+ def test_write_formatter_errors (self ):
536+ with self .assertRaisesRegex (TypeError , 'must be callable or None' ):
537+ csv .writer (StringIO (), formatter = 'str' )
538+ with self .assertRaisesRegex (csv .Error , 'must return a string' ):
539+ self ._write_test ([1 ], '' , formatter = lambda index , value : index )
540+ self ._write_error_test (ZeroDivisionError , [1 ],
541+ formatter = lambda index , value : 1 / 0 )
542+ # A one-argument callable does not fit.
543+ self ._write_error_test (TypeError , [1 ], formatter = repr )
544+
463545 def test_read_skipinitialspace (self ):
464546 self ._read_test (['no space, space, spaces,\t tab' ],
465547 [['no space' , 'space' , 'spaces' , '\t tab' ]],
0 commit comments