Prefer double quotes but only if it doesn't cause more escaping. Adds or removes backslashes as appropriate.
(s: str)
| 167 | |
| 168 | |
| 169 | def normalize_string_quotes(s: str) -> str: |
| 170 | """Prefer double quotes but only if it doesn't cause more escaping. |
| 171 | |
| 172 | Adds or removes backslashes as appropriate. |
| 173 | """ |
| 174 | value = s.lstrip(STRING_PREFIX_CHARS) |
| 175 | if value[:3] == '"""': |
| 176 | return s |
| 177 | |
| 178 | elif value[:3] == "'''": |
| 179 | orig_quote = "'''" |
| 180 | new_quote = '"""' |
| 181 | elif value[0] == '"': |
| 182 | orig_quote = '"' |
| 183 | new_quote = "'" |
| 184 | else: |
| 185 | orig_quote = "'" |
| 186 | new_quote = '"' |
| 187 | first_quote_pos = s.find(orig_quote) |
| 188 | assert first_quote_pos != -1, f"INTERNAL ERROR: Malformed string {s!r}" |
| 189 | |
| 190 | prefix = s[:first_quote_pos] |
| 191 | unescaped_new_quote = _cached_compile(rf"(([^\\]|^)(\\\\)*){new_quote}") |
| 192 | escaped_new_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){new_quote}") |
| 193 | escaped_orig_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){orig_quote}") |
| 194 | body = s[first_quote_pos + len(orig_quote) : -len(orig_quote)] |
| 195 | if "r" in prefix.casefold(): |
| 196 | if unescaped_new_quote.search(body): |
| 197 | # There's at least one unescaped new_quote in this raw string |
| 198 | # so converting is impossible |
| 199 | return s |
| 200 | |
| 201 | # Do not introduce or remove backslashes in raw strings |
| 202 | new_body = body |
| 203 | else: |
| 204 | # remove unnecessary escapes |
| 205 | new_body = sub_twice(escaped_new_quote, rf"\1\2{new_quote}", body) |
| 206 | if body != new_body: |
| 207 | # Consider the string without unnecessary escapes as the original |
| 208 | body = new_body |
| 209 | s = f"{prefix}{orig_quote}{body}{orig_quote}" |
| 210 | new_body = sub_twice(escaped_orig_quote, rf"\1\2{orig_quote}", new_body) |
| 211 | new_body = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_body) |
| 212 | |
| 213 | if "f" in prefix.casefold(): |
| 214 | matches = re.findall( |
| 215 | r""" |
| 216 | (?:(?<!\{)|^)\{ # start of the string or a non-{ followed by a single { |
| 217 | ([^{].*?) # contents of the brackets except if begins with {{ |
| 218 | \}(?:(?!\})|$) # A } followed by end of the string or a non-} |
| 219 | """, |
| 220 | new_body, |
| 221 | re.VERBOSE, |
| 222 | ) |
| 223 | for m in matches: |
| 224 | if "\\" in str(m): |
| 225 | # Do not introduce backslashes in interpolated expressions |
| 226 | return s |
no test coverage detected