Skip to content

Commit a2d0d39

Browse files
Configurable "unparse mode" for ruff_python_codegen::Generator (#21041)
Co-authored-by: Micha Reiser <micha@reiser.io>
1 parent f36fa7d commit a2d0d39

File tree

2 files changed

+77
-43
lines changed

2 files changed

+77
-43
lines changed

crates/ruff_python_codegen/src/generator.rs

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use ruff_source_file::LineEnding;
1717
use super::stylist::{Indentation, Stylist};
1818

1919
mod 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+
6690
pub 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)]
15881613
mod 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:
18141837
type 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"
20052029
if 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"
20232047
if 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"
20412065
if 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);

crates/ruff_python_codegen/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub use generator::Generator;
1+
pub use generator::{Generator, Mode};
22
use ruff_python_parser::{ParseError, parse_module};
33
pub use stylist::{Indentation, Stylist};
44

0 commit comments

Comments
 (0)