Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
- Wrap the `in` clause of comprehensions across lines if necessary (#4699)
- Remove parentheses around multiple exception types in `except` and `except*` without
`as`. (#4720)
- Add `\r` style newlines to the potential newlines to normalize file newlines both from
and to (#4710)

### Configuration

Expand Down
2 changes: 2 additions & 0 deletions docs/the_black_code_style/future_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Currently, the following features are included in the preview style:
across lines if it would otherwise exceed the maximum line length.
- `remove_parens_around_except_types`: Remove parentheses around multiple exception
types in `except` and `except*` without `as`. See PEP 758 for details.
- `normalize_cr_newlines`: Add `\r` style newlines to the potential newlines to
normalize file newlines both from and to.

(labels/unstable-features)=

Expand Down
69 changes: 56 additions & 13 deletions src/black/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -946,7 +946,7 @@ def format_file_in_place(
with open(src, "rb") as buf:
if mode.skip_source_first_line:
header = buf.readline()
src_contents, encoding, newline = decode_bytes(buf.read())
src_contents, encoding, newline = decode_bytes(buf.read(), mode)
try:
dst_contents = format_file_contents(
src_contents, fast=fast, mode=mode, lines=lines
Expand Down Expand Up @@ -1008,7 +1008,9 @@ def format_stdin_to_stdout(
then = datetime.now(timezone.utc)

if content is None:
src, encoding, newline = decode_bytes(sys.stdin.buffer.read())
src, encoding, newline = decode_bytes(sys.stdin.buffer.read(), mode)
elif Preview.normalize_cr_newlines in mode:
src, encoding, newline = content, "utf-8", "\n"
else:
src, encoding, newline = content, "utf-8", ""

Expand All @@ -1026,8 +1028,12 @@ def format_stdin_to_stdout(
)
if write_back == WriteBack.YES:
# Make sure there's a newline after the content
if dst and dst[-1] != "\n":
dst += "\n"
if Preview.normalize_cr_newlines in mode:
if dst and dst[-1] != "\n" and dst[-1] != "\r":
dst += newline
else:
if dst and dst[-1] != "\n":
dst += "\n"
f.write(dst)
elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
now = datetime.now(timezone.utc)
Expand Down Expand Up @@ -1217,7 +1223,17 @@ def f(
def _format_str_once(
src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = ()
) -> str:
src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
if Preview.normalize_cr_newlines in mode:
normalized_contents, _, newline_type = decode_bytes(
src_contents.encode("utf-8"), mode
)

src_node = lib2to3_parse(
normalized_contents.lstrip(), target_versions=mode.target_versions
)
else:
src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)

dst_blocks: list[LinesBlock] = []
if mode.target_versions:
versions = mode.target_versions
Expand Down Expand Up @@ -1262,16 +1278,25 @@ def _format_str_once(
for block in dst_blocks:
dst_contents.extend(block.all_lines())
if not dst_contents:
# Use decode_bytes to retrieve the correct source newline (CRLF or LF),
# and check if normalized_content has more than one line
normalized_content, _, newline = decode_bytes(src_contents.encode("utf-8"))
if "\n" in normalized_content:
return newline
if Preview.normalize_cr_newlines in mode:
if "\n" in normalized_contents:
return newline_type
else:
# Use decode_bytes to retrieve the correct source newline (CRLF or LF),
# and check if normalized_content has more than one line
normalized_content, _, newline = decode_bytes(
src_contents.encode("utf-8"), mode
)
if "\n" in normalized_content:
return newline
return ""
return "".join(dst_contents)
if Preview.normalize_cr_newlines in mode:
return "".join(dst_contents).replace("\n", newline_type)
else:
return "".join(dst_contents)


def decode_bytes(src: bytes) -> tuple[FileContent, Encoding, NewLine]:
def decode_bytes(src: bytes, mode: Mode) -> tuple[FileContent, Encoding, NewLine]:
"""Return a tuple of (decoded_contents, encoding, newline).

`newline` is either CRLF or LF but `decoded_contents` is decoded with
Expand All @@ -1282,7 +1307,25 @@ def decode_bytes(src: bytes) -> tuple[FileContent, Encoding, NewLine]:
if not lines:
return "", encoding, "\n"

newline = "\r\n" if lines[0][-2:] == b"\r\n" else "\n"
if Preview.normalize_cr_newlines in mode:
if lines[0][-2:] == b"\r\n":
if b"\r" in lines[0][:-2]:
newline = "\r"
else:
newline = "\r\n"
elif lines[0][-1:] == b"\n":
if b"\r" in lines[0][:-1]:
newline = "\r"
else:
newline = "\n"
else:
if b"\r" in lines[0]:
newline = "\r"
else:
newline = "\n"
else:
newline = "\r\n" if lines[0][-2:] == b"\r\n" else "\n"

srcbuf.seek(0)
with io.TextIOWrapper(srcbuf, encoding) as tiow:
return tiow.read(), encoding, newline
Expand Down
1 change: 1 addition & 0 deletions src/black/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ class Preview(Enum):
# Remove parentheses around multiple exception types in except and
# except* without as. See PEP 758 for details.
remove_parens_around_except_types = auto()
normalize_cr_newlines = auto()


UNSTABLE_FEATURES: set[Preview] = {
Expand Down
3 changes: 2 additions & 1 deletion src/black/resources/black.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@
"always_one_newline_after_import",
"fix_fmt_skip_in_one_liners",
"wrap_comprehension_in",
"remove_parens_around_except_types"
"remove_parens_around_except_types",
"normalize_cr_newlines"
]
},
"description": "Enable specific features included in the `--unstable` style. Requires `--preview`. No compatibility guarantees are provided on the behavior or existence of any unstable features."
Expand Down
16 changes: 9 additions & 7 deletions src/blackd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import black
from _black_version import version as __version__
from black.concurrency import maybe_install_uvloop
from black.mode import Preview

# This is used internally by tests to shut down the server prematurely
_stop_signal = asyncio.Event()
Expand Down Expand Up @@ -129,13 +130,14 @@ async def handle(request: web.Request, executor: Executor) -> web.Response:
executor, partial(black.format_file_contents, req_str, fast=fast, mode=mode)
)

# Preserve CRLF line endings
nl = req_str.find("\n")
if nl > 0 and req_str[nl - 1] == "\r":
formatted_str = formatted_str.replace("\n", "\r\n")
# If, after swapping line endings, nothing changed, then say so
if formatted_str == req_str:
raise black.NothingChanged
if Preview.normalize_cr_newlines not in mode:
# Preserve CRLF line endings
nl = req_str.find("\n")
if nl > 0 and req_str[nl - 1] == "\r":
formatted_str = formatted_str.replace("\n", "\r\n")
# If, after swapping line endings, nothing changed, then say so
if formatted_str == req_str:
raise black.NothingChanged

# Put the source first line back
req_str = header + req_str
Expand Down
7 changes: 7 additions & 0 deletions tests/test_black.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio
import inspect
import io
import itertools
import logging
import multiprocessing
import os
Expand Down Expand Up @@ -2083,6 +2084,12 @@ def test_carriage_return_edge_cases(self) -> None:
== "class A: ...\n"
)

def test_preview_newline_type_detection(self) -> None:
mode = Mode(enabled_features={Preview.normalize_cr_newlines})
newline_types = ["A\n", "A\r\n", "A\r"]
for test_case in itertools.permutations(newline_types):
assert black.format_str("".join(test_case), mode=mode) == test_case[0] * 3


class TestCaching:
def test_get_cache_dir(
Expand Down
Loading