Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --skip-magic-trailing-comma #1824

Merged
merged 10 commits into from Jan 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 12 additions & 7 deletions README.md
Expand Up @@ -97,6 +97,10 @@ Options:

-S, --skip-string-normalization
Don't normalize string quotes or prefixes.
-C, --skip-magic-trailing-comma
Don't use trailing commas as a reason to
split lines.

--check Don't write the files back, just return the
status. Return code 0 means nothing would
change. Return code 1 means some files
Expand Down Expand Up @@ -127,18 +131,19 @@ Options:
paths are excluded. Use forward slashes for
directories on all platforms (Windows, too).
Exclusions are calculated first, inclusions
later. [default: /(\.eggs|\.git|\.hg|\.mypy
_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-
out|build|dist)/]
later. [default: /(\.direnv|\.eggs|\.git|\.
hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu
ild|buck-out|build|dist)/]

--force-exclude TEXT Like --exclude, but files and directories
matching this regex will be excluded even
when they are passed explicitly as arguments.
when they are passed explicitly as
arguments.

--stdin-filename TEXT The name of the file when passing it through
stdin. Useful to make sure Black will respect
--force-exclude option on some editors that
rely on using stdin.
stdin. Useful to make sure Black will
respect --force-exclude option on some
editors that rely on using stdin.

-q, --quiet Don't emit non-error messages to stderr.
Errors are still emitted; silence those with
Expand Down
3 changes: 3 additions & 0 deletions docs/blackd.md
Expand Up @@ -54,6 +54,9 @@ The headers controlling how source code is formatted are:
- `X-Skip-String-Normalization`: corresponds to the `--skip-string-normalization`
command line flag. If present and its value is not the empty string, no string
normalization will be performed.
- `X-Skip-Magic-Trailing-Comma`: corresponds to the `--skip-magic-trailing-comma`
command line flag. If present and its value is not the empty string, trailing commas
will not be used as a reason to split lines.
- `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as _Black_ does when passed the
`--fast` command line flag.
- `X-Python-Variant`: if set to `pyi`, `blackd` will act as _Black_ does when passed the
Expand Down
18 changes: 14 additions & 4 deletions docs/installation_and_usage.md
Expand Up @@ -52,6 +52,10 @@ Options:

-S, --skip-string-normalization
Don't normalize string quotes or prefixes.
-C, --skip-magic-trailing-comma
Don't use trailing commas as a reason to
split lines.

--check Don't write the files back, just return the
status. Return code 0 means nothing would
change. Return code 1 means some files
Expand Down Expand Up @@ -82,13 +86,19 @@ Options:
paths are excluded. Use forward slashes for
directories on all platforms (Windows, too).
Exclusions are calculated first, inclusions
later. [default: /(\.eggs|\.git|\.hg|\.mypy
_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-
out|build|dist)/]
later. [default: /(\.direnv|\.eggs|\.git|\.
hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu
ild|buck-out|build|dist)/]

--force-exclude TEXT Like --exclude, but files and directories
matching this regex will be excluded even
when they are passed explicitly as arguments.
when they are passed explicitly as
arguments.

--stdin-filename TEXT The name of the file when passing it through
stdin. Useful to make sure Black will
respect --force-exclude option on some
editors that rely on using stdin.

-q, --quiet Don't emit non-error messages to stderr.
Errors are still emitted; silence those with
Expand Down
3 changes: 3 additions & 0 deletions docs/the_black_code_style.md
Expand Up @@ -438,6 +438,9 @@ into one item per line.
How do you make it stop? Just delete that trailing comma and _Black_ will collapse your
collection into one line if it fits.

If you must, you can recover the behaviour of early versions of Black with the option
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might need to be consistent here at some point:

(py38) mbpt-jelle:black jelle$ git grep behavior | wc -l
       7
(py38) mbpt-jelle:black jelle$ git grep behaviour | wc -l
       7

`--skip-magic-trailing-comma` / `-C`.

### r"strings" and R"strings"

_Black_ normalizes string quotes as well as string prefixes, making them lowercase. One
Expand Down
89 changes: 64 additions & 25 deletions src/black/__init__.py
Expand Up @@ -256,6 +256,7 @@ class Mode:
target_versions: Set[TargetVersion] = field(default_factory=set)
line_length: int = DEFAULT_LINE_LENGTH
string_normalization: bool = True
magic_trailing_comma: bool = True
experimental_string_processing: bool = False
is_pyi: bool = False

Expand Down Expand Up @@ -393,6 +394,12 @@ def target_version_option_callback(
is_flag=True,
help="Don't normalize string quotes or prefixes.",
)
@click.option(
"-C",
"--skip-magic-trailing-comma",
is_flag=True,
help="Don't use trailing commas as a reason to split lines.",
)
@click.option(
"--experimental-string-processing",
is_flag=True,
Expand Down Expand Up @@ -520,6 +527,7 @@ def main(
fast: bool,
pyi: bool,
skip_string_normalization: bool,
skip_magic_trailing_comma: bool,
experimental_string_processing: bool,
quiet: bool,
verbose: bool,
Expand All @@ -542,6 +550,7 @@ def main(
line_length=line_length,
is_pyi=pyi,
string_normalization=not skip_string_normalization,
magic_trailing_comma=not skip_magic_trailing_comma,
experimental_string_processing=experimental_string_processing,
)
if config and verbose:
Expand Down Expand Up @@ -1018,13 +1027,12 @@ def f(
versions = detect_target_versions(src_node)
normalize_fmt_off(src_node)
lines = LineGenerator(
mode=mode,
remove_u_prefix="unicode_literals" in future_imports
or supports_feature(versions, Feature.UNICODE_LITERALS),
is_pyi=mode.is_pyi,
normalize_strings=mode.string_normalization,
)
elt = EmptyLineTracker(is_pyi=mode.is_pyi)
empty_line = Line()
empty_line = Line(mode=mode)
after = 0
split_line_features = {
feature
Expand Down Expand Up @@ -1460,6 +1468,7 @@ def get_open_lsqb(self) -> Optional[Leaf]:
class Line:
"""Holds leaves and comments. Can be printed with `str(line)`."""

mode: Mode
depth: int = 0
leaves: List[Leaf] = field(default_factory=list)
# keys ordered like `leaves`
Expand Down Expand Up @@ -1492,8 +1501,11 @@ def append(self, leaf: Leaf, preformatted: bool = False) -> None:
)
if self.inside_brackets or not preformatted:
self.bracket_tracker.mark(leaf)
if self.maybe_should_explode(leaf):
self.should_explode = True
if self.mode.magic_trailing_comma:
if self.has_magic_trailing_comma(leaf):
self.should_explode = True
elif self.has_magic_trailing_comma(leaf, ensure_removable=True):
self.remove_trailing_comma()
if not self.append_comment(leaf):
self.leaves.append(leaf)

Expand Down Expand Up @@ -1669,10 +1681,14 @@ def contains_unsplittable_type_ignore(self) -> bool:
def contains_multiline_strings(self) -> bool:
return any(is_multiline_string(leaf) for leaf in self.leaves)

def maybe_should_explode(self, closing: Leaf) -> bool:
"""Return True if this line should explode (always be split), that is when:
- there's a trailing comma here; and
- it's not a one-tuple.
def has_magic_trailing_comma(
self, closing: Leaf, ensure_removable: bool = False
) -> bool:
"""Return True if we have a magic trailing comma, that is when:
- there's a trailing comma here
- it's not a one-tuple
Additionally, if ensure_removable:
- it's not from square bracket indexing
"""
if not (
closing.type in CLOSING_BRACKETS
Expand All @@ -1681,9 +1697,15 @@ def maybe_should_explode(self, closing: Leaf) -> bool:
):
return False

if closing.type in {token.RBRACE, token.RSQB}:
if closing.type == token.RBRACE:
return True

if closing.type == token.RSQB:
if not ensure_removable:
return True
comma = self.leaves[-1]
return bool(comma.parent and comma.parent.type == syms.listmaker)

if self.is_import:
return True

Expand Down Expand Up @@ -1761,6 +1783,7 @@ def is_complex_subscript(self, leaf: Leaf) -> bool:

def clone(self) -> "Line":
return Line(
mode=self.mode,
depth=self.depth,
inside_brackets=self.inside_brackets,
should_explode=self.should_explode,
Expand Down Expand Up @@ -1919,10 +1942,9 @@ class LineGenerator(Visitor[Line]):
in ways that will no longer stringify to valid Python code on the tree.
"""

is_pyi: bool = False
normalize_strings: bool = True
current_line: Line = field(default_factory=Line)
mode: Mode
remove_u_prefix: bool = False
current_line: Line = field(init=False)

def line(self, indent: int = 0) -> Iterator[Line]:
"""Generate a line.
Expand All @@ -1937,7 +1959,7 @@ def line(self, indent: int = 0) -> Iterator[Line]:
return # Line is empty, don't emit. Creating a new one unnecessary.

complete_line = self.current_line
self.current_line = Line(depth=complete_line.depth + indent)
self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent)
yield complete_line

def visit_default(self, node: LN) -> Iterator[Line]:
Expand All @@ -1961,7 +1983,7 @@ def visit_default(self, node: LN) -> Iterator[Line]:
yield from self.line()

normalize_prefix(node, inside_brackets=any_open_brackets)
if self.normalize_strings and node.type == token.STRING:
if self.mode.string_normalization and node.type == token.STRING:
normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix)
normalize_string_quotes(node)
if node.type == token.NUMBER:
Expand Down Expand Up @@ -2013,7 +2035,7 @@ def visit_stmt(

def visit_suite(self, node: Node) -> Iterator[Line]:
"""Visit a suite."""
if self.is_pyi and is_stub_suite(node):
if self.mode.is_pyi and is_stub_suite(node):
yield from self.visit(node.children[2])
else:
yield from self.visit_default(node)
Expand All @@ -2022,15 +2044,19 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]:
"""Visit a statement without nested statements."""
is_suite_like = node.parent and node.parent.type in STATEMENT
if is_suite_like:
if self.is_pyi and is_stub_body(node):
if self.mode.is_pyi and is_stub_body(node):
yield from self.visit_default(node)
else:
yield from self.line(+1)
yield from self.visit_default(node)
yield from self.line(-1)

else:
if not self.is_pyi or not node.parent or not is_stub_suite(node.parent):
if (
not self.mode.is_pyi
or not node.parent
or not is_stub_suite(node.parent)
):
yield from self.line()
yield from self.visit_default(node)

Expand Down Expand Up @@ -2106,6 +2132,8 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:

def __post_init__(self) -> None:
"""You are in a twisty little maze of passages."""
self.current_line = Line(mode=self.mode)

v = self.visit_stmt
Ø: Set[str] = set()
self.visit_assert_stmt = partial(v, keywords={"assert"}, parens={"assert", ","})
Expand Down Expand Up @@ -4337,6 +4365,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
# `StringSplitter` will break it down further if necessary.
string_value = LL[string_idx].value
string_line = Line(
mode=line.mode,
depth=line.depth + 1,
inside_brackets=True,
should_explode=line.should_explode,
Expand Down Expand Up @@ -4930,7 +4959,7 @@ def bracket_split_build_line(
If `is_body` is True, the result line is one-indented inside brackets and as such
has its first leaf's prefix normalized and a trailing comma added when expected.
"""
result = Line(depth=original.depth)
result = Line(mode=original.mode, depth=original.depth)
if is_body:
result.inside_brackets = True
result.depth += 1
Expand Down Expand Up @@ -5002,7 +5031,9 @@ def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[
if bt.delimiter_count_with_priority(delimiter_priority) == 1:
raise CannotSplit("Splitting a single attribute from its owner looks wrong")

current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
current_line = Line(
mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
)
lowest_depth = sys.maxsize
trailing_comma_safe = True

Expand All @@ -5014,7 +5045,9 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]:
except ValueError:
yield current_line

current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
current_line = Line(
mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
)
current_line.append(leaf)

for leaf in line.leaves:
Expand All @@ -5038,7 +5071,9 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]:
if leaf_priority == delimiter_priority:
yield current_line

current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
current_line = Line(
mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
)
if current_line:
if (
trailing_comma_safe
Expand All @@ -5059,7 +5094,9 @@ def standalone_comment_split(
if not line.contains_standalone_comments(0):
raise CannotSplit("Line does not have any standalone comments")

current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
current_line = Line(
mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
)

def append_to_line(leaf: Leaf) -> Iterator[Line]:
"""Append `leaf` to current line or to new line if appending impossible."""
Expand All @@ -5069,7 +5106,9 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]:
except ValueError:
yield current_line

current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
current_line = Line(
line.mode, depth=line.depth, inside_brackets=line.inside_brackets
)
current_line.append(leaf)

for leaf in line.leaves:
Expand Down Expand Up @@ -5754,7 +5793,7 @@ def should_split_body_explode(line: Line, opening_bracket: Leaf) -> bool:
return False

return max_priority == COMMA_PRIORITY and (
trailing_comma
(line.mode.magic_trailing_comma and trailing_comma)
# always explode imports
or opening_bracket.parent.type in {syms.atom, syms.import_from}
)
Expand Down
6 changes: 6 additions & 0 deletions src/blackd/__init__.py
Expand Up @@ -32,6 +32,7 @@
LINE_LENGTH_HEADER = "X-Line-Length"
PYTHON_VARIANT_HEADER = "X-Python-Variant"
SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization"
SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma"
FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe"
DIFF_HEADER = "X-Diff"

Expand All @@ -40,6 +41,7 @@
LINE_LENGTH_HEADER,
PYTHON_VARIANT_HEADER,
SKIP_STRING_NORMALIZATION_HEADER,
SKIP_MAGIC_TRAILING_COMMA,
FAST_OR_SAFE_HEADER,
DIFF_HEADER,
]
Expand Down Expand Up @@ -114,6 +116,9 @@ async def handle(request: web.Request, executor: Executor) -> web.Response:
skip_string_normalization = bool(
request.headers.get(SKIP_STRING_NORMALIZATION_HEADER, False)
)
skip_magic_trailing_comma = bool(
request.headers.get(SKIP_MAGIC_TRAILING_COMMA, False)
)
fast = False
if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast":
fast = True
Expand All @@ -122,6 +127,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response:
is_pyi=pyi,
line_length=line_length,
string_normalization=not skip_string_normalization,
magic_trailing_comma=not skip_magic_trailing_comma,
)
req_bytes = await request.content.read()
charset = request.charset if request.charset is not None else "utf8"
Expand Down