33use std:: fmt:: Write ;
44use std:: ops:: Deref ;
55
6+ use ruff_python_ast:: str:: Quote ;
67use ruff_python_ast:: {
78 self as ast, Alias , AnyStringFlags , ArgOrKeyword , BoolOp , BytesLiteralFlags , CmpOp ,
89 Comprehension , ConversionFlag , DebugText , ExceptHandler , Expr , Identifier , MatchCase , Operator ,
@@ -67,6 +68,8 @@ pub struct Generator<'a> {
6768 indent : & ' a Indentation ,
6869 /// The line ending to use.
6970 line_ending : LineEnding ,
71+ /// Preferred quote style to use. For more info see [`Generator::with_preferred_quote`].
72+ preferred_quote : Option < Quote > ,
7073 buffer : String ,
7174 indent_depth : usize ,
7275 num_newlines : usize ,
@@ -78,6 +81,7 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> {
7881 Self {
7982 indent : stylist. indentation ( ) ,
8083 line_ending : stylist. line_ending ( ) ,
84+ preferred_quote : None ,
8185 buffer : String :: new ( ) ,
8286 indent_depth : 0 ,
8387 num_newlines : 0 ,
@@ -92,6 +96,7 @@ impl<'a> Generator<'a> {
9296 // Style preferences.
9397 indent,
9498 line_ending,
99+ preferred_quote : None ,
95100 // Internal state.
96101 buffer : String :: new ( ) ,
97102 indent_depth : 0 ,
@@ -100,6 +105,16 @@ impl<'a> Generator<'a> {
100105 }
101106 }
102107
108+ /// Set a preferred quote style for generated source code.
109+ ///
110+ /// - If [`None`], the generator will attempt to preserve the existing quote style whenever possible.
111+ /// - If [`Some`], the generator will prefer the specified quote style, ignoring the one found in the source.
112+ #[ must_use]
113+ pub fn with_preferred_quote ( mut self , quote : Option < Quote > ) -> Self {
114+ self . preferred_quote = quote;
115+ self
116+ }
117+
103118 /// Generate source code from a [`Stmt`].
104119 pub fn stmt ( mut self , stmt : & Stmt ) -> String {
105120 self . unparse_stmt ( stmt) ;
@@ -158,7 +173,8 @@ impl<'a> Generator<'a> {
158173 return ;
159174 }
160175 }
161- let escape = AsciiEscape :: with_preferred_quote ( s, flags. quote_style ( ) ) ;
176+ let quote_style = self . preferred_quote . unwrap_or_else ( || flags. quote_style ( ) ) ;
177+ let escape = AsciiEscape :: with_preferred_quote ( s, quote_style) ;
162178 if let Some ( len) = escape. layout ( ) . len {
163179 self . buffer . reserve ( len) ;
164180 }
@@ -176,7 +192,9 @@ impl<'a> Generator<'a> {
176192 return ;
177193 }
178194 self . p ( flags. prefix ( ) . as_str ( ) ) ;
179- let escape = UnicodeEscape :: with_preferred_quote ( s, flags. quote_style ( ) ) ;
195+
196+ let quote_style = self . preferred_quote . unwrap_or_else ( || flags. quote_style ( ) ) ;
197+ let escape = UnicodeEscape :: with_preferred_quote ( s, quote_style) ;
180198 if let Some ( len) = escape. layout ( ) . len {
181199 self . buffer . reserve ( len) ;
182200 }
@@ -1506,7 +1524,9 @@ impl<'a> Generator<'a> {
15061524 self . buffer += & s;
15071525 return ;
15081526 }
1509- let escape = UnicodeEscape :: with_preferred_quote ( & s, flags. quote_style ( ) ) ;
1527+
1528+ let quote_style = self . preferred_quote . unwrap_or_else ( || flags. quote_style ( ) ) ;
1529+ let escape = UnicodeEscape :: with_preferred_quote ( & s, quote_style) ;
15101530 if let Some ( len) = escape. layout ( ) . len {
15111531 self . buffer . reserve ( len) ;
15121532 }
@@ -1531,6 +1551,9 @@ impl<'a> Generator<'a> {
15311551 flags : AnyStringFlags ,
15321552 ) {
15331553 self . p ( flags. prefix ( ) . as_str ( ) ) ;
1554+
1555+ let flags =
1556+ flags. with_quote_style ( self . preferred_quote . unwrap_or_else ( || flags. quote_style ( ) ) ) ;
15341557 self . p ( flags. quote_str ( ) ) ;
15351558 self . unparse_interpolated_string_body ( values, flags) ;
15361559 self . p ( flags. quote_str ( ) ) ;
@@ -1563,6 +1586,7 @@ impl<'a> Generator<'a> {
15631586
15641587#[ cfg( test) ]
15651588mod tests {
1589+ use ruff_python_ast:: str:: Quote ;
15661590 use ruff_python_ast:: { Mod , ModModule } ;
15671591 use ruff_python_parser:: { self , Mode , ParseOptions , parse_module} ;
15681592 use ruff_source_file:: LineEnding ;
@@ -1580,15 +1604,17 @@ mod tests {
15801604 generator. generate ( )
15811605 }
15821606
1583- /// Like [`round_trip`] but configure the [`Generator`] with the requested `indentation` and
1584- /// `line_ending` settings.
1607+ /// Like [`round_trip`] but configure the [`Generator`] with the requested
1608+ /// `indentation`, ` line_ending` and `preferred_quote ` settings.
15851609 fn round_trip_with (
15861610 indentation : & Indentation ,
15871611 line_ending : LineEnding ,
1612+ preferred_quote : Option < Quote > ,
15881613 contents : & str ,
15891614 ) -> String {
15901615 let module = parse_module ( contents) . unwrap ( ) ;
1591- let mut generator = Generator :: new ( indentation, line_ending) ;
1616+ let mut generator =
1617+ Generator :: new ( indentation, line_ending) . with_preferred_quote ( preferred_quote) ;
15921618 generator. unparse_suite ( module. suite ( ) ) ;
15931619 generator. generate ( )
15941620 }
@@ -1974,6 +2000,7 @@ if True:
19742000 round_trip_with(
19752001 & Indentation :: new( " " . to_string( ) ) ,
19762002 LineEnding :: default ( ) ,
2003+ None ,
19772004 r"
19782005if True:
19792006 pass
@@ -1991,6 +2018,7 @@ if True:
19912018 round_trip_with(
19922019 & Indentation :: new( " " . to_string( ) ) ,
19932020 LineEnding :: default ( ) ,
2021+ None ,
19942022 r"
19952023if True:
19962024 pass
@@ -2008,6 +2036,7 @@ if True:
20082036 round_trip_with(
20092037 & Indentation :: new( "\t " . to_string( ) ) ,
20102038 LineEnding :: default ( ) ,
2039+ None ,
20112040 r"
20122041if True:
20132042 pass
@@ -2029,6 +2058,7 @@ if True:
20292058 round_trip_with(
20302059 & Indentation :: default ( ) ,
20312060 LineEnding :: Lf ,
2061+ None ,
20322062 "if True:\n print(42)" ,
20332063 ) ,
20342064 "if True:\n print(42)" ,
@@ -2038,6 +2068,7 @@ if True:
20382068 round_trip_with(
20392069 & Indentation :: default ( ) ,
20402070 LineEnding :: CrLf ,
2071+ None ,
20412072 "if True:\n print(42)" ,
20422073 ) ,
20432074 "if True:\r \n print(42)" ,
@@ -2047,9 +2078,32 @@ if True:
20472078 round_trip_with(
20482079 & Indentation :: default ( ) ,
20492080 LineEnding :: Cr ,
2081+ None ,
20502082 "if True:\n print(42)" ,
20512083 ) ,
20522084 "if True:\r print(42)" ,
20532085 ) ;
20542086 }
2087+
2088+ #[ test_case:: test_case( r#""'hello'""# , r#""'hello'""# , Quote :: Single ; "basic str ignored" ) ]
2089+ #[ test_case:: test_case( r#"b"'hello'""# , r#"b"'hello'""# , Quote :: Single ; "basic bytes ignored" ) ]
2090+ #[ test_case:: test_case( "'hello'" , r#""hello""# , Quote :: Double ; "basic str double" ) ]
2091+ #[ test_case:: test_case( r#""hello""# , "'hello'" , Quote :: Single ; "basic str single" ) ]
2092+ #[ test_case:: test_case( "b'hello'" , r#"b"hello""# , Quote :: Double ; "basic bytes double" ) ]
2093+ #[ test_case:: test_case( r#"b"hello""# , "b'hello'" , Quote :: Single ; "basic bytes single" ) ]
2094+ #[ test_case:: test_case( r#""hello""# , r#""hello""# , Quote :: Double ; "remain str double" ) ]
2095+ #[ test_case:: test_case( "'hello'" , "'hello'" , Quote :: Single ; "remain str single" ) ]
2096+ #[ test_case:: test_case( "x: list['str']" , r#"x: list["str"]"# , Quote :: Double ; "type ann double" ) ]
2097+ #[ test_case:: test_case( r#"x: list["str"]"# , "x: list['str']" , Quote :: Single ; "type ann single" ) ]
2098+ #[ test_case:: test_case( "f'hello'" , r#"f"hello""# , Quote :: Double ; "basic fstring double" ) ]
2099+ #[ test_case:: test_case( r#"f"hello""# , "f'hello'" , Quote :: Single ; "basic fstring single" ) ]
2100+ fn preferred_quote ( inp : & str , out : & str , quote : Quote ) {
2101+ let got = round_trip_with (
2102+ & Indentation :: default ( ) ,
2103+ LineEnding :: default ( ) ,
2104+ Some ( quote) ,
2105+ inp,
2106+ ) ;
2107+ assert_eq ! ( got, out) ;
2108+ }
20552109}
0 commit comments