@@ -203,11 +203,17 @@ impl Format<PyFormatContext<'_>> for FormatStringPart {
203
203
let raw_content_range = relative_raw_content_range + self . part_range . start ( ) ;
204
204
205
205
let raw_content = & string_content[ relative_raw_content_range] ;
206
- let preferred_quotes = preferred_quotes ( raw_content, quotes, f. options ( ) . quote_style ( ) ) ;
206
+ let is_raw_string = prefix. is_raw_string ( ) ;
207
+ let preferred_quotes = if is_raw_string {
208
+ preferred_quotes_raw ( raw_content, quotes, f. options ( ) . quote_style ( ) )
209
+ } else {
210
+ preferred_quotes ( raw_content, quotes, f. options ( ) . quote_style ( ) )
211
+ } ;
207
212
208
213
write ! ( f, [ prefix, preferred_quotes] ) ?;
209
214
210
- let ( normalized, contains_newlines) = normalize_string ( raw_content, preferred_quotes) ;
215
+ let ( normalized, contains_newlines) =
216
+ normalize_string ( raw_content, preferred_quotes, is_raw_string) ;
211
217
212
218
match normalized {
213
219
Cow :: Borrowed ( _) => {
@@ -223,7 +229,7 @@ impl Format<PyFormatContext<'_>> for FormatStringPart {
223
229
}
224
230
225
231
bitflags ! {
226
- #[ derive( Copy , Clone , Debug ) ]
232
+ #[ derive( Copy , Clone , Debug , PartialEq , Eq ) ]
227
233
pub ( super ) struct StringPrefix : u8 {
228
234
const UNICODE = 0b0000_0001 ;
229
235
/// `r"test"`
@@ -264,6 +270,10 @@ impl StringPrefix {
264
270
pub ( super ) const fn text_len ( self ) -> TextSize {
265
271
TextSize :: new ( self . bits ( ) . count_ones ( ) )
266
272
}
273
+
274
+ pub ( super ) const fn is_raw_string ( self ) -> bool {
275
+ matches ! ( self , StringPrefix :: RAW | StringPrefix :: RAW_UPPER )
276
+ }
267
277
}
268
278
269
279
impl Format < PyFormatContext < ' _ > > for StringPrefix {
@@ -290,6 +300,54 @@ impl Format<PyFormatContext<'_>> for StringPrefix {
290
300
}
291
301
}
292
302
303
+ /// Detects the preferred quotes for raw string `input`.
304
+ /// The configured quote style is preferred unless `input` contains unescaped quotes of the
305
+ /// configured style. For example, `r"foo"` is preferred over `r'foo'` if the configured
306
+ /// quote style is double quotes.
307
+ fn preferred_quotes_raw (
308
+ input : & str ,
309
+ quotes : StringQuotes ,
310
+ configured_style : QuoteStyle ,
311
+ ) -> StringQuotes {
312
+ let configured_quote_char = configured_style. as_char ( ) ;
313
+ let mut chars = input. chars ( ) . peekable ( ) ;
314
+ let contains_unescaped_configured_quotes = loop {
315
+ match chars. next ( ) {
316
+ Some ( '\\' ) => {
317
+ // Ignore escaped characters
318
+ chars. next ( ) ;
319
+ }
320
+ // `"` or `'`
321
+ Some ( c) if c == configured_quote_char => {
322
+ if !quotes. triple {
323
+ break true ;
324
+ }
325
+
326
+ if chars. peek ( ) == Some ( & configured_quote_char) {
327
+ // `""` or `''`
328
+ chars. next ( ) ;
329
+
330
+ if chars. peek ( ) == Some ( & configured_quote_char) {
331
+ // `"""` or `'''`
332
+ break true ;
333
+ }
334
+ }
335
+ }
336
+ Some ( _) => continue ,
337
+ None => break false ,
338
+ }
339
+ } ;
340
+
341
+ StringQuotes {
342
+ triple : quotes. triple ,
343
+ style : if contains_unescaped_configured_quotes {
344
+ quotes. style
345
+ } else {
346
+ configured_style
347
+ } ,
348
+ }
349
+ }
350
+
293
351
/// Detects the preferred quotes for `input`.
294
352
/// * single quoted strings: The preferred quote style is the one that requires less escape sequences.
295
353
/// * triple quoted strings: Use double quotes except the string contains a sequence of `"""`.
@@ -434,7 +492,11 @@ impl Format<PyFormatContext<'_>> for StringQuotes {
434
492
/// with the provided `style`.
435
493
///
436
494
/// Returns the normalized string and whether it contains new lines.
437
- fn normalize_string ( input : & str , quotes : StringQuotes ) -> ( Cow < str > , ContainsNewlines ) {
495
+ fn normalize_string (
496
+ input : & str ,
497
+ quotes : StringQuotes ,
498
+ is_raw : bool ,
499
+ ) -> ( Cow < str > , ContainsNewlines ) {
438
500
// The normalized string if `input` is not yet normalized.
439
501
// `output` must remain empty if `input` is already normalized.
440
502
let mut output = String :: new ( ) ;
@@ -467,7 +529,7 @@ fn normalize_string(input: &str, quotes: StringQuotes) -> (Cow<str>, ContainsNew
467
529
newlines = ContainsNewlines :: Yes ;
468
530
} else if c == '\n' {
469
531
newlines = ContainsNewlines :: Yes ;
470
- } else if !quotes. triple {
532
+ } else if !quotes. triple && !is_raw {
471
533
if c == '\\' {
472
534
if let Some ( next) = input. as_bytes ( ) . get ( index + 1 ) . copied ( ) . map ( char:: from) {
473
535
#[ allow( clippy:: if_same_then_else) ]
0 commit comments