+ elif value[:3] == "'''":
+ orig_quote = "'''"
+ new_quote = '"""'
+ elif value[0] == '"':
+ orig_quote = '"'
+ new_quote = "'"
+ else:
+ orig_quote = "'"
+ new_quote = '"'
+ first_quote_pos = leaf.value.find(orig_quote)
+ if first_quote_pos == -1:
+ return # There's an internal error
+
+ prefix = leaf.value[:first_quote_pos]
+ body = leaf.value[first_quote_pos + len(orig_quote):-len(orig_quote)]
+ unescaped_new_quote = re.compile(r"(([^\\]|^)(\\\\)*)" + new_quote)
+ escaped_orig_quote = re.compile(r"\\(\\\\)*" + orig_quote)
+ if "r" in prefix.casefold():
+ if unescaped_new_quote.search(body):
+ # There's at least one unescaped new_quote in this raw string
+ # so converting is impossible
+ return
+
+ # Do not introduce or remove backslashes in raw strings
+ new_body = body
+ else:
+ new_body = escaped_orig_quote.sub(f"\\1{orig_quote}", body)
+ new_body = unescaped_new_quote.sub(f"\\1\\\\{new_quote}", new_body)
+ if new_quote == '"""' and new_body[-1] == '"':
+ # edge case:
+ new_body = new_body[:-1] + '\\"'
+ orig_escape_count = body.count("\\")
+ new_escape_count = new_body.count("\\")
+ if new_escape_count > orig_escape_count:
+ return # Do not introduce more escaping
+
+ if new_escape_count == orig_escape_count and orig_quote == '"':
+ return # Prefer double quotes
+
+ leaf.value = f"{prefix}{new_quote}{new_body}{new_quote}"
+
+
+def is_python36(node: Node) -> bool:
+ """Return True if the current file is using Python 3.6+ features.
+
+ Currently looking for:
+ - f-strings; and
+ - trailing commas after * or ** in function signatures.
+ """
+ for n in node.pre_order():
+ if n.type == token.STRING:
+ value_head = n.value[:2] # type: ignore
+ if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}:
+ return True
+
+ elif (
+ n.type == syms.typedargslist
+ and n.children
+ and n.children[-1].type == token.COMMA
+ ):
+ for ch in n.children:
+ if ch.type == token.STAR or ch.type == token.DOUBLESTAR:
+ return True
+
+ return False