From ce28be2705ab29f184ec4a00aa3d23340630796d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 21:14:25 -0800 Subject: [PATCH 01/29] Add dedicated preview feature for East Asian Width (#4097) --- src/black/lines.py | 2 +- src/black/mode.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/black/lines.py b/src/black/lines.py index 4050f819757..2a41db173d4 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -851,7 +851,7 @@ def is_line_short_enough( # noqa: C901 if not line_str: line_str = line_to_string(line) - width = str_width if mode.preview else len + width = str_width if Preview.respect_east_asian_width in mode else len if Preview.multiline_string_handling not in mode: return ( diff --git a/src/black/mode.py b/src/black/mode.py index 9df19618363..38b861e39ca 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -195,6 +195,7 @@ class Preview(Enum): single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() allow_form_feeds = auto() + respect_east_asian_width = auto() class Deprecated(UserWarning): From 67b23d71854c19921cc6092c695d3301ab99229c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 11:32:04 -0800 Subject: [PATCH 02/29] Bump actions/setup-python from 4 to 5 (#4101) --- .github/workflows/diff_shades.yml | 4 ++-- .github/workflows/diff_shades_comment.yml | 2 +- .github/workflows/doc.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/pypi_upload.yml | 2 +- .github/workflows/release_tests.yml | 2 +- .github/workflows/test.yml | 4 ++-- .github/workflows/upload_binary.yml | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 6bfc6ca9ed8..8d8be2550b0 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" @@ -57,7 +57,7 @@ jobs: # The baseline revision could be rather old so a full clone is ideal. fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 49fd376d85e..9b3b4b579da 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index fa3d87c70f5..006991a16d8 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 48c26452c54..42a399fd0aa 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9c7aca8f869..2d016cef7a6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,7 +24,7 @@ jobs: fi - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index bbdcdf17a8f..8e3eb67a10d 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/release_tests.yml b/.github/workflows/release_tests.yml index 74729445052..192ba004f81 100644 --- a/.github/workflows/release_tests.yml +++ b/.github/workflows/release_tests.yml @@ -34,7 +34,7 @@ jobs: # Give us all history, branches and tags fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f8928cc42a..55359a23303 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -96,7 +96,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/upload_binary.yml index bb19d48158c..06e55cfe93a 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/upload_binary.yml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up latest Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "*" From 9aea9768cb60d23f2f4d331e94c4ee07ef1683a5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 13:19:02 -0800 Subject: [PATCH 03/29] Only use dummy implementation logic for functions and classes (#4066) Fixes #4063 --- CHANGES.md | 2 ++ src/black/linegen.py | 4 ++-- src/black/nodes.py | 9 +++++++- .../cases/preview_dummy_implementations.py | 22 +++++++++++++++++-- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fa0d2494f67..62caea41c31 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,8 @@ - Allow empty lines at the beginning of all blocks, except immediately before a docstring (#4060) - Fix crash in preview mode when using a short `--line-length` (#4086) +- Keep suites consisting of only an ellipsis on their own lines if they are not + functions or class definitions (#4066) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 073672a5ae7..6934823d340 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -286,7 +286,7 @@ def visit_suite(self, node: Node) -> Iterator[Line]: """Visit a suite.""" if ( self.mode.is_pyi or Preview.dummy_implementations in self.mode - ) and is_stub_suite(node): + ) and is_stub_suite(node, self.mode): yield from self.visit(node.children[2]) else: yield from self.visit_default(node) @@ -314,7 +314,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: if ( not (self.mode.is_pyi or Preview.dummy_implementations in self.mode) or not node.parent - or not is_stub_suite(node.parent) + or not is_stub_suite(node.parent, self.mode) ): yield from self.line() yield from self.visit_default(node) diff --git a/src/black/nodes.py b/src/black/nodes.py index de53f8e36a3..9b8d9a97835 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -736,8 +736,15 @@ def is_funcdef(node: Node) -> bool: return node.type == syms.funcdef -def is_stub_suite(node: Node) -> bool: +def is_stub_suite(node: Node, mode: Mode) -> bool: """Return True if `node` is a suite with a stub body.""" + if node.parent is not None: + if Preview.dummy_implementations in mode and node.parent.type not in ( + syms.funcdef, + syms.async_funcdef, + syms.classdef, + ): + return False # If there is a comment, we want to keep it. if node.prefix.strip(): diff --git a/tests/data/cases/preview_dummy_implementations.py b/tests/data/cases/preview_dummy_implementations.py index 98b69bf87b2..113ac36cdc5 100644 --- a/tests/data/cases/preview_dummy_implementations.py +++ b/tests/data/cases/preview_dummy_implementations.py @@ -1,9 +1,11 @@ # flags: --preview from typing import NoReturn, Protocol, Union, overload +class Empty: + ... def dummy(a): ... -def other(b): ... +async def other(b): ... @overload @@ -48,13 +50,22 @@ def b(arg: Union[int, str, object]) -> Union[int, str]: raise TypeError return arg +def has_comment(): + ... # still a dummy + +if some_condition: + ... + # output from typing import NoReturn, Protocol, Union, overload +class Empty: ... + + def dummy(a): ... -def other(b): ... +async def other(b): ... @overload @@ -98,3 +109,10 @@ def b(arg: Union[int, str, object]) -> Union[int, str]: if not isinstance(arg, (int, str)): raise TypeError return arg + + +def has_comment(): ... # still a dummy + + +if some_condition: + ... From 0c9899956d890a9dc9c3adbc80b478a47846ced9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 14:29:33 -0800 Subject: [PATCH 04/29] Fix path in test message (#4102) --- tests/test_black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_black.py b/tests/test_black.py index 899cbeb111d..23815da9042 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -317,7 +317,7 @@ def test_expression_diff(self) -> None: msg = ( "Expected diff isn't equal to the actual. If you made changes to" " expression.py and this is an anticipated difference, overwrite" - f" tests/data/expression.diff with {dump}" + f" tests/data/cases/expression.diff with {dump}" ) self.assertEqual(expected, actual, msg) From eb7661f8ab9bff344835693c7c08789bb195137e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 14:41:41 -0800 Subject: [PATCH 05/29] Fix another case where we format dummy implementation for non-functions/classes (#4103) --- CHANGES.md | 2 +- src/black/linegen.py | 12 +++++++----- src/black/nodes.py | 17 ++++++++++------- .../data/cases/preview_dummy_implementations.py | 5 +++++ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 62caea41c31..dcf6613b70c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,7 +21,7 @@ docstring (#4060) - Fix crash in preview mode when using a short `--line-length` (#4086) - Keep suites consisting of only an ellipsis on their own lines if they are not - functions or class definitions (#4066) + functions or class definitions (#4066) (#4103) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 6934823d340..245be235231 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -42,6 +42,7 @@ is_atom_with_invisible_parens, is_docstring, is_empty_tuple, + is_function_or_class, is_lpar_token, is_multiline_string, is_name_token, @@ -299,11 +300,12 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]: wrap_in_parentheses(node, child, visible=False) prev_type = child.type - is_suite_like = node.parent and node.parent.type in STATEMENT - if is_suite_like: - if ( - self.mode.is_pyi or Preview.dummy_implementations in self.mode - ) and is_stub_body(node): + if node.parent and node.parent.type in STATEMENT: + if Preview.dummy_implementations in self.mode: + condition = is_function_or_class(node.parent) + else: + condition = self.mode.is_pyi + if condition and is_stub_body(node): yield from self.visit_default(node) else: yield from self.line(+1) diff --git a/src/black/nodes.py b/src/black/nodes.py index 9b8d9a97835..a4f555b4032 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -736,15 +736,18 @@ def is_funcdef(node: Node) -> bool: return node.type == syms.funcdef +def is_function_or_class(node: Node) -> bool: + return node.type in {syms.funcdef, syms.classdef, syms.async_funcdef} + + def is_stub_suite(node: Node, mode: Mode) -> bool: """Return True if `node` is a suite with a stub body.""" - if node.parent is not None: - if Preview.dummy_implementations in mode and node.parent.type not in ( - syms.funcdef, - syms.async_funcdef, - syms.classdef, - ): - return False + if ( + node.parent is not None + and Preview.dummy_implementations in mode + and not is_function_or_class(node.parent) + ): + return False # If there is a comment, we want to keep it. if node.prefix.strip(): diff --git a/tests/data/cases/preview_dummy_implementations.py b/tests/data/cases/preview_dummy_implementations.py index 113ac36cdc5..28b23bb8609 100644 --- a/tests/data/cases/preview_dummy_implementations.py +++ b/tests/data/cases/preview_dummy_implementations.py @@ -56,6 +56,8 @@ def has_comment(): if some_condition: ... +if already_dummy: ... + # output from typing import NoReturn, Protocol, Union, overload @@ -116,3 +118,6 @@ def has_comment(): ... # still a dummy if some_condition: ... + +if already_dummy: + ... From ebd543c0ac9b8a5f17636d0a42c425e5f693860e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 21:37:15 -0800 Subject: [PATCH 06/29] Fix feature detection for parenthesized context managers (#4104) --- CHANGES.md | 1 + src/black/__init__.py | 18 ++- tests/data/cases/pep_572_remove_parens.py | 2 +- tests/test_black.py | 130 ++++++++++++---------- 4 files changed, 93 insertions(+), 58 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index dcf6613b70c..e3b5b7392b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ - Fix bug where `# fmt: off` automatically dedents when used with the `--line-ranges` option, even when it is not within the specified line range. (#4084) +- Fix feature detection for parenthesized context managers (#4104) ### Preview style diff --git a/src/black/__init__.py b/src/black/__init__.py index 5073fa748d5..735ba713b8f 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1351,7 +1351,7 @@ def get_features_used( # noqa: C901 if ( len(atom_children) == 3 and atom_children[0].type == token.LPAR - and atom_children[1].type == syms.testlist_gexp + and _contains_asexpr(atom_children[1]) and atom_children[2].type == token.RPAR ): features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) @@ -1384,6 +1384,22 @@ def get_features_used( # noqa: C901 return features +def _contains_asexpr(node: Union[Node, Leaf]) -> bool: + """Return True if `node` contains an as-pattern.""" + if node.type == syms.asexpr_test: + return True + elif node.type == syms.atom: + if ( + len(node.children) == 3 + and node.children[0].type == token.LPAR + and node.children[2].type == token.RPAR + ): + return _contains_asexpr(node.children[1]) + elif node.type == syms.testlist_gexp: + return any(_contains_asexpr(child) for child in node.children) + return False + + def detect_target_versions( node: Node, *, future_imports: Optional[Set[str]] = None ) -> Set[TargetVersion]: diff --git a/tests/data/cases/pep_572_remove_parens.py b/tests/data/cases/pep_572_remove_parens.py index 88774d81649..24f1ac29168 100644 --- a/tests/data/cases/pep_572_remove_parens.py +++ b/tests/data/cases/pep_572_remove_parens.py @@ -1,4 +1,4 @@ -# flags: --minimum-version=3.8 --no-preview-line-length-1 +# flags: --minimum-version=3.8 if (foo := 0): pass diff --git a/tests/test_black.py b/tests/test_black.py index 23815da9042..0af5fd2a1f4 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -25,6 +25,7 @@ List, Optional, Sequence, + Set, Type, TypeVar, Union, @@ -874,71 +875,88 @@ def test_get_features_used_decorator(self) -> None: ) def test_get_features_used(self) -> None: - node = black.lib2to3_parse("def f(*, arg): ...\n") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("def f(*, arg,): ...\n") - self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF}) - node = black.lib2to3_parse("f(*arg,)\n") - self.assertEqual( - black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL} + self.check_features_used("def f(*, arg): ...\n", set()) + self.check_features_used( + "def f(*, arg,): ...\n", {Feature.TRAILING_COMMA_IN_DEF} ) - node = black.lib2to3_parse("def f(*, arg): f'string'\n") - self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS}) - node = black.lib2to3_parse("123_456\n") - self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES}) - node = black.lib2to3_parse("123456\n") - self.assertEqual(black.get_features_used(node), set()) + self.check_features_used("f(*arg,)\n", {Feature.TRAILING_COMMA_IN_CALL}) + self.check_features_used("def f(*, arg): f'string'\n", {Feature.F_STRINGS}) + self.check_features_used("123_456\n", {Feature.NUMERIC_UNDERSCORES}) + self.check_features_used("123456\n", set()) + source, expected = read_data("cases", "function") - node = black.lib2to3_parse(source) expected_features = { Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF, Feature.F_STRINGS, } - self.assertEqual(black.get_features_used(node), expected_features) - node = black.lib2to3_parse(expected) - self.assertEqual(black.get_features_used(node), expected_features) + self.check_features_used(source, expected_features) + self.check_features_used(expected, expected_features) + source, expected = read_data("cases", "expression") - node = black.lib2to3_parse(source) - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse(expected) - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("lambda a, /, b: ...") - self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) - node = black.lib2to3_parse("def fn(a, /, b): ...") - self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS}) - node = black.lib2to3_parse("def fn(): yield a, b") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("def fn(): return a, b") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("def fn(): yield *b, c") - self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) - node = black.lib2to3_parse("def fn(): return a, *b, c") - self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW}) - node = black.lib2to3_parse("x = a, *b, c") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Any = regular") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Any = (regular, regular)") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c") - self.assertEqual( - black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS} + self.check_features_used(source, set()) + self.check_features_used(expected, set()) + + self.check_features_used("lambda a, /, b: ...\n", {Feature.POS_ONLY_ARGUMENTS}) + self.check_features_used("def fn(a, /, b): ...", {Feature.POS_ONLY_ARGUMENTS}) + + self.check_features_used("def fn(): yield a, b", set()) + self.check_features_used("def fn(): return a, b", set()) + self.check_features_used("def fn(): yield *b, c", {Feature.UNPACKING_ON_FLOW}) + self.check_features_used( + "def fn(): return a, *b, c", {Feature.UNPACKING_ON_FLOW} ) - node = black.lib2to3_parse("try: pass\nexcept Something: pass") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass") - self.assertEqual(black.get_features_used(node), set()) - node = black.lib2to3_parse("try: pass\nexcept *Group: pass") - self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR}) - node = black.lib2to3_parse("a[*b]") - self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) - node = black.lib2to3_parse("a[x, *y(), z] = t") - self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) - node = black.lib2to3_parse("def fn(*args: *T): pass") - self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS}) + self.check_features_used("x = a, *b, c", set()) + + self.check_features_used("x: Any = regular", set()) + self.check_features_used("x: Any = (regular, regular)", set()) + self.check_features_used("x: Any = Complex(Type(1))[something]", set()) + self.check_features_used( + "x: Tuple[int, ...] = a, b, c", {Feature.ANN_ASSIGN_EXTENDED_RHS} + ) + + self.check_features_used("try: pass\nexcept Something: pass", set()) + self.check_features_used("try: pass\nexcept (*Something,): pass", set()) + self.check_features_used( + "try: pass\nexcept *Group: pass", {Feature.EXCEPT_STAR} + ) + + self.check_features_used("a[*b]", {Feature.VARIADIC_GENERICS}) + self.check_features_used("a[x, *y(), z] = t", {Feature.VARIADIC_GENERICS}) + self.check_features_used("def fn(*args: *T): pass", {Feature.VARIADIC_GENERICS}) + + self.check_features_used("with a: pass", set()) + self.check_features_used("with a, b: pass", set()) + self.check_features_used("with a as b: pass", set()) + self.check_features_used("with a as b, c as d: pass", set()) + self.check_features_used("with (a): pass", set()) + self.check_features_used("with (a, b): pass", set()) + self.check_features_used("with (a, b) as (c, d): pass", set()) + self.check_features_used( + "with (a as b): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with ((a as b)): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with (a, b as c): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with (a, (b as c)): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + self.check_features_used( + "with ((a, ((b as c)))): pass", {Feature.PARENTHESIZED_CONTEXT_MANAGERS} + ) + + def check_features_used(self, source: str, expected: Set[Feature]) -> None: + node = black.lib2to3_parse(source) + actual = black.get_features_used(node) + msg = f"Expected {expected} but got {actual} for {source!r}" + try: + self.assertEqual(actual, expected, msg=msg) + except AssertionError: + DebugVisitor.show(node) + raise def test_get_features_used_for_future_flags(self) -> None: for src, features in [ From d9ad09a32b0e0481bb4fef548d35b7a49cc03c5d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 21:55:28 -0800 Subject: [PATCH 07/29] Prepare release 23.12.0 (#4105) --- CHANGES.md | 33 +++++---------------- docs/integrations/source_version_control.md | 4 +-- docs/usage_and_configuration/the_basics.md | 6 ++-- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e3b5b7392b9..223d7d2c819 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,16 @@ # Change Log -## Unreleased +## 23.12.0 ### Highlights - +It's almost 2024, which means it's time for a new edition of _Black_'s stable style! +Together with this release, we'll put out an alpha release 24.1a1 showcasing the draft +2024 stable style, which we'll finalize in the January release. Please try it out and +[share your feedback](https://github.com/psf/black/issues/4042). + +This release (23.12.0) will still produce the 2023 style. Most but not all of the +changes in `--preview` mode will be in the 2024 stable style. ### Stable style @@ -26,8 +32,6 @@ ### Configuration - - - `--line-ranges` now skips _Black_'s internal stability check in `--safe` mode. This avoids a crash on rare inputs that have many unformatted same-content lines. (#4034) @@ -36,33 +40,12 @@ - Upgrade to mypy 1.7.1 (#4049) (#4069) - Faster compiled wheels are now available for CPython 3.12 (#4070) -### Parser - - - -### Performance - - - -### Output - - - -### _Blackd_ - - - ### Integrations - Enable 3.12 CI (#4035) - Build docker images in parallel (#4054) - Build docker images with 3.12 (#4055) -### Documentation - - - ## 23.11.0 ### Highlights diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index 3c7ef89918f..ca810f1d8f6 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black-jupyter # It is recommended to specify the latest version of Python diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index eb92887f64f..2dbb573803c 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -241,8 +241,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.11.0 (compiled: yes) -$ black --required-version 23.11.0 -c "format = 'this'" +black, 23.12.0 (compiled: yes) +$ black --required-version 23.12.0 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -333,7 +333,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.11.0 +black, 23.12.0 ``` #### `--config` From 35ce37ded7bd8fdd3950af19e7c11f311ee7b8d8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Dec 2023 22:28:46 -0800 Subject: [PATCH 08/29] Add new changelog template --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 223d7d2c819..9d79b0fb61a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,52 @@ # Change Log +## Unreleased + +### Highlights + + + +### Stable style + + + +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + ## 23.12.0 ### Highlights From 8fec1c30855890cc9cfce5ae6d633a1c1a21d724 Mon Sep 17 00:00:00 2001 From: Bryce Willey Date: Thu, 14 Dec 2023 03:28:28 -0500 Subject: [PATCH 09/29] Adds paren to deps for hidden extra constraint (#4108) Fix #4107 --- CHANGES.md | 2 ++ pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9d79b0fb61a..69fe34a5052 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,8 @@ +- Fixed a bug that included dependencies from the `d` extra by default (#4108) + ### Parser diff --git a/pyproject.toml b/pyproject.toml index 1098412981a..24b9c07674d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ preview = true # NOTE: You don't need this in your own Black configuration. [build-system] -requires = ["hatchling>=1.8.0", "hatch-vcs", "hatch-fancy-pypi-readme"] +requires = ["hatchling>=1.20.0", "hatch-vcs", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [project] @@ -187,7 +187,7 @@ CC = "clang" build-frontend = { name = "build", args = ["--no-isolation"] } # Unfortunately, hatch doesn't respect MACOSX_DEPLOYMENT_TARGET before-build = [ - "python -m pip install 'hatchling==1.18.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.7.1' 'click==8.1.3'", + "python -m pip install 'hatchling==1.20.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.7.1' 'click==8.1.3'", """sed -i '' -e "600,700s/'10_16'/os.environ['MACOSX_DEPLOYMENT_TARGET'].replace('.', '_')/" $(python -c 'import hatchling.builders.wheel as h; print(h.__file__)') """, ] From ec91a2be3c44d88e1a3960a4937ad6ed3b63464e Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Fri, 22 Dec 2023 17:04:32 -0600 Subject: [PATCH 10/29] Prepare release 23.12.1 (#4124) --- CHANGES.md | 45 +-------------------- docs/integrations/source_version_control.md | 4 +- docs/usage_and_configuration/the_basics.md | 6 +-- 3 files changed, 6 insertions(+), 49 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 69fe34a5052..d0c9e567457 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,54 +1,11 @@ # Change Log -## Unreleased - -### Highlights - - - -### Stable style - - - -### Preview style - - - -### Configuration - - +## 23.12.1 ### Packaging - - - Fixed a bug that included dependencies from the `d` extra by default (#4108) -### Parser - - - -### Performance - - - -### Output - - - -### _Blackd_ - - - -### Integrations - - - -### Documentation - - - ## 23.12.0 ### Highlights diff --git a/docs/integrations/source_version_control.md b/docs/integrations/source_version_control.md index ca810f1d8f6..3b895193941 100644 --- a/docs/integrations/source_version_control.md +++ b/docs/integrations/source_version_control.md @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black # It is recommended to specify the latest version of Python @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black-jupyter # It is recommended to specify the latest version of Python diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 2dbb573803c..4f9856c6a47 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -241,8 +241,8 @@ configuration file for consistent results across environments. ```console $ black --version -black, 23.12.0 (compiled: yes) -$ black --required-version 23.12.0 -c "format = 'this'" +black, 23.12.1 (compiled: yes) +$ black --required-version 23.12.1 -c "format = 'this'" format = "this" $ black --required-version 31.5b2 -c "still = 'beta?!'" Oh no! 💥 💔 💥 The required version does not match the running version! @@ -333,7 +333,7 @@ You can check the version of _Black_ you have installed using the `--version` fl ```console $ black --version -black, 23.12.0 +black, 23.12.1 ``` #### `--config` From 1b831f214a111dfb45a571fc40f4404bb6b5b62c Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Fri, 22 Dec 2023 17:46:06 -0600 Subject: [PATCH 11/29] Add new changelog template (#4125) --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d0c9e567457..526cbd12123 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,52 @@ # Change Log +## Unreleased + +### Highlights + + + +### Stable style + + + +### Preview style + + + +### Configuration + + + +### Packaging + + + +### Parser + + + +### Performance + + + +### Output + + + +### _Blackd_ + + + +### Integrations + + + +### Documentation + + + ## 23.12.1 ### Packaging From 51786141cc4eb3c212be76638e66b91648d0e5f8 Mon Sep 17 00:00:00 2001 From: Anupya Pamidimukkala Date: Thu, 28 Dec 2023 01:23:42 -0500 Subject: [PATCH 12/29] Fix nits, chain comparisons, unused params, hyphens (#4114) --- src/black/brackets.py | 4 ++-- src/black/cache.py | 4 ++-- src/black/comments.py | 3 +-- src/black/linegen.py | 2 +- src/black/lines.py | 4 ++-- src/black/ranges.py | 5 +---- src/black/trans.py | 21 ++++++++++----------- 7 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/black/brackets.py b/src/black/brackets.py index 3020cc0d390..37e6b2590eb 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -115,7 +115,7 @@ def mark(self, leaf: Leaf) -> None: if delim and self.previous is not None: self.delimiters[id(self.previous)] = delim else: - delim = is_split_after_delimiter(leaf, self.previous) + delim = is_split_after_delimiter(leaf) if delim: self.delimiters[id(leaf)] = delim if leaf.type in OPENING_BRACKETS: @@ -215,7 +215,7 @@ def get_open_lsqb(self) -> Optional[Leaf]: return self.bracket_match.get((self.depth - 1, token.RSQB)) -def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: +def is_split_after_delimiter(leaf: Leaf) -> Priority: """Return the priority of the `leaf` delimiter, given a line break after it. The delimiter priorities returned here are from those delimiters that would diff --git a/src/black/cache.py b/src/black/cache.py index 6baa096baca..c844c37b6f8 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -58,9 +58,9 @@ class Cache: @classmethod def read(cls, mode: Mode) -> Self: - """Read the cache if it exists and is well formed. + """Read the cache if it exists and is well-formed. - If it is not well formed, the call to write later should + If it is not well-formed, the call to write later should resolve the issue. """ cache_file = get_cache_file(mode) diff --git a/src/black/comments.py b/src/black/comments.py index 25413121199..52bb024a799 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -221,8 +221,7 @@ def convert_one_fmt_off_pair( if comment.value in FMT_OFF: fmt_off_prefix = "" if len(lines) > 0 and not any( - comment_lineno >= line[0] and comment_lineno <= line[1] - for line in lines + line[0] <= comment_lineno <= line[1] for line in lines ): # keeping indentation of comment by preserving original whitespaces. fmt_off_prefix = prefix.split(comment.value)[0] diff --git a/src/black/linegen.py b/src/black/linegen.py index 245be235231..0fd4a8d9c96 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1635,7 +1635,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf opening_bracket: Optional[Leaf] = None closing_bracket: Optional[Leaf] = None inner_brackets: Set[LeafID] = set() - for index, leaf, leaf_length in line.enumerate_with_length(reversed=True): + for index, leaf, leaf_length in line.enumerate_with_length(is_reversed=True): length += leaf_length if length > line_length: break diff --git a/src/black/lines.py b/src/black/lines.py index 2a41db173d4..0cd4189a778 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -451,7 +451,7 @@ def is_complex_subscript(self, leaf: Leaf) -> bool: ) def enumerate_with_length( - self, reversed: bool = False + self, is_reversed: bool = False ) -> Iterator[Tuple[Index, Leaf, int]]: """Return an enumeration of leaves with their length. @@ -459,7 +459,7 @@ def enumerate_with_length( """ op = cast( Callable[[Sequence[Leaf]], Iterator[Tuple[Index, Leaf]]], - enumerate_reversed if reversed else enumerate, + enumerate_reversed if is_reversed else enumerate, ) for index, leaf in op(self.leaves): length = len(leaf.prefix) + len(leaf.value) diff --git a/src/black/ranges.py b/src/black/ranges.py index 59e19242d47..06fa8790554 100644 --- a/src/black/ranges.py +++ b/src/black/ranges.py @@ -487,10 +487,7 @@ def _find_lines_mapping_index( index = start_index while index < len(lines_mappings): mapping = lines_mappings[index] - if ( - mapping.original_start <= original_line - and original_line <= mapping.original_end - ): + if mapping.original_start <= original_line <= mapping.original_end: return index index += 1 return index diff --git a/src/black/trans.py b/src/black/trans.py index ab3197fa6df..7c7335a005b 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1273,7 +1273,7 @@ def iter_fexpr_spans(s: str) -> Iterator[Tuple[int, int]]: i += 1 continue - # if we're in an expression part of the f-string, fast forward through strings + # if we're in an expression part of the f-string, fast-forward through strings # note that backslashes are not legal in the expression portion of f-strings if stack: delim = None @@ -1740,7 +1740,7 @@ def passes_all_checks(i: Index) -> bool: """ Returns: True iff ALL of the conditions listed in the 'Transformations' - section of this classes' docstring would be be met by returning @i. + section of this classes' docstring would be met by returning @i. """ is_space = string[i] == " " is_split_safe = is_valid_index(i - 1) and string[i - 1] in SPLIT_SAFE_CHARS @@ -1932,7 +1932,7 @@ def _return_match(LL: List[Leaf]) -> Optional[int]: OR None, otherwise. """ - # If this line is apart of a return/yield statement and the first leaf + # If this line is a part of a return/yield statement and the first leaf # contains either the "return" or "yield" keywords... if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[ 0 @@ -1957,7 +1957,7 @@ def _else_match(LL: List[Leaf]) -> Optional[int]: OR None, otherwise. """ - # If this line is apart of a ternary expression and the first leaf + # If this line is a part of a ternary expression and the first leaf # contains the "else" keyword... if ( parent_type(LL[0]) == syms.test @@ -1984,7 +1984,7 @@ def _assert_match(LL: List[Leaf]) -> Optional[int]: OR None, otherwise. """ - # If this line is apart of an assert statement and the first leaf + # If this line is a part of an assert statement and the first leaf # contains the "assert" keyword... if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert": is_valid_index = is_valid_index_factory(LL) @@ -2019,7 +2019,7 @@ def _assign_match(LL: List[Leaf]) -> Optional[int]: OR None, otherwise. """ - # If this line is apart of an expression statement or is a function + # If this line is a part of an expression statement or is a function # argument AND the first leaf contains a variable name... if ( parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power] @@ -2040,7 +2040,7 @@ def _assign_match(LL: List[Leaf]) -> Optional[int]: string_parser = StringParser() idx = string_parser.parse(LL, string_idx) - # The next leaf MAY be a comma iff this line is apart + # The next leaf MAY be a comma iff this line is a part # of a function argument... if ( parent_type(LL[0]) == syms.argument @@ -2187,8 +2187,7 @@ def do_transform( if opening_bracket is not None and opening_bracket in left_leaves: index = left_leaves.index(opening_bracket) if ( - index > 0 - and index < len(left_leaves) - 1 + 0 < index < len(left_leaves) - 1 and left_leaves[index - 1].type == token.COLON and left_leaves[index + 1].value == "lambda" ): @@ -2297,7 +2296,7 @@ def parse(self, leaves: List[Leaf], string_idx: int) -> int: * @leaves[@string_idx].type == token.STRING Returns: - The index directly after the last leaf which is apart of the string + The index directly after the last leaf which is a part of the string trailer, if a "trailer" exists. OR @string_idx + 1, if no string "trailer" exists. @@ -2320,7 +2319,7 @@ def _next_state(self, leaf: Leaf) -> bool: MUST be the leaf directly following @leaf. Returns: - True iff @leaf is apart of the string's trailer. + True iff @leaf is a part of the string's trailer. """ # We ignore empty LPAR or RPAR leaves. if is_empty_par(leaf): From c80685f36183f146f831a5737510cf105f947745 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Thu, 28 Dec 2023 00:24:25 -0600 Subject: [PATCH 13/29] Treat walruses like other binary operators in subscripts (#4109) Fixes #4078 --- CHANGES.md | 3 +++ src/black/lines.py | 9 ++++++++- tests/data/cases/preview_pep_572.py | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 526cbd12123..1444463050f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ +- Fix bug where spaces were not added around parenthesized walruses in subscripts, + unlike other binary operators (#4109) + ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 0cd4189a778..d153b8c2e1b 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -446,8 +446,15 @@ def is_complex_subscript(self, leaf: Leaf) -> bool: if subscript_start.type == syms.subscriptlist: subscript_start = child_towards(subscript_start, leaf) + + # When this is moved out of preview, add syms.namedexpr_test directly to + # TEST_DESCENDANTS in nodes.py + if Preview.walrus_subscript in self.mode: + test_decendants = TEST_DESCENDANTS | {syms.namedexpr_test} + else: + test_decendants = TEST_DESCENDANTS return subscript_start is not None and any( - n.type in TEST_DESCENDANTS for n in subscript_start.pre_order() + n.type in test_decendants for n in subscript_start.pre_order() ) def enumerate_with_length( diff --git a/tests/data/cases/preview_pep_572.py b/tests/data/cases/preview_pep_572.py index 8e801ff6cdc..75ad0cc4176 100644 --- a/tests/data/cases/preview_pep_572.py +++ b/tests/data/cases/preview_pep_572.py @@ -3,5 +3,5 @@ x[:(a:=0)] # output -x[(a := 0):] -x[:(a := 0)] +x[(a := 0) :] +x[: (a := 0)] From bf6cabc8049cbdf4d0b8af33134317a0190a614f Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:24:57 -0800 Subject: [PATCH 14/29] Do not round cache mtimes (#4128) Fixes #4116 This logic was introduced in #3821, I believe as a result of copying logic inside mypy that I think isn't relevant to Black --- CHANGES.md | 2 ++ src/black/cache.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1444463050f..2389f6d39fd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,8 @@ +- Fix cache mtime logic that resulted in false positive cache hits (#4128) + ### Packaging diff --git a/src/black/cache.py b/src/black/cache.py index c844c37b6f8..cfdbc21e92a 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -101,7 +101,7 @@ def is_changed(self, source: Path) -> bool: st = res_src.stat() if st.st_size != old.st_size: return True - if int(st.st_mtime) != int(old.st_mtime): + if st.st_mtime != old.st_mtime: new_hash = Cache.hash_digest(res_src) if new_hash != old.hash: return True From db9c592967b976a16eccd500f3e2676cfff7f29d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 27 Dec 2023 22:59:30 -0800 Subject: [PATCH 15/29] Unify docstring detection (#4095) Co-authored-by: hauntsaninja --- CHANGES.md | 1 + src/black/linegen.py | 4 ++-- src/black/lines.py | 15 +++++++++++---- src/black/mode.py | 1 + src/black/nodes.py | 12 +++++++++++- src/black/strings.py | 2 ++ tests/data/cases/module_docstring_2.py | 2 ++ .../preview_no_blank_line_before_docstring.py | 7 +++++++ 8 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2389f6d39fd..a6587cc5ceb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ +- Format module docstrings the same as class and function docstrings (#4095) - Fix bug where spaces were not added around parenthesized walruses in subscripts, unlike other binary operators (#4109) diff --git a/src/black/linegen.py b/src/black/linegen.py index 0fd4a8d9c96..0972cf432e1 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -424,7 +424,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: if Preview.hex_codes_in_unicode_sequences in self.mode: normalize_unicode_escape_sequences(leaf) - if is_docstring(leaf) and not re.search(r"\\\s*\n", leaf.value): + if is_docstring(leaf, self.mode) and not re.search(r"\\\s*\n", leaf.value): # We're ignoring docstrings with backslash newline escapes because changing # indentation of those changes the AST representation of the code. if self.mode.string_normalization: @@ -477,7 +477,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: quote = quote_char * quote_len # It's invalid to put closing single-character quotes on a new line. - if self.mode and quote_len == 3: + if quote_len == 3: # We need to find the length of the last line of the docstring # to find if we can add the closing quotes to the line without # exceeding the maximum line length. diff --git a/src/black/lines.py b/src/black/lines.py index d153b8c2e1b..8d02267a85b 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -196,7 +196,7 @@ def is_class_paren_empty(self) -> bool: ) @property - def is_triple_quoted_string(self) -> bool: + def _is_triple_quoted_string(self) -> bool: """Is the line a triple quoted string?""" if not self or self.leaves[0].type != token.STRING: return False @@ -209,6 +209,13 @@ def is_triple_quoted_string(self) -> bool: return True return False + @property + def is_docstring(self) -> bool: + """Is the line a docstring?""" + if Preview.unify_docstring_detection not in self.mode: + return self._is_triple_quoted_string + return bool(self) and is_docstring(self.leaves[0], self.mode) + @property def is_chained_assignment(self) -> bool: """Is the line a chained assignment""" @@ -583,7 +590,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: and self.previous_block and self.previous_block.previous_block is None and len(self.previous_block.original_line.leaves) == 1 - and self.previous_block.original_line.is_triple_quoted_string + and self.previous_block.original_line.is_docstring and not (current_line.is_class or current_line.is_def) ): before = 1 @@ -690,7 +697,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: if ( self.previous_line and self.previous_line.is_class - and current_line.is_triple_quoted_string + and current_line.is_docstring ): if Preview.no_blank_line_before_class_docstring in current_line.mode: return 0, 1 @@ -701,7 +708,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: is_empty_first_line_ok = ( Preview.allow_empty_first_line_in_block in current_line.mode and ( - not is_docstring(current_line.leaves[0]) + not is_docstring(current_line.leaves[0], current_line.mode) or ( self.previous_line and self.previous_line.leaves[0] diff --git a/src/black/mode.py b/src/black/mode.py index 38b861e39ca..466b78228fc 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -195,6 +195,7 @@ class Preview(Enum): single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() allow_form_feeds = auto() + unify_docstring_detection = auto() respect_east_asian_width = auto() diff --git a/src/black/nodes.py b/src/black/nodes.py index a4f555b4032..8e0f27e3ded 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -531,7 +531,7 @@ def is_arith_like(node: LN) -> bool: } -def is_docstring(leaf: Leaf) -> bool: +def is_docstring(leaf: Leaf, mode: Mode) -> bool: if leaf.type != token.STRING: return False @@ -539,6 +539,16 @@ def is_docstring(leaf: Leaf) -> bool: if set(prefix).intersection("bBfF"): return False + if ( + Preview.unify_docstring_detection in mode + and leaf.parent + and leaf.parent.type == syms.simple_stmt + and not leaf.parent.prev_sibling + and leaf.parent.parent + and leaf.parent.parent.type == syms.file_input + ): + return True + if prev_siblings_are( leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] ): diff --git a/src/black/strings.py b/src/black/strings.py index 0d30f09ed11..0e0f968824b 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -63,6 +63,8 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: ) else: lines.append(line) + if s.endswith("\n"): + lines.append("") return lines diff --git a/tests/data/cases/module_docstring_2.py b/tests/data/cases/module_docstring_2.py index e1f81b4d76b..1cc9aea9aea 100644 --- a/tests/data/cases/module_docstring_2.py +++ b/tests/data/cases/module_docstring_2.py @@ -1,6 +1,7 @@ # flags: --preview """I am a very helpful module docstring. +With trailing spaces: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, @@ -38,6 +39,7 @@ # output """I am a very helpful module docstring. +With trailing spaces: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, diff --git a/tests/data/cases/preview_no_blank_line_before_docstring.py b/tests/data/cases/preview_no_blank_line_before_docstring.py index 303035a7efb..faeaa1e46e4 100644 --- a/tests/data/cases/preview_no_blank_line_before_docstring.py +++ b/tests/data/cases/preview_no_blank_line_before_docstring.py @@ -29,6 +29,9 @@ class MultilineDocstringsAsWell: and on so many lines... """ +class SingleQuotedDocstring: + + "I'm a docstring but I don't even get triple quotes." # output @@ -57,3 +60,7 @@ class MultilineDocstringsAsWell: and on so many lines... """ + + +class SingleQuotedDocstring: + "I'm a docstring but I don't even get triple quotes." From c35924663cff4f696f9bb91ca9c7775487d95ac6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 15:12:18 -0800 Subject: [PATCH 16/29] [pre-commit.ci] pre-commit autoupdate (#4139) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2896489d724..13479565527 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: additional_dependencies: *version_check_dependencies - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort @@ -39,7 +39,7 @@ repos: exclude: ^src/blib2to3/ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy exclude: ^docs/conf.py @@ -58,13 +58,13 @@ repos: - hypothesmith - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v4.0.0-alpha.8 hooks: - id: prettier exclude: \.github/workflows/diff_shades\.yml - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace From fe3376141c333271d3c64d7fa0e433652e2b48ff Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 1 Jan 2024 15:46:09 -0800 Subject: [PATCH 17/29] Allow empty lines at beginnings of more blocks (#4130) Fixes #4043, fixes #619 These include nested functions and methods. I think the nested function case quite clearly improves readability. I think the method case improves consistency, adherence to PEP 8 and resolves a point of contention. --- CHANGES.md | 2 ++ src/black/lines.py | 5 ++++- tests/data/cases/class_blank_parentheses.py | 1 + .../cases/preview_allow_empty_first_line.py | 19 +++++++++++++++++++ tests/data/cases/preview_form_feeds.py | 1 + 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a6587cc5ceb..fca88612afe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ - Format module docstrings the same as class and function docstrings (#4095) - Fix bug where spaces were not added around parenthesized walruses in subscripts, unlike other binary operators (#4109) +- Address a missing case in the change to allow empty lines at the beginning of all + blocks, except immediately before a docstring (#4130) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 8d02267a85b..4d4f47a44e8 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -745,7 +745,10 @@ def _maybe_empty_lines_for_class_or_def( # noqa: C901 if self.previous_line.depth < current_line.depth and ( self.previous_line.is_class or self.previous_line.is_def ): - return 0, 0 + if self.mode.is_pyi or not Preview.allow_empty_first_line_in_block: + return 0, 0 + else: + return 1 if user_had_newline else 0, 0 comment_to_add_newlines: Optional[LinesBlock] = None if ( diff --git a/tests/data/cases/class_blank_parentheses.py b/tests/data/cases/class_blank_parentheses.py index 1a5721a2889..3c460d9bd79 100644 --- a/tests/data/cases/class_blank_parentheses.py +++ b/tests/data/cases/class_blank_parentheses.py @@ -39,6 +39,7 @@ def test_func(self): class ClassWithEmptyFunc(object): + def func_with_blank_parentheses(): return 5 diff --git a/tests/data/cases/preview_allow_empty_first_line.py b/tests/data/cases/preview_allow_empty_first_line.py index 3e14fa15250..daf78344ad7 100644 --- a/tests/data/cases/preview_allow_empty_first_line.py +++ b/tests/data/cases/preview_allow_empty_first_line.py @@ -62,6 +62,15 @@ def method(self): pass + +def top_level( + a: int, + b: str, +) -> Whatever[Generic, Something]: + + def nested(x: int) -> int: + pass + # output def foo(): @@ -123,6 +132,16 @@ def quux(): class Cls: + def method(self): pass + + +def top_level( + a: int, + b: str, +) -> Whatever[Generic, Something]: + + def nested(x: int) -> int: + pass diff --git a/tests/data/cases/preview_form_feeds.py b/tests/data/cases/preview_form_feeds.py index c236f177a95..dc3bd6cfe2e 100644 --- a/tests/data/cases/preview_form_feeds.py +++ b/tests/data/cases/preview_form_feeds.py @@ -203,6 +203,7 @@ def bar(a=1, b: bool = False): class Baz: + def __init__(self): pass From b9ad4da2e81f6ec66d292b85f284889211e052b4 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 1 Jan 2024 16:55:25 -0800 Subject: [PATCH 18/29] Revert "confine pre-commit to stages (#3940)" (#4137) This reverts commit 7686989fc89aad5ea235a34977ebf8c81c26c4eb. --- .pre-commit-hooks.yaml | 2 -- CHANGES.md | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 54a03efe7a1..a1ff41fded8 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,7 +4,6 @@ name: black description: "Black: The uncompromising Python code formatter" entry: black - stages: [pre-commit, pre-merge-commit, pre-push, manual] language: python minimum_pre_commit_version: 2.9.2 require_serial: true @@ -14,7 +13,6 @@ description: "Black: The uncompromising Python code formatter (with Jupyter Notebook support)" entry: black - stages: [pre-commit, pre-merge-commit, pre-push, manual] language: python minimum_pre_commit_version: 2.9.2 require_serial: true diff --git a/CHANGES.md b/CHANGES.md index fca88612afe..360319ac964 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,9 @@ +- Revert the change to run Black's pre-commit integration only on specific git hooks + (#3940) for better compatibility with older versions of pre-commit (#4137) + ### Documentation +- Fix comment handling when parenthesising conditional expressions (#4134) - Format module docstrings the same as class and function docstrings (#4095) - Fix bug where spaces were not added around parenthesized walruses in subscripts, unlike other binary operators (#4109) diff --git a/src/black/linegen.py b/src/black/linegen.py index 574c89b880c..4d468ce0f2e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -170,8 +170,12 @@ def visit_test(self, node: Node) -> Iterator[Line]: ) if not already_parenthesized: + # Similar to logic in wrap_in_parentheses lpar = Leaf(token.LPAR, "") rpar = Leaf(token.RPAR, "") + prefix = node.prefix + node.prefix = "" + lpar.prefix = prefix node.insert_child(0, lpar) node.append_child(rpar) diff --git a/tests/data/cases/conditional_expression.py b/tests/data/cases/conditional_expression.py index c30cd76c791..76251bd9318 100644 --- a/tests/data/cases/conditional_expression.py +++ b/tests/data/cases/conditional_expression.py @@ -67,6 +67,28 @@ def something(): else ValuesListIterable ) + +def foo(wait: bool = True): + # This comment is two + # lines long + + # This is only one + time.sleep(1) if wait else None + time.sleep(1) if wait else None + + # With newline above + time.sleep(1) if wait else None + # Without newline above + time.sleep(1) if wait else None + + +a = "".join( + ( + "", # comment + "" if True else "", + ) +) + # output long_kwargs_single_line = my_function( @@ -159,3 +181,23 @@ def something(): if named else FlatValuesListIterable if flat else ValuesListIterable ) + + +def foo(wait: bool = True): + # This comment is two + # lines long + + # This is only one + time.sleep(1) if wait else None + time.sleep(1) if wait else None + + # With newline above + time.sleep(1) if wait else None + # Without newline above + time.sleep(1) if wait else None + + +a = "".join(( + "", # comment + "" if True else "", +)) From e11eaf2f44d3db5713fb99bdec966ba974b60c8c Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 1 Jan 2024 20:14:57 -0800 Subject: [PATCH 23/29] Make `blank_line_after_nested_stub_class` work for methods (#4141) Fixes #4113 Authored by dhruvmanila --- CHANGES.md | 1 + src/black/lines.py | 8 ++++---- tests/data/cases/nested_stub.py | 27 ++++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3dc0c87f89a..8fb8677dd77 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ - Remove empty lines before docstrings in async functions (#4132) - Address a missing case in the change to allow empty lines at the beginning of all blocks, except immediately before a docstring (#4130) +- For stubs, fix logic to enforce empty line after nested classes with bodies (#4141) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index b544c5e0035..9eb5785da57 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -640,15 +640,15 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: if previous_def is not None: assert self.previous_line is not None if self.mode.is_pyi: - if depth and not current_line.is_def and self.previous_line.is_def: - # Empty lines between attributes and methods should be preserved. - before = 1 if user_had_newline else 0 - elif ( + if ( Preview.blank_line_after_nested_stub_class in self.mode and previous_def.is_class and not previous_def.is_stub_class ): before = 1 + elif depth and not current_line.is_def and self.previous_line.is_def: + # Empty lines between attributes and methods should be preserved. + before = 1 if user_had_newline else 0 elif depth: before = 0 else: diff --git a/tests/data/cases/nested_stub.py b/tests/data/cases/nested_stub.py index b81549ec115..ef13c588ce6 100644 --- a/tests/data/cases/nested_stub.py +++ b/tests/data/cases/nested_stub.py @@ -18,6 +18,18 @@ def function_definition(self): ... assignment = 1 def f2(self) -> str: ... + +class TopLevel: + class Nested1: + foo: int + def bar(self): ... + field = 1 + + class Nested2: + def bar(self): ... + foo: int + field = 1 + # output import sys @@ -41,4 +53,17 @@ def f1(self) -> str: ... def function_definition(self): ... assignment = 1 - def f2(self) -> str: ... \ No newline at end of file + def f2(self) -> str: ... + +class TopLevel: + class Nested1: + foo: int + def bar(self): ... + + field = 1 + + class Nested2: + def bar(self): ... + foo: int + + field = 1 From b7c3a9fedd4cfcc6a6a88aacc7b0f599b63d4716 Mon Sep 17 00:00:00 2001 From: Dragorn421 Date: Thu, 11 Jan 2024 16:46:17 +0100 Subject: [PATCH 24/29] Docs: Add note on `--exclude` about possibly verbose regex (#4145) Co-authored-by: Jelle Zijlstra --- docs/usage_and_configuration/the_basics.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 4f9856c6a47..b541f07907c 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -268,6 +268,11 @@ recursive searches. An empty value means no paths are excluded. Use forward slas directories on all platforms (Windows, too). By default, Black also ignores all paths listed in `.gitignore`. Changing this value will override all default exclusions. +If the regular expression contains newlines, it is treated as a +[verbose regular expression](https://docs.python.org/3/library/re.html#re.VERBOSE). This +is typically useful when setting these options in a `pyproject.toml` configuration file; +see [Configuration format](#configuration-format) for more information. + #### `--extend-exclude` Like `--exclude`, but adds additional files and directories on top of the default values From 9a331d606f3fd60cac19bfbfc3f98cbe8be2517d Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:04:15 -0600 Subject: [PATCH 25/29] fix: Don't allow unparenthesizing walruses (#4155) Signed-off-by: RedGuy12 <61329810+RedGuy12@users.noreply.github.com> Signed-off-by: RedGuy12 --- CHANGES.md | 1 + src/black/linegen.py | 6 +++++- tests/data/cases/walrus_in_dict.py | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 tests/data/cases/walrus_in_dict.py diff --git a/CHANGES.md b/CHANGES.md index 8fb8677dd77..2bd58ed49ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,7 @@ - Address a missing case in the change to allow empty lines at the beginning of all blocks, except immediately before a docstring (#4130) - For stubs, fix logic to enforce empty line after nested classes with bodies (#4141) +- Fix crash when using a walrus in a dictionary (#4155) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 4d468ce0f2e..9a3eb0ce73f 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -242,7 +242,11 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: if i == 0: continue if node.children[i - 1].type == token.COLON: - if child.type == syms.atom and child.children[0].type == token.LPAR: + if ( + child.type == syms.atom + and child.children[0].type == token.LPAR + and not is_walrus_assignment(child) + ): if maybe_make_parens_invisible_in_atom( child, parent=node, diff --git a/tests/data/cases/walrus_in_dict.py b/tests/data/cases/walrus_in_dict.py new file mode 100644 index 00000000000..c33eecd84a6 --- /dev/null +++ b/tests/data/cases/walrus_in_dict.py @@ -0,0 +1,7 @@ +# flags: --preview +{ + "is_update": (up := commit.hash in update_hashes) +} + +# output +{"is_update": (up := commit.hash in update_hashes)} From 7f60f3dbd7d2d36011fbae6c140b35802932952b Mon Sep 17 00:00:00 2001 From: Kevin Paulson Date: Fri, 19 Jan 2024 18:54:32 -0500 Subject: [PATCH 26/29] Update using_black_with_other_tools.md to ensure flake8 configuration examples are consistant (#4157) --- docs/guides/using_black_with_other_tools.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 22c641a7420..e642a1aef33 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -145,7 +145,7 @@ There are a few deviations that cause incompatibilities with _Black_. ``` max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203, E704 ``` #### Why those options above? @@ -184,7 +184,7 @@ extend-ignore = E203, E704 ```ini [flake8] max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203, E704 ``` @@ -195,7 +195,7 @@ extend-ignore = E203 ```ini [flake8] max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203, E704 ``` From 995e4ada14d63a9bec39c5fc83275d0e49742618 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:13:26 -0800 Subject: [PATCH 27/29] Fix unnecessary nesting when wrapping long dict (#4135) Fixes #4129 --- CHANGES.md | 1 + src/black/linegen.py | 7 ++-- tests/data/cases/preview_long_dict_values.py | 38 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2bd58ed49ff..1e75fb58563 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ blocks, except immediately before a docstring (#4130) - For stubs, fix logic to enforce empty line after nested classes with bodies (#4141) - Fix crash when using a walrus in a dictionary (#4155) +- Fix unnecessary parentheses when wrapping long dicts (#4135) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 9a3eb0ce73f..dd296eb801d 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -244,15 +244,14 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: if node.children[i - 1].type == token.COLON: if ( child.type == syms.atom - and child.children[0].type == token.LPAR + and child.children[0].type in OPENING_BRACKETS and not is_walrus_assignment(child) ): - if maybe_make_parens_invisible_in_atom( + maybe_make_parens_invisible_in_atom( child, parent=node, remove_brackets_around_comma=False, - ): - wrap_in_parentheses(node, child, visible=False) + ) else: wrap_in_parentheses(node, child, visible=False) yield from self.visit_default(node) diff --git a/tests/data/cases/preview_long_dict_values.py b/tests/data/cases/preview_long_dict_values.py index fbbacd13d1d..54da76038dc 100644 --- a/tests/data/cases/preview_long_dict_values.py +++ b/tests/data/cases/preview_long_dict_values.py @@ -37,6 +37,26 @@ } +class Random: + def func(): + random_service.status.active_states.inactive = ( + make_new_top_level_state_from_dict( + { + "topLevelBase": { + "secondaryBase": { + "timestamp": 1234, + "latitude": 1, + "longitude": 2, + "actionTimestamp": Timestamp( + seconds=1530584000, nanos=0 + ).ToJsonString(), + } + }, + } + ) + ) + + # output @@ -89,3 +109,21 @@ } ), } + + +class Random: + def func(): + random_service.status.active_states.inactive = ( + make_new_top_level_state_from_dict({ + "topLevelBase": { + "secondaryBase": { + "timestamp": 1234, + "latitude": 1, + "longitude": 2, + "actionTimestamp": ( + Timestamp(seconds=1530584000, nanos=0).ToJsonString() + ), + } + }, + }) + ) From 6f3fb78444655f883780dcc19349226833c677c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:22:56 -0800 Subject: [PATCH 28/29] Bump actions/cache from 3 to 4 (#4162) Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/diff_shades.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 8d8be2550b0..0e1aab00e34 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -72,7 +72,7 @@ jobs: - name: Attempt to use cached baseline analysis id: baseline-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ matrix.baseline-analysis }} key: ${{ matrix.baseline-cache-key }} From 8fe602b1fa91dc6db682d1dba79a8a7341597271 Mon Sep 17 00:00:00 2001 From: Daniel Krzeminski Date: Mon, 22 Jan 2024 11:46:57 -0600 Subject: [PATCH 29/29] fix pathlib exception handling with symlinks (#4161) Fixes #4077 --- CHANGES.md | 2 ++ src/black/__init__.py | 6 +++++- src/black/files.py | 24 ++++++++++++++++-------- tests/test_black.py | 14 ++++++++++++++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1e75fb58563..f29834a3f7f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,8 @@ +- Fix symlink handling, properly catch and ignore symlinks that point outside of root + (#4161) - Fix cache mtime logic that resulted in false positive cache hits (#4128) ### Packaging diff --git a/src/black/__init__.py b/src/black/__init__.py index 735ba713b8f..e3cbaab5f1d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -49,6 +49,7 @@ find_user_pyproject_toml, gen_python_files, get_gitignore, + get_root_relative_path, normalize_path_maybe_ignore, parse_pyproject_toml, path_is_excluded, @@ -700,7 +701,10 @@ def get_sources( # Compare the logic here to the logic in `gen_python_files`. if is_stdin or path.is_file(): - root_relative_path = path.absolute().relative_to(root).as_posix() + root_relative_path = get_root_relative_path(path, root, report) + + if root_relative_path is None: + continue root_relative_path = "/" + root_relative_path diff --git a/src/black/files.py b/src/black/files.py index 858303ca1a3..65951efdbe8 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -259,14 +259,7 @@ def normalize_path_maybe_ignore( try: abspath = path if path.is_absolute() else Path.cwd() / path normalized_path = abspath.resolve() - try: - root_relative_path = normalized_path.relative_to(root).as_posix() - except ValueError: - if report: - report.path_ignored( - path, f"is a symbolic link that points outside {root}" - ) - return None + root_relative_path = get_root_relative_path(normalized_path, root, report) except OSError as e: if report: @@ -276,6 +269,21 @@ def normalize_path_maybe_ignore( return root_relative_path +def get_root_relative_path( + path: Path, + root: Path, + report: Optional[Report] = None, +) -> Optional[str]: + """Returns the file path relative to the 'root' directory""" + try: + root_relative_path = path.absolute().relative_to(root).as_posix() + except ValueError: + if report: + report.path_ignored(path, f"is a symbolic link that points outside {root}") + return None + return root_relative_path + + def _path_is_ignored( root_relative_path: str, root: Path, diff --git a/tests/test_black.py b/tests/test_black.py index 0af5fd2a1f4..2b5fab5d28d 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2592,6 +2592,20 @@ def test_symlinks(self) -> None: outside_root_symlink.resolve.assert_called_once() ignored_symlink.resolve.assert_not_called() + def test_get_sources_with_stdin_symlink_outside_root( + self, + ) -> None: + path = THIS_DIR / "data" / "include_exclude_tests" + stdin_filename = str(path / "b/exclude/a.py") + outside_root_symlink = Path("/target_directory/a.py") + with patch("pathlib.Path.resolve", return_value=outside_root_symlink): + assert_collected_sources( + root=Path("target_directory/"), + src=["-"], + expected=[], + stdin_filename=stdin_filename, + ) + @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None)) def test_get_sources_with_stdin(self) -> None: src = ["-"]