Skip to content

Commit 626b32f

Browse files
MeGaGiGaGonJelleZijlstrapre-commit-ci[bot]
authored
Add normalizing for \r style newlines (#4710)
* Normalize newlines * Fix tired mistakes * Add changelog entry * Update documentation * Move changes to preview * Fix test formatting * Update schema * Fix typo in CHANGES.md Co-authored-by: Jelle Zijlstra <[email protected]> * Fix typo in docs/the_black_code_style/future_style.md Co-authored-by: Jelle Zijlstra <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Jelle Zijlstra <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 57a4612 commit 626b32f

7 files changed

Lines changed: 79 additions & 21 deletions

File tree

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
- Wrap the `in` clause of comprehensions across lines if necessary (#4699)
3838
- Remove parentheses around multiple exception types in `except` and `except*` without
3939
`as`. (#4720)
40+
- Add `\r` style newlines to the potential newlines to normalize file newlines both from
41+
and to (#4710)
4042

4143
### Configuration
4244

docs/the_black_code_style/future_style.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Currently, the following features are included in the preview style:
3333
across lines if it would otherwise exceed the maximum line length.
3434
- `remove_parens_around_except_types`: Remove parentheses around multiple exception
3535
types in `except` and `except*` without `as`. See PEP 758 for details.
36+
- `normalize_cr_newlines`: Add `\r` style newlines to the potential newlines to
37+
normalize file newlines both from and to.
3638

3739
(labels/unstable-features)=
3840

src/black/__init__.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ def format_file_in_place(
946946
with open(src, "rb") as buf:
947947
if mode.skip_source_first_line:
948948
header = buf.readline()
949-
src_contents, encoding, newline = decode_bytes(buf.read())
949+
src_contents, encoding, newline = decode_bytes(buf.read(), mode)
950950
try:
951951
dst_contents = format_file_contents(
952952
src_contents, fast=fast, mode=mode, lines=lines
@@ -1008,7 +1008,9 @@ def format_stdin_to_stdout(
10081008
then = datetime.now(timezone.utc)
10091009

10101010
if content is None:
1011-
src, encoding, newline = decode_bytes(sys.stdin.buffer.read())
1011+
src, encoding, newline = decode_bytes(sys.stdin.buffer.read(), mode)
1012+
elif Preview.normalize_cr_newlines in mode:
1013+
src, encoding, newline = content, "utf-8", "\n"
10121014
else:
10131015
src, encoding, newline = content, "utf-8", ""
10141016

@@ -1026,8 +1028,12 @@ def format_stdin_to_stdout(
10261028
)
10271029
if write_back == WriteBack.YES:
10281030
# Make sure there's a newline after the content
1029-
if dst and dst[-1] != "\n":
1030-
dst += "\n"
1031+
if Preview.normalize_cr_newlines in mode:
1032+
if dst and dst[-1] != "\n" and dst[-1] != "\r":
1033+
dst += newline
1034+
else:
1035+
if dst and dst[-1] != "\n":
1036+
dst += "\n"
10311037
f.write(dst)
10321038
elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
10331039
now = datetime.now(timezone.utc)
@@ -1217,7 +1223,17 @@ def f(
12171223
def _format_str_once(
12181224
src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = ()
12191225
) -> str:
1220-
src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
1226+
if Preview.normalize_cr_newlines in mode:
1227+
normalized_contents, _, newline_type = decode_bytes(
1228+
src_contents.encode("utf-8"), mode
1229+
)
1230+
1231+
src_node = lib2to3_parse(
1232+
normalized_contents.lstrip(), target_versions=mode.target_versions
1233+
)
1234+
else:
1235+
src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
1236+
12211237
dst_blocks: list[LinesBlock] = []
12221238
if mode.target_versions:
12231239
versions = mode.target_versions
@@ -1262,16 +1278,25 @@ def _format_str_once(
12621278
for block in dst_blocks:
12631279
dst_contents.extend(block.all_lines())
12641280
if not dst_contents:
1265-
# Use decode_bytes to retrieve the correct source newline (CRLF or LF),
1266-
# and check if normalized_content has more than one line
1267-
normalized_content, _, newline = decode_bytes(src_contents.encode("utf-8"))
1268-
if "\n" in normalized_content:
1269-
return newline
1281+
if Preview.normalize_cr_newlines in mode:
1282+
if "\n" in normalized_contents:
1283+
return newline_type
1284+
else:
1285+
# Use decode_bytes to retrieve the correct source newline (CRLF or LF),
1286+
# and check if normalized_content has more than one line
1287+
normalized_content, _, newline = decode_bytes(
1288+
src_contents.encode("utf-8"), mode
1289+
)
1290+
if "\n" in normalized_content:
1291+
return newline
12701292
return ""
1271-
return "".join(dst_contents)
1293+
if Preview.normalize_cr_newlines in mode:
1294+
return "".join(dst_contents).replace("\n", newline_type)
1295+
else:
1296+
return "".join(dst_contents)
12721297

12731298

1274-
def decode_bytes(src: bytes) -> tuple[FileContent, Encoding, NewLine]:
1299+
def decode_bytes(src: bytes, mode: Mode) -> tuple[FileContent, Encoding, NewLine]:
12751300
"""Return a tuple of (decoded_contents, encoding, newline).
12761301
12771302
`newline` is either CRLF or LF but `decoded_contents` is decoded with
@@ -1282,7 +1307,25 @@ def decode_bytes(src: bytes) -> tuple[FileContent, Encoding, NewLine]:
12821307
if not lines:
12831308
return "", encoding, "\n"
12841309

1285-
newline = "\r\n" if lines[0][-2:] == b"\r\n" else "\n"
1310+
if Preview.normalize_cr_newlines in mode:
1311+
if lines[0][-2:] == b"\r\n":
1312+
if b"\r" in lines[0][:-2]:
1313+
newline = "\r"
1314+
else:
1315+
newline = "\r\n"
1316+
elif lines[0][-1:] == b"\n":
1317+
if b"\r" in lines[0][:-1]:
1318+
newline = "\r"
1319+
else:
1320+
newline = "\n"
1321+
else:
1322+
if b"\r" in lines[0]:
1323+
newline = "\r"
1324+
else:
1325+
newline = "\n"
1326+
else:
1327+
newline = "\r\n" if lines[0][-2:] == b"\r\n" else "\n"
1328+
12861329
srcbuf.seek(0)
12871330
with io.TextIOWrapper(srcbuf, encoding) as tiow:
12881331
return tiow.read(), encoding, newline

src/black/mode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ class Preview(Enum):
235235
# Remove parentheses around multiple exception types in except and
236236
# except* without as. See PEP 758 for details.
237237
remove_parens_around_except_types = auto()
238+
normalize_cr_newlines = auto()
238239

239240

240241
UNSTABLE_FEATURES: set[Preview] = {

src/black/resources/black.schema.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@
8787
"always_one_newline_after_import",
8888
"fix_fmt_skip_in_one_liners",
8989
"wrap_comprehension_in",
90-
"remove_parens_around_except_types"
90+
"remove_parens_around_except_types",
91+
"normalize_cr_newlines"
9192
]
9293
},
9394
"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."

src/blackd/__init__.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import black
2323
from _black_version import version as __version__
2424
from black.concurrency import maybe_install_uvloop
25+
from black.mode import Preview
2526

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

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

140142
# Put the source first line back
141143
req_str = header + req_str

tests/test_black.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import inspect
55
import io
6+
import itertools
67
import logging
78
import multiprocessing
89
import os
@@ -2083,6 +2084,12 @@ def test_carriage_return_edge_cases(self) -> None:
20832084
== "class A: ...\n"
20842085
)
20852086

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

20872094
class TestCaching:
20882095
def test_get_cache_dir(

0 commit comments

Comments
 (0)