]> git.madduck.net Git - etc/vim.git/commitdiff

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Normalize string quotes (#75)
authorZsolt Dollenstein <zsol.zsol@gmail.com>
Sat, 31 Mar 2018 18:21:25 +0000 (19:21 +0100)
committerŁukasz Langa <lukasz@langa.pl>
Sat, 31 Mar 2018 18:21:25 +0000 (11:21 -0700)
* Normalize string quotes

Convert single-quoted strings to double-quoted. Convert triple single-quoted strings to triple double-quoted. Do not touch any strings where conversion would increase the number of backslashes.

Fixes #51.

* reformat Black itself

14 files changed:
black.py
setup.py
tests/comments.py
tests/comments2.py
tests/comments3.py
tests/comments4.py
tests/composition.py
tests/empty_lines.py
tests/expression.py
tests/fmtonoff.py
tests/fstring.py
tests/function.py
tests/string_quotes.py [new file with mode: 0644]
tests/test_black.py

index 82fe5d11cdc7f7551d999141fb21fe57c97e8255..dc03e0a9b3a324cb266eae50143bf7a97dcb72d9 100644 (file)
--- a/black.py
+++ b/black.py
@@ -47,9 +47,9 @@ LeafID = int
 Priority = int
 Index = int
 LN = Union[Leaf, Node]
-SplitFunc = Callable[['Line', bool], Iterator['Line']]
+SplitFunc = Callable[["Line", bool], Iterator["Line"]]
 out = partial(click.secho, bold=True, err=True)
-err = partial(click.secho, fg='red', err=True)
+err = partial(click.secho, fg="red", err=True)
 
 
 class NothingChanged(UserWarning):
@@ -94,15 +94,15 @@ class FormatOff(FormatError):
 
 @click.command()
 @click.option(
-    '-l',
-    '--line-length',
+    "-l",
+    "--line-length",
     type=int,
     default=DEFAULT_LINE_LENGTH,
-    help='How many character per line to allow.',
+    help="How many character per line to allow.",
     show_default=True,
 )
 @click.option(
-    '--check',
+    "--check",
     is_flag=True,
     help=(
         "Don't write back the files, just return the status.  Return code 0 "
@@ -111,13 +111,13 @@ class FormatOff(FormatError):
     ),
 )
 @click.option(
-    '--fast/--safe',
+    "--fast/--safe",
     is_flag=True,
-    help='If --fast given, skip temporary sanity checks. [default: --safe]',
+    help="If --fast given, skip temporary sanity checks. [default: --safe]",
 )
 @click.version_option(version=__version__)
 @click.argument(
-    'src',
+    "src",
     nargs=-1,
     type=click.Path(
         exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True
@@ -136,17 +136,17 @@ def main(
         elif p.is_file():
             # if a file was explicitly given, we don't care about its extension
             sources.append(p)
-        elif s == '-':
-            sources.append(Path('-'))
+        elif s == "-":
+            sources.append(Path("-"))
         else:
-            err(f'invalid path: {s}')
+            err(f"invalid path: {s}")
     if len(sources) == 0:
         ctx.exit(0)
     elif len(sources) == 1:
         p = sources[0]
         report = Report(check=check)
         try:
-            if not p.is_file() and str(p) == '-':
+            if not p.is_file() and str(p) == "-":
                 changed = format_stdin_to_stdout(
                     line_length=line_length, fast=fast, write_back=not check
                 )
@@ -202,7 +202,7 @@ async def schedule_formatting(
     report = Report(check=not write_back)
     for src, task in tasks.items():
         if not task.done():
-            report.failed(src, 'timed out, cancelling')
+            report.failed(src, "timed out, cancelling")
             task.cancel()
             cancelled.append(task)
         elif task.cancelled():
@@ -214,7 +214,7 @@ async def schedule_formatting(
     if cancelled:
         await asyncio.gather(*cancelled, loop=loop, return_exceptions=True)
     else:
-        out('All done! ✨ 🍰 ✨')
+        out("All done! ✨ 🍰 ✨")
     click.echo(str(report))
     return report.return_code
 
@@ -272,7 +272,7 @@ def format_file_contents(
     valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
     `line_length` is passed to :func:`format_str`.
     """
-    if src_contents.strip() == '':
+    if src_contents.strip() == "":
         raise NothingChanged
 
     dst_contents = format_str(src_contents, line_length=line_length)
@@ -319,8 +319,8 @@ GRAMMARS = [
 def lib2to3_parse(src_txt: str) -> Node:
     """Given a string with source, return the lib2to3 Node."""
     grammar = pygram.python_grammar_no_print_statement
-    if src_txt[-1] != '\n':
-        nl = '\r\n' if '\r\n' in src_txt[:1024] else '\n'
+    if src_txt[-1] != "\n":
+        nl = "\r\n" if "\r\n" in src_txt[:1024] else "\n"
         src_txt += nl
     for grammar in GRAMMARS:
         drv = driver.Driver(grammar, pytree.convert)
@@ -350,7 +350,7 @@ def lib2to3_unparse(node: Node) -> str:
     return code
 
 
-T = TypeVar('T')
+T = TypeVar("T")
 
 
 class Visitor(Generic[T]):
@@ -370,7 +370,7 @@ class Visitor(Generic[T]):
             name = token.tok_name[node.type]
         else:
             name = type_repr(node.type)
-        yield from getattr(self, f'visit_{name}', self.visit_default)(node)
+        yield from getattr(self, f"visit_{name}", self.visit_default)(node)
 
     def visit_default(self, node: LN) -> Iterator[T]:
         """Default `visit_*()` implementation. Recurses to children of `node`."""
@@ -384,24 +384,24 @@ class DebugVisitor(Visitor[T]):
     tree_depth: int = 0
 
     def visit_default(self, node: LN) -> Iterator[T]:
-        indent = ' ' * (2 * self.tree_depth)
+        indent = " " * (2 * self.tree_depth)
         if isinstance(node, Node):
             _type = type_repr(node.type)
-            out(f'{indent}{_type}', fg='yellow')
+            out(f"{indent}{_type}", fg="yellow")
             self.tree_depth += 1
             for child in node.children:
                 yield from self.visit(child)
 
             self.tree_depth -= 1
-            out(f'{indent}/{_type}', fg='yellow', bold=False)
+            out(f"{indent}/{_type}", fg="yellow", bold=False)
         else:
             _type = token.tok_name.get(node.type, str(node.type))
-            out(f'{indent}{_type}', fg='blue', nl=False)
+            out(f"{indent}{_type}", fg="blue", nl=False)
             if node.prefix:
                 # We don't have to handle prefixes for `Node` objects since
                 # that delegates to the first child anyway.
-                out(f' {node.prefix!r}', fg='green', bold=False, nl=False)
-            out(f' {node.value!r}', fg='blue', bold=False)
+                out(f" {node.prefix!r}", fg="green", bold=False, nl=False)
+            out(f" {node.value!r}", fg="blue", bold=False)
 
     @classmethod
     def show(cls, code: str) -> None:
@@ -415,7 +415,7 @@ class DebugVisitor(Visitor[T]):
 
 KEYWORDS = set(keyword.kwlist)
 WHITESPACE = {token.DEDENT, token.INDENT, token.NEWLINE}
-FLOW_CONTROL = {'return', 'raise', 'break', 'continue'}
+FLOW_CONTROL = {"return", "raise", "break", "continue"}
 STATEMENT = {
     syms.if_stmt,
     syms.while_stmt,
@@ -427,7 +427,7 @@ STATEMENT = {
     syms.classdef,
 }
 STANDALONE_COMMENT = 153
-LOGIC_OPERATORS = {'and', 'or'}
+LOGIC_OPERATORS = {"and", "or"}
 COMPARATORS = {
     token.LESS,
     token.GREATER,
@@ -500,14 +500,14 @@ class BracketTracker:
                     self.delimiters[id(self.previous)] = STRING_PRIORITY
                 elif (
                     leaf.type == token.NAME
-                    and leaf.value == 'for'
+                    and leaf.value == "for"
                     and leaf.parent
                     and leaf.parent.type in {syms.comp_for, syms.old_comp_for}
                 ):
                     self.delimiters[id(self.previous)] = COMPREHENSION_PRIORITY
                 elif (
                     leaf.type == token.NAME
-                    and leaf.value == 'if'
+                    and leaf.value == "if"
                     and leaf.parent
                     and leaf.parent.type in {syms.comp_if, syms.old_comp_if}
                 ):
@@ -612,7 +612,7 @@ class Line:
         return (
             bool(self)
             and self.leaves[0].type == token.NAME
-            and self.leaves[0].value == 'class'
+            and self.leaves[0].value == "class"
         )
 
     @property
@@ -628,12 +628,12 @@ class Line:
         except IndexError:
             second_leaf = None
         return (
-            (first_leaf.type == token.NAME and first_leaf.value == 'def')
+            (first_leaf.type == token.NAME and first_leaf.value == "def")
             or (
                 first_leaf.type == token.ASYNC
                 and second_leaf is not None
                 and second_leaf.type == token.NAME
-                and second_leaf.value == 'def'
+                and second_leaf.value == "def"
             )
         )
 
@@ -655,7 +655,7 @@ class Line:
         return (
             bool(self)
             and self.leaves[0].type == token.NAME
-            and self.leaves[0].value == 'yield'
+            and self.leaves[0].value == "yield"
         )
 
     @property
@@ -722,7 +722,7 @@ class Line:
         To avoid splitting on the comma in this situation, increase the depth of
         tokens between `for` and `in`.
         """
-        if leaf.type == token.NAME and leaf.value == 'for':
+        if leaf.type == token.NAME and leaf.value == "for":
             self.has_for = True
             self.bracket_tracker.depth += 1
             self._for_loop_variable = True
@@ -732,7 +732,7 @@ class Line:
 
     def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool:
         """See `maybe_increment_for_loop_variable` above for explanation."""
-        if self._for_loop_variable and leaf.type == token.NAME and leaf.value == 'in':
+        if self._for_loop_variable and leaf.type == token.NAME and leaf.value == "in":
             self.bracket_tracker.depth -= 1
             self._for_loop_variable = False
             return True
@@ -745,7 +745,7 @@ class Line:
             comment.type == STANDALONE_COMMENT
             and self.bracket_tracker.any_open_brackets()
         ):
-            comment.prefix = ''
+            comment.prefix = ""
             return False
 
         if comment.type != token.COMMENT:
@@ -754,7 +754,7 @@ class Line:
         after = len(self.leaves) - 1
         if after == -1:
             comment.type = STANDALONE_COMMENT
-            comment.prefix = ''
+            comment.prefix = ""
             return False
 
         else:
@@ -786,17 +786,17 @@ class Line:
     def __str__(self) -> str:
         """Render the line."""
         if not self:
-            return '\n'
+            return "\n"
 
-        indent = '    ' * self.depth
+        indent = "    " * self.depth
         leaves = iter(self.leaves)
         first = next(leaves)
-        res = f'{first.prefix}{indent}{first.value}'
+        res = f"{first.prefix}{indent}{first.value}"
         for leaf in leaves:
             res += str(leaf)
         for _, comment in self.comments:
             res += str(comment)
-        return res + '\n'
+        return res + "\n"
 
     def __bool__(self) -> bool:
         """Return True if the line has leaves or comments."""
@@ -832,9 +832,9 @@ class UnformattedLines(Line):
         `depth` is not used for indentation in this case.
         """
         if not self:
-            return '\n'
+            return "\n"
 
-        res = ''
+        res = ""
         for leaf in self.leaves:
             res += str(leaf)
         return res
@@ -888,9 +888,9 @@ class EmptyLineTracker:
         if current_line.leaves:
             # Consume the first leaf's extra newlines.
             first_leaf = current_line.leaves[0]
-            before = first_leaf.prefix.count('\n')
+            before = first_leaf.prefix.count("\n")
             before = min(before, max_allowed)
-            first_leaf.prefix = ''
+            first_leaf.prefix = ""
         else:
             before = 0
         depth = current_line.depth
@@ -1009,6 +1009,8 @@ class LineGenerator(Visitor[Line]):
 
             else:
                 normalize_prefix(node, inside_brackets=any_open_brackets)
+                if node.type == token.STRING:
+                    normalize_string_quotes(node)
                 if node.type not in WHITESPACE:
                     self.current_line.append(node)
         yield from super().visit_default(node)
@@ -1098,14 +1100,14 @@ class LineGenerator(Visitor[Line]):
     def __attrs_post_init__(self) -> None:
         """You are in a twisty little maze of passages."""
         v = self.visit_stmt
-        self.visit_if_stmt = partial(v, keywords={'if', 'else', 'elif'})
-        self.visit_while_stmt = partial(v, keywords={'while', 'else'})
-        self.visit_for_stmt = partial(v, keywords={'for', 'else'})
-        self.visit_try_stmt = partial(v, keywords={'try', 'except', 'else', 'finally'})
-        self.visit_except_clause = partial(v, keywords={'except'})
-        self.visit_funcdef = partial(v, keywords={'def'})
-        self.visit_with_stmt = partial(v, keywords={'with'})
-        self.visit_classdef = partial(v, keywords={'class'})
+        self.visit_if_stmt = partial(v, keywords={"if", "else", "elif"})
+        self.visit_while_stmt = partial(v, keywords={"while", "else"})
+        self.visit_for_stmt = partial(v, keywords={"for", "else"})
+        self.visit_try_stmt = partial(v, keywords={"try", "except", "else", "finally"})
+        self.visit_except_clause = partial(v, keywords={"except"})
+        self.visit_funcdef = partial(v, keywords={"def"})
+        self.visit_with_stmt = partial(v, keywords={"with"})
+        self.visit_classdef = partial(v, keywords={"class"})
         self.visit_async_funcdef = self.visit_async_stmt
         self.visit_decorated = self.visit_decorators
 
@@ -1119,9 +1121,9 @@ ALWAYS_NO_SPACE = CLOSING_BRACKETS | {token.COMMA, STANDALONE_COMMENT}
 
 def whitespace(leaf: Leaf) -> str:  # noqa C901
     """Return whitespace prefix if needed for the given `leaf`."""
-    NO = ''
-    SPACE = ' '
-    DOUBLESPACE = '  '
+    NO = ""
+    SPACE = " "
+    DOUBLESPACE = "  "
     t = leaf.type
     p = leaf.parent
     v = leaf.value
@@ -1185,7 +1187,7 @@ def whitespace(leaf: Leaf) -> str:  # noqa C901
             and prevp.parent.type == syms.shift_expr
             and prevp.prev_sibling
             and prevp.prev_sibling.type == token.NAME
-            and prevp.prev_sibling.value == 'print'  # type: ignore
+            and prevp.prev_sibling.value == "print"  # type: ignore
         ):
             # Python 2 print chevron
             return NO
@@ -1342,7 +1344,7 @@ def whitespace(leaf: Leaf) -> str:  # noqa C901
                 return NO
 
         elif t == token.NAME:
-            if v == 'import':
+            if v == "import":
                 return SPACE
 
             if prev and prev.type == token.DOT:
@@ -1416,17 +1418,17 @@ def generate_comments(leaf: Leaf) -> Iterator[Leaf]:
     if not p:
         return
 
-    if '#' not in p:
+    if "#" not in p:
         return
 
     consumed = 0
     nlines = 0
-    for index, line in enumerate(p.split('\n')):
+    for index, line in enumerate(p.split("\n")):
         consumed += len(line) + 1  # adding the length of the split '\n'
         line = line.lstrip()
         if not line:
             nlines += 1
-        if not line.startswith('#'):
+        if not line.startswith("#"):
             continue
 
         if index == 0 and leaf.type != token.ENDMARKER:
@@ -1434,12 +1436,12 @@ def generate_comments(leaf: Leaf) -> Iterator[Leaf]:
         else:
             comment_type = STANDALONE_COMMENT
         comment = make_comment(line)
-        yield Leaf(comment_type, comment, prefix='\n' * nlines)
+        yield Leaf(comment_type, comment, prefix="\n" * nlines)
 
-        if comment in {'# fmt: on', '# yapf: enable'}:
+        if comment in {"# fmt: on", "# yapf: enable"}:
             raise FormatOn(consumed)
 
-        if comment in {'# fmt: off', '# yapf: disable'}:
+        if comment in {"# fmt: off", "# yapf: disable"}:
             raise FormatOff(consumed)
 
         nlines = 0
@@ -1455,13 +1457,13 @@ def make_comment(content: str) -> str:
     """
     content = content.rstrip()
     if not content:
-        return '#'
+        return "#"
 
-    if content[0] == '#':
+    if content[0] == "#":
         content = content[1:]
-    if content and content[0] not in ' !:#':
-        content = ' ' + content
-    return '#' + content
+    if content and content[0] not in " !:#":
+        content = " " + content
+    return "#" + content
 
 
 def split_line(
@@ -1481,10 +1483,10 @@ def split_line(
         yield line
         return
 
-    line_str = str(line).strip('\n')
+    line_str = str(line).strip("\n")
     if (
         len(line_str) <= line_length
-        and '\n' not in line_str  # multiline strings
+        and "\n" not in line_str  # multiline strings
         and not line.contains_standalone_comments
     ):
         yield line
@@ -1504,7 +1506,7 @@ def split_line(
         result: List[Line] = []
         try:
             for l in split_func(line, py36):
-                if str(l).strip('\n') == line_str:
+                if str(l).strip("\n") == line_str:
                     raise CannotSplit("Split function returned an unchanged result")
 
                 result.extend(
@@ -1703,7 +1705,7 @@ def delimiter_split(line: Line, py36: bool = False) -> Iterator[Line]:
             and current_line.leaves[-1].type != token.COMMA
             and trailing_comma_safe
         ):
-            current_line.append(Leaf(token.COMMA, ','))
+            current_line.append(Leaf(token.COMMA, ","))
         yield current_line
 
 
@@ -1749,8 +1751,8 @@ def is_import(leaf: Leaf) -> bool:
     return bool(
         t == token.NAME
         and (
-            (v == 'import' and p and p.type == syms.import_name)
-            or (v == 'from' and p and p.type == syms.import_from)
+            (v == "import" and p and p.type == syms.import_name)
+            or (v == "from" and p and p.type == syms.import_from)
         )
     )
 
@@ -1762,15 +1764,52 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None:
     Note: don't use backslashes for formatting or you'll lose your voting rights.
     """
     if not inside_brackets:
-        spl = leaf.prefix.split('#')
-        if '\\' not in spl[0]:
-            nl_count = spl[-1].count('\n')
+        spl = leaf.prefix.split("#")
+        if "\\" not in spl[0]:
+            nl_count = spl[-1].count("\n")
             if len(spl) > 1:
                 nl_count -= 1
-            leaf.prefix = '\n' * nl_count
+            leaf.prefix = "\n" * nl_count
             return
 
-    leaf.prefix = ''
+    leaf.prefix = ""
+
+
+def normalize_string_quotes(leaf: Leaf) -> None:
+    value = leaf.value.lstrip("furbFURB")
+    if value[:3] == '"""':
+        return
+
+    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
+
+    body = leaf.value[first_quote_pos + len(orig_quote):-len(orig_quote)]
+    new_body = body.replace(f"\\{orig_quote}", orig_quote).replace(
+        new_quote, f"\\{new_quote}"
+    )
+    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
+
+    prefix = leaf.value[:first_quote_pos]
+    leaf.value = f"{prefix}{new_quote}{new_body}{new_quote}"
 
 
 def is_python36(node: Node) -> bool:
@@ -1783,7 +1822,7 @@ def is_python36(node: Node) -> bool:
     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'}:
+            if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}:
                 return True
 
         elif (
@@ -1798,9 +1837,9 @@ def is_python36(node: Node) -> bool:
     return False
 
 
-PYTHON_EXTENSIONS = {'.py'}
+PYTHON_EXTENSIONS = {".py"}
 BLACKLISTED_DIRECTORIES = {
-    'build', 'buck-out', 'dist', '_build', '.git', '.hg', '.mypy_cache', '.tox', '.venv'
+    "build", "buck-out", "dist", "_build", ".git", ".hg", ".mypy_cache", ".tox", ".venv"
 }
 
 
@@ -1830,16 +1869,16 @@ class Report:
     def done(self, src: Path, changed: bool) -> None:
         """Increment the counter for successful reformatting. Write out a message."""
         if changed:
-            reformatted = 'would reformat' if self.check else 'reformatted'
-            out(f'{reformatted} {src}')
+            reformatted = "would reformat" if self.check else "reformatted"
+            out(f"{reformatted} {src}")
             self.change_count += 1
         else:
-            out(f'{src} already well formatted, good job.', bold=False)
+            out(f"{src} already well formatted, good job.", bold=False)
             self.same_count += 1
 
     def failed(self, src: Path, message: str) -> None:
         """Increment the counter for failed reformatting. Write out a message."""
-        err(f'error: cannot format {src}: {message}')
+        err(f"error: cannot format {src}: {message}")
         self.failure_count += 1
 
     @property
@@ -1876,19 +1915,19 @@ class Report:
             failed = "failed to reformat"
         report = []
         if self.change_count:
-            s = 's' if self.change_count > 1 else ''
+            s = "s" if self.change_count > 1 else ""
             report.append(
-                click.style(f'{self.change_count} file{s} {reformatted}', bold=True)
+                click.style(f"{self.change_count} file{s} {reformatted}", bold=True)
             )
         if self.same_count:
-            s = 's' if self.same_count > 1 else ''
-            report.append(f'{self.same_count} file{s} {unchanged}')
+            s = "s" if self.same_count > 1 else ""
+            report.append(f"{self.same_count} file{s} {unchanged}")
         if self.failure_count:
-            s = 's' if self.failure_count > 1 else ''
+            s = "s" if self.failure_count > 1 else ""
             report.append(
-                click.style(f'{self.failure_count} file{s} {failed}', fg='red')
+                click.style(f"{self.failure_count} file{s} {failed}", fg="red")
             )
-        return ', '.join(report) + '.'
+        return ", ".join(report) + "."
 
 
 def assert_equivalent(src: str, dst: str) -> None:
@@ -1935,17 +1974,17 @@ def assert_equivalent(src: str, dst: str) -> None:
     try:
         dst_ast = ast.parse(dst)
     except Exception as exc:
-        log = dump_to_file(''.join(traceback.format_tb(exc.__traceback__)), dst)
+        log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
         raise AssertionError(
             f"INTERNAL ERROR: Black produced invalid code: {exc}. "
             f"Please report a bug on https://github.com/ambv/black/issues.  "
             f"This invalid output might be helpful: {log}"
         ) from None
 
-    src_ast_str = '\n'.join(_v(src_ast))
-    dst_ast_str = '\n'.join(_v(dst_ast))
+    src_ast_str = "\n".join(_v(src_ast))
+    dst_ast_str = "\n".join(_v(dst_ast))
     if src_ast_str != dst_ast_str:
-        log = dump_to_file(diff(src_ast_str, dst_ast_str, 'src', 'dst'))
+        log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
         raise AssertionError(
             f"INTERNAL ERROR: Black produced code that is not equivalent to "
             f"the source.  "
@@ -1959,8 +1998,8 @@ def assert_stable(src: str, dst: str, line_length: int) -> None:
     newdst = format_str(dst, line_length=line_length)
     if dst != newdst:
         log = dump_to_file(
-            diff(src, dst, 'source', 'first pass'),
-            diff(dst, newdst, 'first pass', 'second pass'),
+            diff(src, dst, "source", "first pass"),
+            diff(dst, newdst, "first pass", "second pass"),
         )
         raise AssertionError(
             f"INTERNAL ERROR: Black produced different code on the second pass "
@@ -1975,11 +2014,11 @@ def dump_to_file(*output: str) -> str:
     import tempfile
 
     with tempfile.NamedTemporaryFile(
-        mode='w', prefix='blk_', suffix='.log', delete=False
+        mode="w", prefix="blk_", suffix=".log", delete=False
     ) as f:
         for lines in output:
             f.write(lines)
-            f.write('\n')
+            f.write("\n")
     return f.name
 
 
@@ -1987,9 +2026,9 @@ def diff(a: str, b: str, a_name: str, b_name: str) -> str:
     """Return a unified diff string between strings `a` and `b`."""
     import difflib
 
-    a_lines = [line + '\n' for line in a.split('\n')]
-    b_lines = [line + '\n' for line in b.split('\n')]
-    return ''.join(
+    a_lines = [line + "\n" for line in a.split("\n")]
+    b_lines = [line + "\n" for line in b.split("\n")]
+    return "".join(
         difflib.unified_diff(a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5)
     )
 
@@ -2023,5 +2062,5 @@ def shutdown(loop: BaseEventLoop) -> None:
         loop.close()
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()
index 0f9141d7406f0f357a251e7eae5bd552aaf40796..13969df95d451ffe55eaf6caf8ad390822e2d78e 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -11,48 +11,48 @@ CURRENT_DIR = Path(__file__).parent
 
 
 def get_long_description():
-    readme_md = CURRENT_DIR / 'README.md'
-    with open(readme_md, encoding='utf8') as ld_file:
+    readme_md = CURRENT_DIR / "README.md"
+    with open(readme_md, encoding="utf8") as ld_file:
         return ld_file.read()
 
 
 def get_version():
-    black_py = CURRENT_DIR / 'black.py'
-    _version_re = re.compile(r'__version__\s+=\s+(?P<version>.*)')
-    with open(black_py, 'r', encoding='utf8') as f:
-        version = _version_re.search(f.read()).group('version')
+    black_py = CURRENT_DIR / "black.py"
+    _version_re = re.compile(r"__version__\s+=\s+(?P<version>.*)")
+    with open(black_py, "r", encoding="utf8") as f:
+        version = _version_re.search(f.read()).group("version")
     return str(ast.literal_eval(version))
 
 
 setup(
-    name='black',
+    name="black",
     version=get_version(),
     description="The uncompromising code formatter.",
     long_description=get_long_description(),
     long_description_content_type="text/markdown",
-    keywords='automation formatter yapf autopep8 pyfmt gofmt rustfmt',
-    author='Łukasz Langa',
-    author_email='lukasz@langa.pl',
-    url='https://github.com/ambv/black',
-    license='MIT',
-    py_modules=['black'],
-    packages=['blib2to3', 'blib2to3.pgen2'],
-    package_data={'blib2to3': ['*.txt']},
+    keywords="automation formatter yapf autopep8 pyfmt gofmt rustfmt",
+    author="Łukasz Langa",
+    author_email="lukasz@langa.pl",
+    url="https://github.com/ambv/black",
+    license="MIT",
+    py_modules=["black"],
+    packages=["blib2to3", "blib2to3.pgen2"],
+    package_data={"blib2to3": ["*.txt"]},
     python_requires=">=3.6",
     zip_safe=False,
-    install_requires=['click', 'attrs>=17.4.0'],
-    test_suite='tests.test_black',
+    install_requires=["click", "attrs>=17.4.0"],
+    test_suite="tests.test_black",
     classifiers=[
-        'Development Status :: 3 - Alpha',
-        'Environment :: Console',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: 3 :: Only',
-        'Topic :: Software Development :: Libraries :: Python Modules',
-        'Topic :: Software Development :: Quality Assurance',
+        "Development Status :: 3 - Alpha",
+        "Environment :: Console",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3 :: Only",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        "Topic :: Software Development :: Quality Assurance",
     ],
-    entry_points={'console_scripts': ['black=black:main']},
+    entry_points={"console_scripts": ["black=black:main"]},
 )
index 3a39afd97e881942428cff4e64ee34abfa15bd46..8ce9ffee49af68759a7605cceea6779c5b70515b 100644 (file)
@@ -43,7 +43,7 @@ def function(default=None):
 
 
 # Explains why we use global state.
-GLOBAL_STATE = {'a': a(1), 'b': a(2), 'c': a(3)}
+GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)}
 
 
 # Another comment!
@@ -76,7 +76,7 @@ async def wat():
         result = await x.method1()
     # Comment after ending a block.
     if result:
-        print('A OK', file=sys.stdout)
+        print("A OK", file=sys.stdout)
         # Comment between things.
         print()
 
index e90c0795da7924335964a53dc00c377663807ab0..49ef2dce09a696c8e83c72c2b7ab54e5f352af5c 100644 (file)
@@ -125,23 +125,23 @@ instruction()
 
 __all__ = [
     # Super-special typing primitives.
-    'Any',
-    'Callable',
-    'ClassVar',
+    "Any",
+    "Callable",
+    "ClassVar",
     # ABCs (from collections.abc).
-    'AbstractSet',  # collections.abc.Set.
-    'ByteString',
-    'Container',
+    "AbstractSet",  # collections.abc.Set.
+    "ByteString",
+    "Container",
     # Concrete collection types.
-    'Counter',
-    'Deque',
-    'Dict',
-    'DefaultDict',
-    'List',
-    'Set',
-    'FrozenSet',
-    'NamedTuple',  # Not really a type.
-    'Generator',
+    "Counter",
+    "Deque",
+    "Dict",
+    "DefaultDict",
+    "List",
+    "Set",
+    "FrozenSet",
+    "NamedTuple",  # Not really a type.
+    "Generator",
 ]
 
 # Comment before function.
@@ -212,7 +212,7 @@ short
     ]
     lcomp3 = [
         # This one is actually too long to fit in a single line.
-        element.split('\n', 1)[0]
+        element.split("\n", 1)[0]
         # yup
         for element in collection.select_elements()
         # right
@@ -228,7 +228,7 @@ short
     # let's return
     return Node(
         syms.simple_stmt,
-        [Node(statement, result), Leaf(token.NEWLINE, '\n')],  # FIXME: \r\n?
+        [Node(statement, result), Leaf(token.NEWLINE, "\n")],  # FIXME: \r\n?
     )
 
 
index b57f8f3d5b1130a176145a5a4220ab749f221f47..c1f0e7598aa84751a4a63c41aff99dfcb2f0e2ea 100644 (file)
@@ -1,7 +1,7 @@
 def func():
     lcomp3 = [
         # This one is actually too long to fit in a single line.
-        element.split('\n', 1)[0]
+        element.split("\n", 1)[0]
         # yup
         for element in collection.select_elements()
         # right
index e74bf50a607b02b78020064716394c6834974671..241b6cee72e11e8e8b8b6d937a2f5aaea10b0a68 100644 (file)
@@ -61,7 +61,7 @@ class C:
 
 def foo(list_a, list_b):
     results = (
-        User.query.filter(User.foo == 'bar').filter(  # Because foo.
+        User.query.filter(User.foo == "bar").filter(  # Because foo.
             db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
         ).filter(
             User.xyz.is_(None)
index 7b462ac717c6b8b794ead7c14406e88a8195c07f..fb27b3e55304914e2cb7f3019ed43de7de211280 100644 (file)
@@ -3,19 +3,19 @@ class C:
     def test(self) -> None:
         with patch("black.out", print):
             self.assertEqual(
-                unstyle(str(report)), '1 file reformatted, 1 file failed to reformat.'
+                unstyle(str(report)), "1 file reformatted, 1 file failed to reformat."
             )
             self.assertEqual(
                 unstyle(str(report)),
-                '1 file reformatted, 1 file left unchanged, 1 file failed to reformat.',
+                "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.",
             )
             self.assertEqual(
                 unstyle(str(report)),
-                '2 files reformatted, 1 file left unchanged, '
-                '1 file failed to reformat.',
+                "2 files reformatted, 1 file left unchanged, "
+                "1 file failed to reformat.",
             )
             self.assertEqual(
                 unstyle(str(report)),
-                '2 files reformatted, 2 files left unchanged, '
-                '2 files failed to reformat.',
+                "2 files reformatted, 2 files left unchanged, "
+                "2 files failed to reformat.",
             )
index ec04337670dfd0b22211422d59419ac0e761fff4..d001db42f04768e6226b1fa3348afe427a924acd 100644 (file)
@@ -64,7 +64,7 @@ def g():
         return DOUBLESPACE
 
     # Another comment because more comments
-    assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
+    assert p is not None, f'INTERNAL ERROR: hand-made leaf without parent: {leaf!r}'
 
     prev = leaf.prev_sibling
     if not prev:
@@ -90,9 +90,9 @@ def g():
 
 
 def f():
-    NO = ''
-    SPACE = ' '
-    DOUBLESPACE = '  '
+    NO = ""
+    SPACE = " "
+    DOUBLESPACE = "  "
 
     t = leaf.type
     p = leaf.parent  # trailing comment
@@ -139,9 +139,9 @@ def f():
 
 
 def g():
-    NO = ''
-    SPACE = ' '
-    DOUBLESPACE = '  '
+    NO = ""
+    SPACE = " "
+    DOUBLESPACE = "  "
 
     t = leaf.type
     p = leaf.parent
index 79e7c7ecef70327bba591e6f3057baa6dcb980b4..1e8fa5c960fbefd411454d129f46aa79ec4cc199 100644 (file)
@@ -157,8 +157,8 @@ last_call()
 
 
 ...
-'some_string'
-b'\\xa3'
+"some_string"
+b"\\xa3"
 Name
 None
 True
@@ -193,18 +193,18 @@ flags & ~select.EPOLLIN and waiters.write_task is not None
 lambda arg: None
 lambda a=True: a
 lambda a, b, c=True: a
-lambda a, b, c=True, *, d=(1 << v2), e='str': a
-lambda a, b, c=True, *vararg, d=(v1 << 2), e='str', **kwargs: a + b
+lambda a, b, c=True, *, d=(1 << v2), e="str": a
+lambda a, b, c=True, *vararg, d=(v1 << 2), e="str", **kwargs: a + b
 1 if True else 2
 str or None if True else str or bytes or None
 (str or None) if True else (str or bytes or None)
 str or None if (1 if True else 2) else str or bytes or None
 (str or None) if (1 if True else 2) else (str or bytes or None)
-{'2.7': dead, '3.7': (long_live or die_hard)}
-{'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}}
+{"2.7": dead, "3.7": (long_live or die_hard)}
+{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}}
 {**a, **b, **c}
-{'2.7', '3.6', '3.7', '3.8', '3.9', ('4.0' if gilectomy else '3.10')}
-({'a': 'b'}, (True or False), (+value), 'string', b'bytes') or None
+{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")}
+({"a": "b"}, (True or False), (+value), "string", b"bytes") or None
 ()
 (1,)
 (1, 2)
@@ -214,14 +214,14 @@ str or None if (1 if True else 2) else str or bytes or None
 [1, 2, 3]
 {i for i in (1, 2, 3)}
 {(i ** 2) for i in (1, 2, 3)}
-{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))}
+{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
 {((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
 [i for i in (1, 2, 3)]
 [(i ** 2) for i in (1, 2, 3)]
-[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))]
+[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))]
 [((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
 {i: 0 for i in (1, 2, 3)}
-{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}
+{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))}
 {a: b * 2 for a, b in dictionary.items()}
 {a: b * -2 for a, b in dictionary.items()}
 {
@@ -232,14 +232,14 @@ Python3 > Python2 > COBOL
 Life is Life
 call()
 call(arg)
-call(kwarg='hey')
-call(arg, kwarg='hey')
-call(arg, another, kwarg='hey', **kwargs)
+call(kwarg="hey")
+call(arg, kwarg="hey")
+call(arg, another, kwarg="hey", **kwargs)
 call(
     this_is_a_very_long_variable_which_will_force_a_delimiter_split,
     arg,
     another,
-    kwarg='hey',
+    kwarg="hey",
     **kwargs
 )  # note: no trailing comma pre-3.6
 call(*gidgets[:2])
@@ -283,15 +283,15 @@ numpy[:, l[-2]]
 numpy[:, ::-1]
 numpy[np.newaxis, :]
 (str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None)
-{'2.7': dead, '3.7': long_live or die_hard}
-{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'}
+{"2.7": dead, "3.7": long_live or die_hard}
+{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"}
 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C]
 (SomeName)
 SomeName
 (Good, Bad, Ugly)
 (i for i in (1, 2, 3))
 ((i ** 2) for i in (1, 2, 3))
-((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c')))
+((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c")))
 (((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
 (*starred)
 a = (1,)
index 3e3db11b5fa3fb25ab1b794f07c33b07ec7a4734..4bacfcfd2d3d69d39bc70682a1268d5361361927 100644 (file)
@@ -15,10 +15,10 @@ def func_no_args():
   for i in range(10):
     print(i)
     continue
-  exec("new-style exec", {}, {})
+  exec('new-style exec', {}, {})
   return None
 async def coroutine(arg, exec=False):
- "Single-line docstring. Multiline is harder to reformat."
+ 'Single-line docstring. Multiline is harder to reformat.'
  async with some_connection() as conn:
      await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2)
  await asyncio.sleep(1)
@@ -27,7 +27,7 @@ async def coroutine(arg, exec=False):
 with_args=True,
 many_args=[1,2,3]
 )
-def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str:
+def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str:
  return text[number:-1]
 # fmt: on
 def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''):
@@ -83,7 +83,7 @@ from third_party import X, Y, Z
 
 from library import some_connection, some_decorator
 
-f'trigger 3.6 mode'
+f"trigger 3.6 mode"
 # fmt: off
 def func_no_args():
   a; b; c
@@ -92,10 +92,10 @@ def func_no_args():
   for i in range(10):
     print(i)
     continue
-  exec("new-style exec", {}, {})
+  exec('new-style exec', {}, {})
   return None
 async def coroutine(arg, exec=False):
- "Single-line docstring. Multiline is harder to reformat."
+ 'Single-line docstring. Multiline is harder to reformat.'
  async with some_connection() as conn:
      await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2)
  await asyncio.sleep(1)
@@ -104,12 +104,12 @@ async def coroutine(arg, exec=False):
 with_args=True,
 many_args=[1,2,3]
 )
-def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str:
+def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str:
  return text[number:-1]
 # fmt: on
 
 
-def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''):
+def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""):
     offset = attr.ib(default=attr.Factory(lambda: _r.uniform(10000, 200000)))
     assert task._cancel_stack[:len(old_stack)] == old_stack
 
@@ -123,7 +123,7 @@ def spaces_types(
     f: int = -1,
     g: int = 1 if False else 2,
     h: str = "",
-    i: str = r'',
+    i: str = r"",
 ):
     ...
 
index 6b821beffeca3dc59f92e289a7527d238d649bc3..b288cbc413003446a1c3c0e82e2616a873b9ae95 100644 (file)
@@ -1,5 +1,5 @@
-f'f-string without formatted values is just a string'
-f'{{NOT a formatted value}}'
-f'some f-string with {a} {few():.2f} {formatted.values!r}'
+f"f-string without formatted values is just a string"
+f"{{NOT a formatted value}}"
+f"some f-string with {a} {few():.2f} {formatted.values!r}"
 f"{f'{nested} inner'} outer"
-f'space between opening braces: { {a for a in (1, 2, 3)}}'
+f"space between opening braces: { {a for a in (1, 2, 3)}}"
index 387e441b4d3dbd7a6813ce53e7668ca11e8c58d2..007cc98ace10238fbf44d93832f944817c1f4a57 100644 (file)
@@ -80,7 +80,7 @@ from third_party import X, Y, Z
 
 from library import some_connection, some_decorator
 
-f'trigger 3.6 mode'
+f"trigger 3.6 mode"
 
 
 def func_no_args():
@@ -103,7 +103,7 @@ def func_no_args():
 async def coroutine(arg, exec=False):
     "Single-line docstring. Multiline is harder to reformat."
     async with some_connection() as conn:
-        await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2)
+        await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2)
     await asyncio.sleep(1)
 
 
@@ -120,7 +120,7 @@ def function_signature_stress_test(
     return text[number:-1]
 
 
-def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''):
+def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""):
     offset = attr.ib(default=attr.Factory(lambda: _r.uniform(10000, 200000)))
     assert task._cancel_stack[:len(old_stack)] == old_stack
 
@@ -134,7 +134,7 @@ def spaces_types(
     f: int = -1,
     g: int = 1 if False else 2,
     h: str = "",
-    i: str = r'',
+    i: str = r"",
 ):
     ...
 
diff --git a/tests/string_quotes.py b/tests/string_quotes.py
new file mode 100644 (file)
index 0000000..1080aaf
--- /dev/null
@@ -0,0 +1,37 @@
+"Hello"
+"Don't do that"
+'Here is a "'
+'What\'s the deal here?'
+"What's the deal \"here\"?"
+"And \"here\"?"
+"""Strings with "" in them"""
+'''Strings with "" in them'''
+'''Here's a "'''
+'''Here's a " '''
+'''Just a normal triple
+quote'''
+f"just a normal {f} string"
+f'''This is a triple-quoted {f}-string'''
+f'MOAR {" ".join([])}'
+f"MOAR {' '.join([])}"
+r"raw string ftw"
+
+# output
+
+"Hello"
+"Don't do that"
+'Here is a "'
+"What's the deal here?"
+'What\'s the deal "here"?'
+'And "here"?'
+"""Strings with "" in them"""
+"""Strings with "" in them"""
+'''Here's a "'''
+"""Here's a " """
+"""Just a normal triple
+quote"""
+f"just a normal {f} string"
+f"""This is a triple-quoted {f}-string"""
+f'MOAR {" ".join([])}'
+f"MOAR {' '.join([])}"
+r"raw string ftw"
index 1c22e54db7a03f5addb06bf98b13dad527401893..30ecaf693a4346f0a487074d38969c29ece034d1 100644 (file)
@@ -17,25 +17,25 @@ ff = partial(black.format_file_in_place, line_length=ll, fast=True)
 fs = partial(black.format_str, line_length=ll)
 THIS_FILE = Path(__file__)
 THIS_DIR = THIS_FILE.parent
-EMPTY_LINE = '# EMPTY LINE WITH WHITESPACE' + ' (this comment will be removed)'
+EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)"
 
 
 def dump_to_stderr(*output: str) -> str:
-    return '\n' + '\n'.join(output) + '\n'
+    return "\n" + "\n".join(output) + "\n"
 
 
 def read_data(name: str) -> Tuple[str, str]:
     """read_data('test_name') -> 'input', 'output'"""
-    if not name.endswith(('.py', '.out')):
-        name += '.py'
+    if not name.endswith((".py", ".out")):
+        name += ".py"
     _input: List[str] = []
     _output: List[str] = []
-    with open(THIS_DIR / name, 'r', encoding='utf8') as test:
+    with open(THIS_DIR / name, "r", encoding="utf8") as test:
         lines = test.readlines()
     result = _input
     for line in lines:
-        line = line.replace(EMPTY_LINE, '')
-        if line.rstrip() == '# output':
+        line = line.replace(EMPTY_LINE, "")
+        if line.rstrip() == "# output":
             result = _output
             continue
 
@@ -43,23 +43,23 @@ def read_data(name: str) -> Tuple[str, str]:
     if _input and not _output:
         # If there's no output marker, treat the entire file as already pre-formatted.
         _output = _input[:]
-    return ''.join(_input).strip() + '\n', ''.join(_output).strip() + '\n'
+    return "".join(_input).strip() + "\n", "".join(_output).strip() + "\n"
 
 
 class BlackTestCase(unittest.TestCase):
     maxDiff = None
 
     def assertFormatEqual(self, expected: str, actual: str) -> None:
-        if actual != expected and not os.environ.get('SKIP_AST_PRINT'):
+        if actual != expected and not os.environ.get("SKIP_AST_PRINT"):
             bdv: black.DebugVisitor[Any]
-            black.out('Expected tree:', fg='green')
+            black.out("Expected tree:", fg="green")
             try:
                 exp_node = black.lib2to3_parse(expected)
                 bdv = black.DebugVisitor()
                 list(bdv.visit(exp_node))
             except Exception as ve:
                 black.err(str(ve))
-            black.out('Actual tree:', fg='red')
+            black.out("Actual tree:", fg="red")
             try:
                 exp_node = black.lib2to3_parse(actual)
                 bdv = black.DebugVisitor()
@@ -70,7 +70,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_self(self) -> None:
-        source, expected = read_data('test_black')
+        source, expected = read_data("test_black")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -79,19 +79,19 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_black(self) -> None:
-        source, expected = read_data('../black')
+        source, expected = read_data("../black")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, line_length=ll)
-        self.assertFalse(ff(THIS_DIR / '..' / 'black.py'))
+        self.assertFalse(ff(THIS_DIR / ".." / "black.py"))
 
     def test_piping(self) -> None:
-        source, expected = read_data('../black')
+        source, expected = read_data("../black")
         hold_stdin, hold_stdout = sys.stdin, sys.stdout
         try:
             sys.stdin, sys.stdout = StringIO(source), StringIO()
-            sys.stdin.name = '<stdin>'
+            sys.stdin.name = "<stdin>"
             black.format_stdin_to_stdout(line_length=ll, fast=True, write_back=True)
             sys.stdout.seek(0)
             actual = sys.stdout.read()
@@ -103,16 +103,16 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_setup(self) -> None:
-        source, expected = read_data('../setup')
+        source, expected = read_data("../setup")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, line_length=ll)
-        self.assertFalse(ff(THIS_DIR / '..' / 'setup.py'))
+        self.assertFalse(ff(THIS_DIR / ".." / "setup.py"))
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_function(self) -> None:
-        source, expected = read_data('function')
+        source, expected = read_data("function")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -120,7 +120,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_expression(self) -> None:
-        source, expected = read_data('expression')
+        source, expected = read_data("expression")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -128,7 +128,15 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_fstring(self) -> None:
-        source, expected = read_data('fstring')
+        source, expected = read_data("fstring")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, line_length=ll)
+
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_string_quotes(self) -> None:
+        source, expected = read_data("string_quotes")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -136,7 +144,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_comments(self) -> None:
-        source, expected = read_data('comments')
+        source, expected = read_data("comments")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -144,7 +152,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_comments2(self) -> None:
-        source, expected = read_data('comments2')
+        source, expected = read_data("comments2")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -152,7 +160,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_comments3(self) -> None:
-        source, expected = read_data('comments3')
+        source, expected = read_data("comments3")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -160,7 +168,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_comments4(self) -> None:
-        source, expected = read_data('comments4')
+        source, expected = read_data("comments4")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -168,7 +176,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_cantfit(self) -> None:
-        source, expected = read_data('cantfit')
+        source, expected = read_data("cantfit")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -176,7 +184,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_import_spacing(self) -> None:
-        source, expected = read_data('import_spacing')
+        source, expected = read_data("import_spacing")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -184,7 +192,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_composition(self) -> None:
-        source, expected = read_data('composition')
+        source, expected = read_data("composition")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -192,7 +200,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_empty_lines(self) -> None:
-        source, expected = read_data('empty_lines')
+        source, expected = read_data("empty_lines")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -200,7 +208,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_python2(self) -> None:
-        source, expected = read_data('python2')
+        source, expected = read_data("python2")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         # black.assert_equivalent(source, actual)
@@ -208,7 +216,7 @@ class BlackTestCase(unittest.TestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_fmtonoff(self) -> None:
-        source, expected = read_data('fmtonoff')
+        source, expected = read_data("fmtonoff")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -226,68 +234,68 @@ class BlackTestCase(unittest.TestCase):
             err_lines.append(msg)
 
         with patch("black.out", out), patch("black.err", err):
-            report.done(Path('f1'), changed=False)
+            report.done(Path("f1"), changed=False)
             self.assertEqual(len(out_lines), 1)
             self.assertEqual(len(err_lines), 0)
-            self.assertEqual(out_lines[-1], 'f1 already well formatted, good job.')
-            self.assertEqual(unstyle(str(report)), '1 file left unchanged.')
+            self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
+            self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
             self.assertEqual(report.return_code, 0)
-            report.done(Path('f2'), changed=True)
+            report.done(Path("f2"), changed=True)
             self.assertEqual(len(out_lines), 2)
             self.assertEqual(len(err_lines), 0)
-            self.assertEqual(out_lines[-1], 'reformatted f2')
+            self.assertEqual(out_lines[-1], "reformatted f2")
             self.assertEqual(
-                unstyle(str(report)), '1 file reformatted, 1 file left unchanged.'
+                unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
             )
             self.assertEqual(report.return_code, 0)
             report.check = True
             self.assertEqual(report.return_code, 1)
             report.check = False
-            report.failed(Path('e1'), 'boom')
+            report.failed(Path("e1"), "boom")
             self.assertEqual(len(out_lines), 2)
             self.assertEqual(len(err_lines), 1)
-            self.assertEqual(err_lines[-1], 'error: cannot format e1: boom')
+            self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
             self.assertEqual(
                 unstyle(str(report)),
-                '1 file reformatted, 1 file left unchanged, '
-                '1 file failed to reformat.',
+                "1 file reformatted, 1 file left unchanged, "
+                "1 file failed to reformat.",
             )
             self.assertEqual(report.return_code, 123)
-            report.done(Path('f3'), changed=True)
+            report.done(Path("f3"), changed=True)
             self.assertEqual(len(out_lines), 3)
             self.assertEqual(len(err_lines), 1)
-            self.assertEqual(out_lines[-1], 'reformatted f3')
+            self.assertEqual(out_lines[-1], "reformatted f3")
             self.assertEqual(
                 unstyle(str(report)),
-                '2 files reformatted, 1 file left unchanged, '
-                '1 file failed to reformat.',
+                "2 files reformatted, 1 file left unchanged, "
+                "1 file failed to reformat.",
             )
             self.assertEqual(report.return_code, 123)
-            report.failed(Path('e2'), 'boom')
+            report.failed(Path("e2"), "boom")
             self.assertEqual(len(out_lines), 3)
             self.assertEqual(len(err_lines), 2)
-            self.assertEqual(err_lines[-1], 'error: cannot format e2: boom')
+            self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
             self.assertEqual(
                 unstyle(str(report)),
-                '2 files reformatted, 1 file left unchanged, '
-                '2 files failed to reformat.',
+                "2 files reformatted, 1 file left unchanged, "
+                "2 files failed to reformat.",
             )
             self.assertEqual(report.return_code, 123)
-            report.done(Path('f4'), changed=False)
+            report.done(Path("f4"), changed=False)
             self.assertEqual(len(out_lines), 4)
             self.assertEqual(len(err_lines), 2)
-            self.assertEqual(out_lines[-1], 'f4 already well formatted, good job.')
+            self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
             self.assertEqual(
                 unstyle(str(report)),
-                '2 files reformatted, 2 files left unchanged, '
-                '2 files failed to reformat.',
+                "2 files reformatted, 2 files left unchanged, "
+                "2 files failed to reformat.",
             )
             self.assertEqual(report.return_code, 123)
             report.check = True
             self.assertEqual(
                 unstyle(str(report)),
-                '2 files would be reformatted, 2 files would be left unchanged, '
-                '2 files would fail to reformat.',
+                "2 files would be reformatted, 2 files would be left unchanged, "
+                "2 files would fail to reformat.",
             )
 
     def test_is_python36(self) -> None:
@@ -297,20 +305,20 @@ class BlackTestCase(unittest.TestCase):
         self.assertTrue(black.is_python36(node))
         node = black.lib2to3_parse("def f(*, arg): f'string'\n")
         self.assertTrue(black.is_python36(node))
-        source, expected = read_data('function')
+        source, expected = read_data("function")
         node = black.lib2to3_parse(source)
         self.assertTrue(black.is_python36(node))
         node = black.lib2to3_parse(expected)
         self.assertTrue(black.is_python36(node))
-        source, expected = read_data('expression')
+        source, expected = read_data("expression")
         node = black.lib2to3_parse(source)
         self.assertFalse(black.is_python36(node))
         node = black.lib2to3_parse(expected)
         self.assertFalse(black.is_python36(node))
 
     def test_debug_visitor(self) -> None:
-        source, _ = read_data('debug_visitor.py')
-        expected, _ = read_data('debug_visitor.out')
+        source, _ = read_data("debug_visitor.py")
+        expected, _ = read_data("debug_visitor.out")
         out_lines = []
         err_lines = []
 
@@ -322,8 +330,8 @@ class BlackTestCase(unittest.TestCase):
 
         with patch("black.out", out), patch("black.err", err):
             black.DebugVisitor.show(source)
-        actual = '\n'.join(out_lines) + '\n'
-        log_name = ''
+        actual = "\n".join(out_lines) + "\n"
+        log_name = ""
         if expected != actual:
             log_name = black.dump_to_file(*out_lines)
         self.assertEqual(
@@ -333,5 +341,5 @@ class BlackTestCase(unittest.TestCase):
         )
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()