]> 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:

Use the separate pass for `# fmt: off` on all code
authorŁukasz Langa <lukasz@langa.pl>
Wed, 20 Jun 2018 00:32:41 +0000 (17:32 -0700)
committerŁukasz Langa <lukasz@langa.pl>
Wed, 20 Jun 2018 00:32:41 +0000 (17:32 -0700)
This removes the hacky exception-based handling that didn't work across
statement boundaries.

Fixes #335

README.md
black.py
tests/data/fmtonoff2.py [new file with mode: 0644]
tests/test_black.py

index 67ff18aff76d57c793b54614e9bfa4debb5f5d22..716a14edfb07921586998d711c9d35b5fc8cb104 100644 (file)
--- a/README.md
+++ b/README.md
@@ -824,7 +824,11 @@ More details can be found in [CONTRIBUTING](CONTRIBUTING.md).
 
 * typing stub files (`.pyi`) now have blank lines added after constants (#340)
 
-* `# fmt: off` and `# fmt: on` now work also within bracket pairs (#329)
+* `# fmt: off` and `# fmt: on` are now much more dependable:
+
+  * they now work also within bracket pairs (#329)
+
+  * they now correctly work across function/class boundaries (#335)
 
 * fixed improper formatting of f-strings with quotes inside interpolated
   expressions (#322)
index 7682f7c393776a1b75be292e36f6620fed8925a3..7e39c92009d2aefcf3b2910f9dce1fcca8b2cbe2 100644 (file)
--- a/black.py
+++ b/black.py
@@ -29,7 +29,6 @@ from typing import (
     Sequence,
     Set,
     Tuple,
-    Type,
     TypeVar,
     Union,
     cast,
@@ -90,34 +89,6 @@ class CannotSplit(Exception):
     """
 
 
-class FormatError(Exception):
-    """Base exception for `# fmt: on` and `# fmt: off` handling.
-
-    It holds the number of bytes of the prefix consumed before the format
-    control comment appeared.
-    """
-
-    def __init__(self, consumed: int) -> None:
-        super().__init__(consumed)
-        self.consumed = consumed
-
-    def trim_prefix(self, leaf: Leaf) -> None:
-        leaf.prefix = leaf.prefix[self.consumed :]
-
-    def leaf_from_consumed(self, leaf: Leaf) -> Leaf:
-        """Returns a new Leaf from the consumed part of the prefix."""
-        unformatted_prefix = leaf.prefix[: self.consumed]
-        return Leaf(token.NEWLINE, unformatted_prefix)
-
-
-class FormatOn(FormatError):
-    """Found a comment like `# fmt: on` in the file."""
-
-
-class FormatOff(FormatError):
-    """Found a comment like `# fmt: off` in the file."""
-
-
 class WriteBack(Enum):
     NO = 0
     YES = 1
@@ -759,13 +730,15 @@ class DebugVisitor(Visitor[T]):
             out(f" {node.value!r}", fg="blue", bold=False)
 
     @classmethod
-    def show(cls, code: str) -> None:
+    def show(cls, code: Union[str, Leaf, Node]) -> None:
         """Pretty-print the lib2to3 AST of a given string of `code`.
 
         Convenience method for debugging.
         """
         v: DebugVisitor[None] = DebugVisitor()
-        list(v.visit(lib2to3_parse(code)))
+        if isinstance(code, str):
+            code = lib2to3_parse(code)
+        list(v.visit(code))
 
 
 KEYWORDS = set(keyword.kwlist)
@@ -1306,55 +1279,6 @@ class Line:
         return bool(self.leaves or self.comments)
 
 
-class UnformattedLines(Line):
-    """Just like :class:`Line` but stores lines which aren't reformatted."""
-
-    def append(self, leaf: Leaf, preformatted: bool = True) -> None:
-        """Just add a new `leaf` to the end of the lines.
-
-        The `preformatted` argument is ignored.
-
-        Keeps track of indentation `depth`, which is useful when the user
-        says `# fmt: on`. Otherwise, doesn't do anything with the `leaf`.
-        """
-        try:
-            list(generate_comments(leaf))
-        except FormatOn as f_on:
-            self.leaves.append(f_on.leaf_from_consumed(leaf))
-            raise
-
-        self.leaves.append(leaf)
-        if leaf.type == token.INDENT:
-            self.depth += 1
-        elif leaf.type == token.DEDENT:
-            self.depth -= 1
-
-    def __str__(self) -> str:
-        """Render unformatted lines from leaves which were added with `append()`.
-
-        `depth` is not used for indentation in this case.
-        """
-        if not self:
-            return "\n"
-
-        res = ""
-        for leaf in self.leaves:
-            res += str(leaf)
-        return res
-
-    def append_comment(self, comment: Leaf) -> bool:
-        """Not implemented in this class. Raises `NotImplementedError`."""
-        raise NotImplementedError("Unformatted lines don't store comments separately.")
-
-    def maybe_remove_trailing_comma(self, closing: Leaf) -> bool:
-        """Does nothing and returns False."""
-        return False
-
-    def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool:
-        """Does nothing and returns False."""
-        return False
-
-
 @dataclass
 class EmptyLineTracker:
     """Provides a stateful method that returns the number of potential extra
@@ -1376,9 +1300,6 @@ class EmptyLineTracker:
         This is for separating `def`, `async def` and `class` with extra empty
         lines (two on module-level).
         """
-        if isinstance(current_line, UnformattedLines):
-            return 0, 0
-
         before, after = self._maybe_empty_lines(current_line)
         before -= self.previous_after
         self.previous_after = after
@@ -1482,7 +1403,7 @@ class LineGenerator(Visitor[Line]):
     current_line: Line = Factory(Line)
     remove_u_prefix: bool = False
 
-    def line(self, indent: int = 0, type: Type[Line] = Line) -> Iterator[Line]:
+    def line(self, indent: int = 0) -> Iterator[Line]:
         """Generate a line.
 
         If the line is empty, only emit if it makes sense.
@@ -1491,67 +1412,39 @@ class LineGenerator(Visitor[Line]):
         If any lines were generated, set up a new current_line.
         """
         if not self.current_line:
-            if self.current_line.__class__ == type:
-                self.current_line.depth += indent
-            else:
-                self.current_line = type(depth=self.current_line.depth + indent)
+            self.current_line.depth += indent
             return  # Line is empty, don't emit. Creating a new one unnecessary.
 
         complete_line = self.current_line
-        self.current_line = type(depth=complete_line.depth + indent)
+        self.current_line = Line(depth=complete_line.depth + indent)
         yield complete_line
 
-    def visit(self, node: LN) -> Iterator[Line]:
-        """Main method to visit `node` and its children.
-
-        Yields :class:`Line` objects.
-        """
-        if isinstance(self.current_line, UnformattedLines):
-            # File contained `# fmt: off`
-            yield from self.visit_unformatted(node)
-
-        else:
-            yield from super().visit(node)
-
     def visit_default(self, node: LN) -> Iterator[Line]:
         """Default `visit_*()` implementation. Recurses to children of `node`."""
         if isinstance(node, Leaf):
             any_open_brackets = self.current_line.bracket_tracker.any_open_brackets()
-            try:
-                for comment in generate_comments(node):
-                    if any_open_brackets:
-                        # any comment within brackets is subject to splitting
-                        self.current_line.append(comment)
-                    elif comment.type == token.COMMENT:
-                        # regular trailing comment
-                        self.current_line.append(comment)
-                        yield from self.line()
-
-                    else:
-                        # regular standalone comment
-                        yield from self.line()
-
-                        self.current_line.append(comment)
-                        yield from self.line()
-
-            except FormatOff as f_off:
-                f_off.trim_prefix(node)
-                yield from self.line(type=UnformattedLines)
-                yield from self.visit(node)
-
-            except FormatOn as f_on:
-                # This only happens here if somebody says "fmt: on" multiple
-                # times in a row.
-                f_on.trim_prefix(node)
-                yield from self.visit_default(node)
+            for comment in generate_comments(node):
+                if any_open_brackets:
+                    # any comment within brackets is subject to splitting
+                    self.current_line.append(comment)
+                elif comment.type == token.COMMENT:
+                    # regular trailing comment
+                    self.current_line.append(comment)
+                    yield from self.line()
 
-            else:
-                normalize_prefix(node, inside_brackets=any_open_brackets)
-                if self.normalize_strings and node.type == token.STRING:
-                    normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix)
-                    normalize_string_quotes(node)
-                if node.type not in WHITESPACE:
-                    self.current_line.append(node)
+                else:
+                    # regular standalone comment
+                    yield from self.line()
+
+                    self.current_line.append(comment)
+                    yield from self.line()
+
+            normalize_prefix(node, inside_brackets=any_open_brackets)
+            if self.normalize_strings and node.type == token.STRING:
+                normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix)
+                normalize_string_quotes(node)
+            if node.type not in WHITESPACE:
+                self.current_line.append(node)
         yield from super().visit_default(node)
 
     def visit_INDENT(self, node: Node) -> Iterator[Line]:
@@ -1648,23 +1541,10 @@ class LineGenerator(Visitor[Line]):
         yield from self.visit_default(leaf)
         yield from self.line()
 
-    def visit_unformatted(self, node: LN) -> Iterator[Line]:
-        """Used when file contained a `# fmt: off`."""
-        if isinstance(node, Node):
-            for child in node.children:
-                yield from self.visit(child)
-
-        else:
-            try:
-                self.current_line.append(node)
-            except FormatOn as f_on:
-                f_on.trim_prefix(node)
-                yield from self.line()
-                yield from self.visit(node)
-
-            if node.type == token.ENDMARKER:
-                # somebody decided not to put a final `# fmt: on`
-                yield from self.line()
+    def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]:
+        if not self.current_line.bracket_tracker.any_open_brackets():
+            yield from self.line()
+        yield from self.visit_default(leaf)
 
     def __attrs_post_init__(self) -> None:
         """You are in a twisty little maze of passages."""
@@ -1969,6 +1849,9 @@ def container_of(leaf: Leaf) -> LN:
         if parent.children[0].prefix != same_prefix:
             break
 
+        if parent.type == syms.file_input:
+            break
+
         if parent.type in SURROUNDED_BY_BRACKETS:
             break
 
@@ -2106,16 +1989,6 @@ def generate_comments(leaf: LN) -> Iterator[Leaf]:
     """
     for pc in list_comments(leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER):
         yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines)
-        if pc.value in FMT_ON:
-            raise FormatOn(pc.consumed)
-
-        if pc.value in FMT_OFF:
-            if pc.type == STANDALONE_COMMENT:
-                raise FormatOff(pc.consumed)
-
-            prev = preceding_leaf(leaf)
-            if not prev or prev.type in WHITESPACE:  # standalone comment in disguise
-                raise FormatOff(pc.consumed)
 
 
 @dataclass
@@ -2188,7 +2061,7 @@ def split_line(
     If `py36` is True, splitting may generate syntax that is only compatible
     with Python 3.6 and later.
     """
-    if isinstance(line, UnformattedLines) or line.is_comment:
+    if line.is_comment:
         yield line
         return
 
@@ -2680,28 +2553,29 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None:
 
 
 def normalize_fmt_off(node: Node) -> None:
-    """Allow `# fmt: off`/`# fmt: on` within bracket pairs.
-
-    Ignores `# fmt: off` and `# fmt: on` outside of brackets.
-
-    Raises :exc:`SyntaxError` if no matching `# fmt: on` is found for a `# fmt: off`
-    given inside brackets.
-    """
+    """Convert content between `# fmt: off`/`# fmt: on` into standalone comments."""
     try_again = True
     while try_again:
-        try_again = hide_fmt_off(node)
+        try_again = convert_one_fmt_off_pair(node)
 
 
-def hide_fmt_off(node: Node) -> bool:
-    bt = BracketTracker()
-    for leaf in node.leaves():
-        bt.mark(leaf)
-        if bt.depth == 0:
-            continue
+def convert_one_fmt_off_pair(node: Node) -> bool:
+    """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment.
 
+    Returns True if a pair was converted.
+    """
+    for leaf in node.leaves():
         previous_consumed = 0
         for comment in list_comments(leaf.prefix, is_endmarker=False):
             if comment.value in FMT_OFF:
+                # We only want standalone comments. If there's no previous leaf or
+                # the previous leaf is indentation, it's a standalone comment in
+                # disguise.
+                if comment.type != STANDALONE_COMMENT:
+                    prev = preceding_leaf(leaf)
+                    if prev and prev.type not in WHITESPACE:
+                        continue
+
                 ignored_nodes = list(generate_ignored_nodes(leaf))
                 first = ignored_nodes[0]  # Can be a container node with the `leaf`.
                 parent = first.parent
@@ -2710,6 +2584,10 @@ def hide_fmt_off(node: Node) -> bool:
                 hidden_value = (
                     comment.value + "\n" + "".join(str(n) for n in ignored_nodes)
                 )
+                if hidden_value.endswith("\n"):
+                    # That happens when one of the `ignored_nodes` ended with a NEWLINE
+                    # leaf (possibly followed by a DEDENT).
+                    hidden_value = hidden_value[:-1]
                 first_idx = None
                 for ignored in ignored_nodes:
                     index = ignored.remove()
@@ -2733,8 +2611,12 @@ def hide_fmt_off(node: Node) -> bool:
 
 
 def generate_ignored_nodes(leaf: Leaf) -> Iterator[LN]:
+    """Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
+
+    Stops at the end of the block.
+    """
     container: Optional[LN] = container_of(leaf)
-    while container is not None:
+    while container is not None and container.type != token.ENDMARKER:
         for comment in list_comments(container.prefix, is_endmarker=False):
             if comment.value in FMT_ON:
                 return
diff --git a/tests/data/fmtonoff2.py b/tests/data/fmtonoff2.py
new file mode 100644 (file)
index 0000000..4309506
--- /dev/null
@@ -0,0 +1,31 @@
+import pytest
+
+TmSt = 1
+TmEx = 2
+
+# fmt: off
+
+# Test data:
+#   Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]]
+
+@pytest.mark.parametrize('test', [
+
+    # Test don't manage the volume
+    [
+        ('stuff', 'in')
+    ],
+])
+def test_fader(test):
+    pass
+
+def check_fader(test):
+    pass
+
+def test_calculate_fades():
+    calcs = [
+        # one is zero/none
+        (0, 4, 0, 0, 10,        0, 0, 6, 10),
+        (None, 4, 0, 0, 10,     0, 0, 6, 10),
+    ]
+
+# fmt: on
index 3418df9f5bec92327b42791420c5ae856ef9a691..6638dc4f119f83ab4c2cd3e2e14272362f803813 100644 (file)
@@ -400,6 +400,14 @@ class BlackTestCase(unittest.TestCase):
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, line_length=ll)
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_fmtonoff2(self) -> None:
+        source, expected = read_data("fmtonoff2")
+        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_remove_empty_parentheses_after_class(self) -> None:
         source, expected = read_data("class_blank_parentheses")