@@ -1501,6 +1501,12 @@ def test_delimiters(self):
15011501 self .assertEqual (dialect .delimiter , ',' )
15021502 self .assertEqual (dialect .quotechar , '"' )
15031503
1504+ def test_sniff_regex_backtracking (self ):
1505+ # gh-109638: this artificial sample used to take minutes.
1506+ sniffer = csv .Sniffer ()
1507+ sample = '"",' * 100 + '"' * 100 + '0' + '"' * 100 + '0'
1508+ self .assertEqual (sniffer .sniff (sample ).delimiter , ',' )
1509+
15041510 def test_doublequote (self ):
15051511 sniffer = csv .Sniffer ()
15061512 dialect = sniffer .sniff (self .header1 )
@@ -1514,6 +1520,81 @@ def test_doublequote(self):
15141520 dialect = sniffer .sniff (self .sample9 )
15151521 self .assertTrue (dialect .doublequote )
15161522
1523+ def test_doublequote_without_delimiter (self ):
1524+ sniffer = csv .Sniffer ()
1525+ for quotechar in ('"' , "'" ):
1526+ with self .subTest (quotechar = quotechar ):
1527+ self .assertEqual (
1528+ sniffer ._guess_quote_and_delimiter (
1529+ f'{ quotechar } a{ quotechar } { quotechar } b{ quotechar } ' ,
1530+ None ,
1531+ ),
1532+ (quotechar , True , '' , 0 ),
1533+ )
1534+ self .assertEqual (
1535+ sniffer ._guess_quote_and_delimiter (
1536+ f'{ quotechar } ab{ quotechar } ' ,
1537+ None ,
1538+ ),
1539+ (quotechar , False , '' , 0 ),
1540+ )
1541+ self .assertEqual (
1542+ sniffer ._guess_quote_and_delimiter (
1543+ f'{ quotechar } a{ quotechar } \n '
1544+ f'x{ quotechar } { quotechar } { quotechar } { quotechar } x' ,
1545+ None ,
1546+ ),
1547+ (quotechar , False , '' , 0 ),
1548+ )
1549+
1550+ def test_doublequote_with_carriage_return (self ):
1551+ sniffer = csv .Sniffer ()
1552+ for record_delimiter in ('\r \n ' , '\r ' ):
1553+ for quotechar in ('"' , "'" ):
1554+ with self .subTest (
1555+ record_delimiter = record_delimiter ,
1556+ quotechar = quotechar ,
1557+ ):
1558+ data = (
1559+ f'x,{ quotechar } plain{ quotechar } { record_delimiter } '
1560+ f'{ quotechar } a{ quotechar } { quotechar } b{ quotechar } ,y'
1561+ f'{ record_delimiter } '
1562+ )
1563+ self .assertEqual (
1564+ sniffer ._guess_quote_and_delimiter (data , None ),
1565+ (quotechar , True , ',' , 0 ),
1566+ )
1567+ self .assertIs (sniffer .sniff (data ).doublequote , True )
1568+
1569+ def test_doublequote_across_quoted_fields (self ):
1570+ sniffer = csv .Sniffer ()
1571+ for delimiter in (',' , ' ' , '' ):
1572+ for quotechar in ('"' , "'" ):
1573+ with self .subTest (
1574+ delimiter = delimiter ,
1575+ quotechar = quotechar ,
1576+ ):
1577+ separator = delimiter or '\n '
1578+ data = separator .join ((
1579+ f'{ quotechar } { separator } { quotechar } ' ,
1580+ quotechar * 2 ,
1581+ f'{ quotechar } { separator } { quotechar } ' ,
1582+ ))
1583+ result = sniffer ._guess_quote_and_delimiter (data , None )
1584+ self .assertEqual (result [0 ], quotechar )
1585+ self .assertIs (result [1 ], False )
1586+ self .assertEqual (result [2 ], delimiter )
1587+ if delimiter :
1588+ self .assertIs (
1589+ sniffer .sniff (data ).doublequote ,
1590+ False ,
1591+ )
1592+ self .assertFalse (sniffer ._detect_doublequote (
1593+ f'a{ quotechar } b{ separator } { data } ' ,
1594+ delimiter ,
1595+ quotechar ,
1596+ ))
1597+
15171598 def test_guess_delimiter_crlf_not_chosen (self ):
15181599 # Ensure that we pick the real delimiter ("|") over "\r" in a tie.
15191600 sniffer = csv .Sniffer ()
0 commit comments