@@ -17,6 +17,7 @@ use ruff_source_file::LineEnding;
1717use super :: stylist:: { Indentation , Stylist } ;
1818
1919mod precedence {
20+ pub ( crate ) const MIN : u8 = 0 ;
2021 pub ( crate ) const NAMED_EXPR : u8 = 1 ;
2122 pub ( crate ) const ASSIGN : u8 = 3 ;
2223 pub ( crate ) const ANN_ASSIGN : u8 = 5 ;
@@ -63,13 +64,36 @@ mod precedence {
6364 pub ( crate ) const MAX : u8 = 63 ;
6465}
6566
67+ #[ derive( Default ) ]
68+ pub enum Mode {
69+ /// Ruff's default unparsing behaviour.
70+ #[ default]
71+ Default ,
72+ /// Emits same output as [`ast.unparse`](https://docs.python.org/3/library/ast.html#ast.unparse).
73+ AstUnparse ,
74+ }
75+
76+ impl Mode {
77+ /// Quote style to use.
78+ ///
79+ /// - [`Default`](`Mode::Default`): Output of `[AnyStringFlags.quote_style`].
80+ /// - [`AstUnparse`](`Mode::AstUnparse`): Always return [`Quote::Single`].
81+ #[ must_use]
82+ fn quote_style ( & self , flags : impl StringFlags ) -> Quote {
83+ match self {
84+ Self :: Default => flags. quote_style ( ) ,
85+ Self :: AstUnparse => Quote :: Single ,
86+ }
87+ }
88+ }
89+
6690pub struct Generator < ' a > {
6791 /// The indentation style to use.
6892 indent : & ' a Indentation ,
6993 /// The line ending to use.
7094 line_ending : LineEnding ,
71- /// Preferred quote style to use. For more info see [`Generator::with_preferred_quote`] .
72- preferred_quote : Option < Quote > ,
95+ /// Unparsed code style. See [`Mode`] for more info .
96+ mode : Mode ,
7397 buffer : String ,
7498 indent_depth : usize ,
7599 num_newlines : usize ,
@@ -81,7 +105,7 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> {
81105 Self {
82106 indent : stylist. indentation ( ) ,
83107 line_ending : stylist. line_ending ( ) ,
84- preferred_quote : None ,
108+ mode : Mode :: default ( ) ,
85109 buffer : String :: new ( ) ,
86110 indent_depth : 0 ,
87111 num_newlines : 0 ,
@@ -96,7 +120,7 @@ impl<'a> Generator<'a> {
96120 // Style preferences.
97121 indent,
98122 line_ending,
99- preferred_quote : None ,
123+ mode : Mode :: Default ,
100124 // Internal state.
101125 buffer : String :: new ( ) ,
102126 indent_depth : 0 ,
@@ -105,13 +129,10 @@ impl<'a> Generator<'a> {
105129 }
106130 }
107131
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.
132+ /// Sets the mode for code unparsing.
112133 #[ must_use]
113- pub fn with_preferred_quote ( mut self , quote : Option < Quote > ) -> Self {
114- self . preferred_quote = quote ;
134+ pub fn with_mode ( mut self , mode : Mode ) -> Self {
135+ self . mode = mode ;
115136 self
116137 }
117138
@@ -173,7 +194,7 @@ impl<'a> Generator<'a> {
173194 return ;
174195 }
175196 }
176- let quote_style = self . preferred_quote . unwrap_or_else ( || flags . quote_style ( ) ) ;
197+ let quote_style = self . mode . quote_style ( flags ) ;
177198 let escape = AsciiEscape :: with_preferred_quote ( s, quote_style) ;
178199 if let Some ( len) = escape. layout ( ) . len {
179200 self . buffer . reserve ( len) ;
@@ -193,7 +214,7 @@ impl<'a> Generator<'a> {
193214 }
194215 self . p ( flags. prefix ( ) . as_str ( ) ) ;
195216
196- let quote_style = self . preferred_quote . unwrap_or_else ( || flags . quote_style ( ) ) ;
217+ let quote_style = self . mode . quote_style ( flags ) ;
197218 let escape = UnicodeEscape :: with_preferred_quote ( s, quote_style) ;
198219 if let Some ( len) = escape. layout ( ) . len {
199220 self . buffer . reserve ( len) ;
@@ -1303,7 +1324,11 @@ impl<'a> Generator<'a> {
13031324 if tuple. is_empty ( ) {
13041325 self . p ( "()" ) ;
13051326 } else {
1306- group_if ! ( precedence:: TUPLE , {
1327+ let lvl = match self . mode {
1328+ Mode :: Default => precedence:: TUPLE ,
1329+ Mode :: AstUnparse => precedence:: MIN ,
1330+ } ;
1331+ group_if ! ( lvl, {
13071332 let mut first = true ;
13081333 for item in tuple {
13091334 self . p_delim( & mut first, ", " ) ;
@@ -1525,7 +1550,7 @@ impl<'a> Generator<'a> {
15251550 return ;
15261551 }
15271552
1528- let quote_style = self . preferred_quote . unwrap_or_else ( || flags . quote_style ( ) ) ;
1553+ let quote_style = self . mode . quote_style ( flags ) ;
15291554 let escape = UnicodeEscape :: with_preferred_quote ( & s, quote_style) ;
15301555 if let Some ( len) = escape. layout ( ) . len {
15311556 self . buffer . reserve ( len) ;
@@ -1552,8 +1577,8 @@ impl<'a> Generator<'a> {
15521577 ) {
15531578 self . p ( flags. prefix ( ) . as_str ( ) ) ;
15541579
1555- let flags =
1556- flags. with_quote_style ( self . preferred_quote . unwrap_or_else ( || flags . quote_style ( ) ) ) ;
1580+ let quote_style = self . mode . quote_style ( flags ) ;
1581+ let flags = flags. with_quote_style ( quote_style) ;
15571582 self . p ( flags. quote_str ( ) ) ;
15581583 self . unparse_interpolated_string_body ( values, flags) ;
15591584 self . p ( flags. quote_str ( ) ) ;
@@ -1586,14 +1611,13 @@ impl<'a> Generator<'a> {
15861611
15871612#[ cfg( test) ]
15881613mod tests {
1589- use ruff_python_ast:: str:: Quote ;
15901614 use ruff_python_ast:: { Mod , ModModule } ;
15911615 use ruff_python_parser:: { self , Mode , ParseOptions , parse_module} ;
15921616 use ruff_source_file:: LineEnding ;
15931617
15941618 use crate :: stylist:: Indentation ;
15951619
1596- use super :: Generator ;
1620+ use super :: { Generator , Mode as UnparseMode } ;
15971621
15981622 fn round_trip ( contents : & str ) -> String {
15991623 let indentation = Indentation :: default ( ) ;
@@ -1605,16 +1629,15 @@ mod tests {
16051629 }
16061630
16071631 /// Like [`round_trip`] but configure the [`Generator`] with the requested
1608- /// `indentation`, `line_ending` and `preferred_quote ` settings.
1632+ /// `indentation`, `line_ending` and `unparse_mode ` settings.
16091633 fn round_trip_with (
16101634 indentation : & Indentation ,
16111635 line_ending : LineEnding ,
1612- preferred_quote : Option < Quote > ,
1636+ unparse_mode : UnparseMode ,
16131637 contents : & str ,
16141638 ) -> String {
16151639 let module = parse_module ( contents) . unwrap ( ) ;
1616- let mut generator =
1617- Generator :: new ( indentation, line_ending) . with_preferred_quote ( preferred_quote) ;
1640+ let mut generator = Generator :: new ( indentation, line_ending) . with_mode ( unparse_mode) ;
16181641 generator. unparse_suite ( module. suite ( ) ) ;
16191642 generator. generate ( )
16201643 }
@@ -1814,6 +1837,7 @@ except* Exception as e:
18141837type Y = str"
18151838 ) ;
18161839 assert_eq ! ( round_trip( r"x = (1, 2, 3)" ) , r"x = 1, 2, 3" ) ;
1840+ assert_eq ! ( round_trip( r"x = (1, (2, 3))" ) , r"x = 1, (2, 3)" ) ;
18171841 assert_eq ! ( round_trip( r"-(1) + ~(2) + +(3)" ) , r"-1 + ~2 + +3" ) ;
18181842 assert_round_trip ! (
18191843 r"def f():
@@ -2000,7 +2024,7 @@ if True:
20002024 round_trip_with(
20012025 & Indentation :: new( " " . to_string( ) ) ,
20022026 LineEnding :: default ( ) ,
2003- None ,
2027+ UnparseMode :: Default ,
20042028 r"
20052029if True:
20062030 pass
@@ -2018,7 +2042,7 @@ if True:
20182042 round_trip_with(
20192043 & Indentation :: new( " " . to_string( ) ) ,
20202044 LineEnding :: default ( ) ,
2021- None ,
2045+ UnparseMode :: Default ,
20222046 r"
20232047if True:
20242048 pass
@@ -2036,7 +2060,7 @@ if True:
20362060 round_trip_with(
20372061 & Indentation :: new( "\t " . to_string( ) ) ,
20382062 LineEnding :: default ( ) ,
2039- None ,
2063+ UnparseMode :: Default ,
20402064 r"
20412065if True:
20422066 pass
@@ -2058,7 +2082,7 @@ if True:
20582082 round_trip_with(
20592083 & Indentation :: default ( ) ,
20602084 LineEnding :: Lf ,
2061- None ,
2085+ UnparseMode :: Default ,
20622086 "if True:\n print(42)" ,
20632087 ) ,
20642088 "if True:\n print(42)" ,
@@ -2068,7 +2092,7 @@ if True:
20682092 round_trip_with(
20692093 & Indentation :: default ( ) ,
20702094 LineEnding :: CrLf ,
2071- None ,
2095+ UnparseMode :: Default ,
20722096 "if True:\n print(42)" ,
20732097 ) ,
20742098 "if True:\r \n print(42)" ,
@@ -2078,30 +2102,40 @@ if True:
20782102 round_trip_with(
20792103 & Indentation :: default ( ) ,
20802104 LineEnding :: Cr ,
2081- None ,
2105+ UnparseMode :: Default ,
20822106 "if True:\n print(42)" ,
20832107 ) ,
20842108 "if True:\r print(42)" ,
20852109 ) ;
20862110 }
20872111
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 ) {
2112+ #[ test_case:: test_case( r#""'hello'""# , r#""'hello'""# ; "basic str ignored" ) ]
2113+ #[ test_case:: test_case( r#"b"'hello'""# , r#"b"'hello'""# ; "basic bytes ignored" ) ]
2114+ #[ test_case:: test_case( r#""hello""# , "'hello'" ; "basic str single" ) ]
2115+ #[ test_case:: test_case( r#"b"hello""# , "b'hello'" ; "basic bytes single" ) ]
2116+ #[ test_case:: test_case( "'hello'" , "'hello'" ; "remain str single" ) ]
2117+ #[ test_case:: test_case( r#"x: list["str"]"# , "x: list['str']" ; "type ann single" ) ]
2118+ #[ test_case:: test_case( r#"f"hello""# , "f'hello'" ; "basic fstring single" ) ]
2119+ fn ast_unparse_quote ( inp : & str , out : & str ) {
2120+ let got = round_trip_with (
2121+ & Indentation :: default ( ) ,
2122+ LineEnding :: default ( ) ,
2123+ UnparseMode :: AstUnparse ,
2124+ inp,
2125+ ) ;
2126+ assert_eq ! ( got, out) ;
2127+ }
2128+
2129+ #[ test_case:: test_case( "a," , "(a,)" ; "basic single" ) ]
2130+ #[ test_case:: test_case( "a, b" , "(a, b)" ; "basic multi" ) ]
2131+ #[ test_case:: test_case( "x = a," , "x = (a,)" ; "basic assign single" ) ]
2132+ #[ test_case:: test_case( "x = a, b" , "x = (a, b)" ; "basic assign multi" ) ]
2133+ #[ test_case:: test_case( "a, (b, c)" , "(a, (b, c))" ; "nested" ) ]
2134+ fn ast_tuple_parentheses ( inp : & str , out : & str ) {
21012135 let got = round_trip_with (
21022136 & Indentation :: default ( ) ,
21032137 LineEnding :: default ( ) ,
2104- Some ( quote ) ,
2138+ UnparseMode :: AstUnparse ,
21052139 inp,
21062140 ) ;
21072141 assert_eq ! ( got, out) ;
0 commit comments