Skip to content

Commit

Permalink
Work around stability errors due to optional trailing commas
Browse files Browse the repository at this point in the history
Optional trailing commas put by Black become magic trailing commas on another
pass of the tool.  Since they are influencing formatting around optional
parentheses, on rare occasions the tool changes its mind in terms of putting
parentheses or not.

Ideally this would never be the case but sadly the decision to put optional
parentheses or not (which looks at pre-existing "magic" trailing commas) is
happening around the same time as the decision to put an optional trailing
comma.  Untangling the two proved to be impractically difficult.

This shameful workaround uses the fact that the formatting instability
introduced by magic trailing commas is deterministic: if the optional trailing
comma becoming a pre-existing "magic" trailing comma changes formatting, the
second pass becomes stable since there is no variable factor anymore on pass 3,
4, and so on.

For most files, this will introduce no performance penalty since `--safe` is
already re-formatting everything twice to ensure formatting stability.  We're
using this result and if all's good, the behavior is equivalent.  If there is
a difference, we treat the second result as the binding one, and check its
sanity again.
  • Loading branch information
ambv committed Apr 25, 2021
1 parent 368f043 commit 4d91cfc
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 7 deletions.
24 changes: 17 additions & 7 deletions src/black/__init__.py
Expand Up @@ -1016,7 +1016,17 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo

if not fast:
assert_equivalent(src_contents, dst_contents)
assert_stable(src_contents, dst_contents, mode=mode)

# Forced second pass to work around optional trailing commas (becoming
# forced trailing commas on pass 2) interacting differently with optional
# parentheses. Admittedly ugly.
dst_contents_pass2 = format_str(dst_contents, mode=mode)
if dst_contents != dst_contents_pass2:
dst_contents = dst_contents_pass2
assert_equivalent(src_contents, dst_contents, pass_num=2)
assert_stable(src_contents, dst_contents, mode=mode)
# Note: no need to explicitly call `assert_stable` if `dst_contents` was
# the same as `dst_contents_pass2`.
return dst_contents


Expand Down Expand Up @@ -6489,7 +6499,7 @@ def _stringify_ast(
yield f"{' ' * depth}) # /{node.__class__.__name__}"


def assert_equivalent(src: str, dst: str) -> None:
def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None:
"""Raise AssertionError if `src` and `dst` aren't equivalent."""
try:
src_ast = parse_ast(src)
Expand All @@ -6504,9 +6514,9 @@ def assert_equivalent(src: str, dst: str) -> None:
except Exception as exc:
log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
raise AssertionError(
f"INTERNAL ERROR: Black produced invalid code: {exc}. Please report a bug"
" on https://github.com/psf/black/issues. This invalid output might be"
f" helpful: {log}"
f"INTERNAL ERROR: Black produced invalid code on pass {pass_num}: {exc}. "
"Please report a bug on https://github.com/psf/black/issues. "
f"This invalid output might be helpful: {log}"
) from None

src_ast_str = "\n".join(_stringify_ast(src_ast))
Expand All @@ -6515,8 +6525,8 @@ def assert_equivalent(src: str, dst: str) -> None:
log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
raise AssertionError(
"INTERNAL ERROR: Black produced code that is not equivalent to the"
" source. Please report a bug on https://github.com/psf/black/issues. "
f" This diff might be helpful: {log}"
f" source on pass {pass_num}. Please report a bug on "
f"https://github.com/psf/black/issues. This diff might be helpful: {log}"
) from None


Expand Down
18 changes: 18 additions & 0 deletions tests/test_black.py
Expand Up @@ -254,6 +254,24 @@ def test_trailing_comma_optional_parens_stability3(self) -> None:
actual = fs(source)
black.assert_stable(source, actual, DEFAULT_MODE)

@patch("black.dump_to_file", dump_to_stderr)
def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
source, _expected = read_data("trailing_comma_optional_parens1")
actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
black.assert_stable(source, actual, DEFAULT_MODE)

@patch("black.dump_to_file", dump_to_stderr)
def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
source, _expected = read_data("trailing_comma_optional_parens2")
actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
black.assert_stable(source, actual, DEFAULT_MODE)

@patch("black.dump_to_file", dump_to_stderr)
def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
source, _expected = read_data("trailing_comma_optional_parens3")
actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
black.assert_stable(source, actual, DEFAULT_MODE)

@patch("black.dump_to_file", dump_to_stderr)
def test_pep_572(self) -> None:
source, expected = read_data("pep_572")
Expand Down

0 comments on commit 4d91cfc

Please sign in to comment.