From: Ɓukasz Langa Date: Tue, 19 Jun 2018 01:14:10 +0000 (-0700) Subject: Support `# fmt: off/on` pairs within brackets X-Git-Url: https://git.madduck.net/etc/vim.git/commitdiff_plain/8a8c58252cc023ae250d6febd24f50a8166450d4?ds=sidebyside;hp=013cb2b374cc6d649d25bc35ba592a8f88f484df Support `# fmt: off/on` pairs within brackets Fixes #329 --- diff --git a/README.md b/README.md index bc27a8a..67ff18a 100644 --- a/README.md +++ b/README.md @@ -824,6 +824,8 @@ 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) + * fixed improper formatting of f-strings with quotes inside interpolated expressions (#322) diff --git a/black.py b/black.py index f51b10e..7682f7c 100644 --- a/black.py +++ b/black.py @@ -628,6 +628,7 @@ def format_str( is_pyi = bool(mode & FileMode.PYI) py36 = bool(mode & FileMode.PYTHON36) or is_python36(src_node) normalize_strings = not bool(mode & FileMode.NO_STRING_NORMALIZATION) + normalize_fmt_off(src_node) lines = LineGenerator( remove_u_prefix=py36 or "unicode_literals" in future_imports, is_pyi=is_pyi, @@ -781,6 +782,7 @@ STATEMENT = { syms.classdef, } STANDALONE_COMMENT = 153 +token.tok_name[STANDALONE_COMMENT] = "STANDALONE_COMMENT" LOGIC_OPERATORS = {"and", "or"} COMPARATORS = { token.LESS, @@ -821,6 +823,18 @@ UNPACKING_PARENTS = { syms.testlist_gexp, syms.testlist_star_expr, } +SURROUNDED_BY_BRACKETS = { + syms.typedargslist, + syms.arglist, + syms.subscriptlist, + syms.vfplist, + syms.import_as_names, + syms.yield_expr, + syms.testlist_gexp, + syms.testlist_star_expr, + syms.listmaker, + syms.dictsetmaker, +} TEST_DESCENDANTS = { syms.test, syms.lambdef, @@ -1940,6 +1954,28 @@ def child_towards(ancestor: Node, descendant: LN) -> Optional[LN]: return node +def container_of(leaf: Leaf) -> LN: + """Return `leaf` or one of its ancestors that is the topmost container of it. + + By "container" we mean a node where `leaf` is the very first child. + """ + same_prefix = leaf.prefix + container: LN = leaf + while container: + parent = container.parent + if parent is None: + break + + if parent.children[0].prefix != same_prefix: + break + + if parent.type in SURROUNDED_BY_BRACKETS: + break + + container = parent + return container + + def is_split_after_delimiter(leaf: Leaf, previous: Leaf = None) -> int: """Return the priority of the `leaf` delimiter, given a line break after it. @@ -2091,7 +2127,7 @@ class ProtoComment: @lru_cache(maxsize=4096) -def list_comments(prefix: str, is_endmarker: bool) -> List[ProtoComment]: +def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: result: List[ProtoComment] = [] if not prefix or "#" not in prefix: return result @@ -2643,6 +2679,71 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None: check_lpar = isinstance(child, Leaf) and child.value in parens_after +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. + """ + try_again = True + while try_again: + try_again = hide_fmt_off(node) + + +def hide_fmt_off(node: Node) -> bool: + bt = BracketTracker() + for leaf in node.leaves(): + bt.mark(leaf) + if bt.depth == 0: + continue + + previous_consumed = 0 + for comment in list_comments(leaf.prefix, is_endmarker=False): + if comment.value in FMT_OFF: + ignored_nodes = list(generate_ignored_nodes(leaf)) + first = ignored_nodes[0] # Can be a container node with the `leaf`. + parent = first.parent + prefix = first.prefix + first.prefix = prefix[comment.consumed :] + hidden_value = ( + comment.value + "\n" + "".join(str(n) for n in ignored_nodes) + ) + first_idx = None + for ignored in ignored_nodes: + index = ignored.remove() + if first_idx is None: + first_idx = index + assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)" + assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)" + parent.insert_child( + first_idx, + Leaf( + STANDALONE_COMMENT, + hidden_value, + prefix=prefix[:previous_consumed] + "\n" * comment.newlines, + ), + ) + return True + + previous_consumed += comment.consumed + + return False + + +def generate_ignored_nodes(leaf: Leaf) -> Iterator[LN]: + container: Optional[LN] = container_of(leaf) + while container is not None: + for comment in list_comments(container.prefix, is_endmarker=False): + if comment.value in FMT_ON: + return + + yield container + + container = container.next_sibling + + def maybe_make_parens_invisible_in_atom(node: LN) -> bool: """If it's safe, make the parens in the atom `node` invisible, recursively.""" if ( diff --git a/tests/data/fmtonoff.py b/tests/data/fmtonoff.py index 5666dd8..373e5c7 100644 --- a/tests/data/fmtonoff.py +++ b/tests/data/fmtonoff.py @@ -40,6 +40,10 @@ def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r'' def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... def spaces2(result= _core.Value(None)): ... +something = { + # fmt: off + key: 'value', +} def example(session): # fmt: off result = session\ @@ -78,10 +82,11 @@ def long_lines(): \n? ) $ - """, # fmt: off - re.MULTILINE | re.VERBOSE + """, + # fmt: off + re.MULTILINE|re.VERBOSE + # fmt: on ) - # fmt: on def single_literal_yapf_disable(): """Black does not support this.""" BAZ = { @@ -89,6 +94,28 @@ def single_literal_yapf_disable(): (5, 6, 7, 8), (9, 10, 11, 12), } # yapf: disable +cfg.rule( + "Default", "address", + xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], + xxxxxx="xx_xxxxx", xxxxxxx="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + xxxxxxxxx_xxxx=True, xxxxxxxx_xxxxxxxxxx=False, + xxxxxx_xxxxxx=2, xxxxxx_xxxxx_xxxxxxxx=70, xxxxxx_xxxxxx_xxxxx=True, + # fmt: off + xxxxxxx_xxxxxxxxxxxx={ + "xxxxxxxx": { + "xxxxxx": False, + "xxxxxxx": False, + "xxxx_xxxxxx": "xxxxx", + }, + "xxxxxxxx-xxxxx": { + "xxxxxx": False, + "xxxxxxx": True, + "xxxx_xxxxxx": "xxxxxx", + }, + }, + # fmt: on + xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5 +) # fmt: off # No formatting to the end of the file l=[1,2,3] @@ -157,6 +184,12 @@ def spaces2(result=_core.Value(None)): ... +something = { + # fmt: off + key: 'value', +} + + def example(session): # fmt: off result = session\ @@ -202,10 +235,11 @@ def long_lines(): \n? ) $ - """, # fmt: off - re.MULTILINE | re.VERBOSE, + """, + # fmt: off + re.MULTILINE|re.VERBOSE + # fmt: on ) - # fmt: on def single_literal_yapf_disable(): @@ -213,6 +247,33 @@ def single_literal_yapf_disable(): BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable +cfg.rule( + "Default", + "address", + xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], + xxxxxx="xx_xxxxx", + xxxxxxx="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + xxxxxxxxx_xxxx=True, + xxxxxxxx_xxxxxxxxxx=False, + xxxxxx_xxxxxx=2, + xxxxxx_xxxxx_xxxxxxxx=70, + xxxxxx_xxxxxx_xxxxx=True, + # fmt: off + xxxxxxx_xxxxxxxxxxxx={ + "xxxxxxxx": { + "xxxxxx": False, + "xxxxxxx": False, + "xxxx_xxxxxx": "xxxxx", + }, + "xxxxxxxx-xxxxx": { + "xxxxxx": False, + "xxxxxxx": True, + "xxxx_xxxxxx": "xxxxxx", + }, + }, + # fmt: on + xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, +) # fmt: off # No formatting to the end of the file l=[1,2,3]