From 58f31a70efe6509ce8213afac998bc5d5bb7e34d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 7 Nov 2023 22:10:35 -0800 Subject: [PATCH 01/33] Add new release template --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b565d510a71..9446927b8d1 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.11.0 ### Highlights From 1b6b0bfcac37428f7f2eb6c97fd0a25628324db7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 17 Nov 2023 15:57:00 +0000 Subject: [PATCH 02/33] Improve annotations for `black.concurrency.cancel` (#4047) --- src/black/concurrency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/concurrency.py b/src/black/concurrency.py index 55c96b66c86..ff0a8f5fd32 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -38,7 +38,7 @@ def maybe_install_uvloop() -> None: pass -def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None: +def cancel(tasks: Iterable["asyncio.Future[Any]"]) -> None: """asyncio signal handler that cancels all `tasks` and reports to stderr.""" err("Aborted!") for task in tasks: From 5773d5cd2b532da185808f974a5875ca09064e28 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:39:44 -0600 Subject: [PATCH 03/33] Document target version inference (#4048) --- docs/usage_and_configuration/the_basics.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 6e7ee584cf9..546fdc474e8 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -67,6 +67,10 @@ In a [configuration file](#configuration-via-a-file), you can write: target-version = ["py38", "py39", "py310", "py311"] ``` +By default, Black will infer target versions from the project metadata in +`pyproject.toml`, specifically the `[project.requires-python]` field. If this does not +yield conclusive results, Black will use per-file auto-detection. + _Black_ uses this option to decide what grammar to use to parse your code. In addition, it may use it to decide what style to use. For example, support for a trailing comma after `*args` in a function call was added in Python 3.5, so _Black_ will add this comma From 85b1c71a3445f32860d7b139ae4de4824f6ae102 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 18 Nov 2023 19:15:07 +0000 Subject: [PATCH 04/33] Block aiohttp==3.9.0 from being installed in CI on Windows/pypy (#4051) --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c0302d2302a..bea8e77ba04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,8 @@ dynamic = ["readme", "version"] colorama = ["colorama>=0.4.3"] uvloop = ["uvloop>=0.15.2"] d = [ - "aiohttp>=3.7.4", + "aiohttp>=3.7.4; sys_platform != 'win32' or implementation_name != 'pypy'", + "aiohttp>=3.7.4, !=3.9.0; sys_platform == 'win32' and implementation_name == 'pypy'", ] jupyter = [ "ipython>=7.8.0", From c4cd200a063a4d5546b547809aa1e607f03c3f59 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 18 Nov 2023 19:41:46 +0000 Subject: [PATCH 05/33] Make flake8 pass when run with Python 3.12 (#4050) --- src/black/parsing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/black/parsing.py b/src/black/parsing.py index ea282d1805c..178a7ef10e2 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -175,7 +175,7 @@ def stringify_ast(node: ast.AST, depth: int = 0) -> Iterator[str]: except AttributeError: continue - yield f"{' ' * (depth+1)}{field}=" + yield f"{' ' * (depth + 1)}{field}=" if isinstance(value, list): for item in value: @@ -211,6 +211,6 @@ def stringify_ast(node: ast.AST, depth: int = 0) -> Iterator[str]: normalized = value.rstrip() else: normalized = value - yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" + yield f"{' ' * (depth + 2)}{normalized!r}, # {value.__class__.__name__}" yield f"{' ' * depth}) # /{node.__class__.__name__}" From d93a942a79762484a0f72c6fa271b45ec377009b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 18 Nov 2023 19:42:36 +0000 Subject: [PATCH 06/33] Upgrade mypy to 1.6.1 (#4049) --- .pre-commit-config.yaml | 2 +- CHANGES.md | 2 +- pyproject.toml | 4 ++-- scripts/check_pre_commit_rev_in_example.py | 2 +- scripts/check_version_in_basics_example.py | 2 +- scripts/diff_shades_gha_helper.py | 2 +- scripts/fuzz.py | 2 +- scripts/make_width_table.py | 2 +- src/blackd/__init__.py | 4 +--- tests/optional.py | 10 +++++++++- 10 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 623e661ac07..c153746b621 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: exclude: ^src/blib2to3/ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.6.1 hooks: - id: mypy exclude: ^docs/conf.py diff --git a/CHANGES.md b/CHANGES.md index 9446927b8d1..13b6c7bdb21 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,7 +20,7 @@ ### Packaging - +- Upgrade to mypy 1.6.1 (#4049) ### Parser diff --git a/pyproject.toml b/pyproject.toml index bea8e77ba04..f8f5155e898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,7 @@ macos-max-compat = true enable-by-default = false dependencies = [ "hatch-mypyc>=0.16.0", - "mypy==1.5.1", + "mypy==1.6.1", "click==8.1.3", # avoid https://github.com/pallets/click/issues/2558 ] require-runtime-dependencies = true @@ -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.5.1' 'click==8.1.3'", + "python -m pip install 'hatchling==1.18.0' hatch-vcs hatch-fancy-pypi-readme 'hatch-mypyc>=0.16.0' 'mypy==1.6.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__)') """, ] diff --git a/scripts/check_pre_commit_rev_in_example.py b/scripts/check_pre_commit_rev_in_example.py index 107c6444dca..cc45a31e1ed 100644 --- a/scripts/check_pre_commit_rev_in_example.py +++ b/scripts/check_pre_commit_rev_in_example.py @@ -14,7 +14,7 @@ import commonmark import yaml -from bs4 import BeautifulSoup # type: ignore[import] +from bs4 import BeautifulSoup # type: ignore[import-untyped] def main(changes: str, source_version_control: str) -> None: diff --git a/scripts/check_version_in_basics_example.py b/scripts/check_version_in_basics_example.py index 0f42bafe334..c90fdb43da5 100644 --- a/scripts/check_version_in_basics_example.py +++ b/scripts/check_version_in_basics_example.py @@ -8,7 +8,7 @@ import sys import commonmark -from bs4 import BeautifulSoup # type: ignore[import] +from bs4 import BeautifulSoup # type: ignore[import-untyped] def main(changes: str, the_basics: str) -> None: diff --git a/scripts/diff_shades_gha_helper.py b/scripts/diff_shades_gha_helper.py index 895516deb51..8cd8ba155ce 100644 --- a/scripts/diff_shades_gha_helper.py +++ b/scripts/diff_shades_gha_helper.py @@ -119,7 +119,7 @@ def main() -> None: @main.command("config", help="Acquire run configuration and metadata.") @click.argument("event", type=click.Choice(["push", "pull_request"])) def config(event: Literal["push", "pull_request"]) -> None: - import diff_shades # type: ignore[import] + import diff_shades # type: ignore[import-not-found] if event == "push": jobs = [{"mode": "preview-changes", "force-flag": "--force-preview-style"}] diff --git a/scripts/fuzz.py b/scripts/fuzz.py index 929d3eac4f5..018b66e0ea2 100644 --- a/scripts/fuzz.py +++ b/scripts/fuzz.py @@ -80,7 +80,7 @@ def test_idempotent_any_syntatically_valid_python( try: import sys - import atheris # type: ignore[import] + import atheris # type: ignore[import-not-found] except ImportError: pass else: diff --git a/scripts/make_width_table.py b/scripts/make_width_table.py index 3c7cae60f7f..1b53c1b2cc9 100644 --- a/scripts/make_width_table.py +++ b/scripts/make_width_table.py @@ -20,7 +20,7 @@ from os.path import basename, dirname, join from typing import Iterable, Tuple -import wcwidth # type: ignore[import] +import wcwidth # type: ignore[import-not-found] def make_width_table() -> Iterable[Tuple[int, int, int]]: diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py index 972f24181cb..6b0f3d33295 100644 --- a/src/blackd/__init__.py +++ b/src/blackd/__init__.py @@ -74,9 +74,7 @@ def main(bind_host: str, bind_port: int) -> None: app = make_app() ver = black.__version__ black.out(f"blackd version {ver} listening on {bind_host} port {bind_port}") - # TODO: aiohttp had an incorrect annotation for `print` argument, - # It'll be fixed once aiohttp releases that code - web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None) # type: ignore[arg-type] + web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None) def make_app() -> web.Application: diff --git a/tests/optional.py b/tests/optional.py index 3f5277b6b03..70ee823e316 100644 --- a/tests/optional.py +++ b/tests/optional.py @@ -26,7 +26,15 @@ from pytest import StashKey except ImportError: # pytest < 7 - from _pytest.store import StoreKey as StashKey # type: ignore[import, no-redef] + # + # "isort: skip" is needed or it moves the "type: ignore" to the following line + # because of the line length, and then mypy complains. + # Of course, adding the "isort: skip" means that + # flake8-bugbear then also complains about the line length, + # so we *also* need a "noqa" comment for good measure :) + from _pytest.store import ( # type: ignore[import-not-found, no-redef] # isort: skip # noqa: B950 + StoreKey as StashKey, + ) log = logging.getLogger(__name__) From 11da02da72ed437facde3658bb61ddebce21e7a4 Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Sat, 18 Nov 2023 11:47:05 -0800 Subject: [PATCH 07/33] Handle more huggable immediately nested parens/brackets. (#4012) Fixes #4011 --- CHANGES.md | 3 + docs/conf.py | 36 +++--- docs/the_black_code_style/future_style.md | 17 ++- src/black/cache.py | 6 +- src/black/handle_ipynb_magics.py | 54 ++++---- src/black/linegen.py | 69 ++++++++-- ..._parens_with_braces_and_square_brackets.py | 122 ++++++++++++++++-- .../cases/preview_long_strings__regression.py | 15 +-- 8 files changed, 233 insertions(+), 89 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 13b6c7bdb21..29f037b4767 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ +- Additional cases of immediately nested tuples, lists, and dictionaries are now + indented less (#4012) + ### Configuration diff --git a/docs/conf.py b/docs/conf.py index 6b645435325..52a849d06a4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -149,15 +149,13 @@ def make_pypi_svg(version: str) -> None: # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). -latex_documents = [ - ( - master_doc, - "black.tex", - "Documentation for Black", - "Łukasz Langa and contributors to Black", - "manual", - ) -] +latex_documents = [( + master_doc, + "black.tex", + "Documentation for Black", + "Łukasz Langa and contributors to Black", + "manual", +)] # -- Options for manual page output ------------------------------------------ @@ -172,17 +170,15 @@ def make_pypi_svg(version: str) -> None: # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "Black", - "Documentation for Black", - author, - "Black", - "The uncompromising Python code formatter", - "Miscellaneous", - ) -] +texinfo_documents = [( + master_doc, + "Black", + "Documentation for Black", + author, + "Black", + "The uncompromising Python code formatter", + "Miscellaneous", +)] # -- Options for Epub output ------------------------------------------------- diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 944ffad033e..428bd87ab50 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -116,8 +116,7 @@ my_dict = { ### Improved multiline dictionary and list indentation for sole function parameter For better readability and less verticality, _Black_ now pairs parentheses ("(", ")") -with braces ("{", "}") and square brackets ("[", "]") on the same line for single -parameter function calls. For example: +with braces ("{", "}") and square brackets ("[", "]") on the same line. For example: ```python foo( @@ -127,6 +126,14 @@ foo( 3, ] ) + +nested_array = [ + [ + 1, + 2, + 3, + ] +] ``` will be changed to: @@ -137,6 +144,12 @@ foo([ 2, 3, ]) + +nested_array = [[ + 1, + 2, + 3, +]] ``` This also applies to list and dictionary unpacking: diff --git a/src/black/cache.py b/src/black/cache.py index 6a332304981..6baa096baca 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -124,9 +124,9 @@ def filtered_cached(self, sources: Iterable[Path]) -> Tuple[Set[Path], Set[Path] def write(self, sources: Iterable[Path]) -> None: """Update the cache file data and write a new cache file.""" - self.file_data.update(**{ - str(src.resolve()): Cache.get_file_data(src) for src in sources - }) + self.file_data.update( + **{str(src.resolve()): Cache.get_file_data(src) for src in sources} + ) try: CACHE_DIR.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile( diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 55ef2267df8..5b2847cb0c4 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -17,36 +17,30 @@ from black.output import out from black.report import NothingChanged -TRANSFORMED_MAGICS = frozenset( - ( - "get_ipython().run_cell_magic", - "get_ipython().system", - "get_ipython().getoutput", - "get_ipython().run_line_magic", - ) -) -TOKENS_TO_IGNORE = frozenset( - ( - "ENDMARKER", - "NL", - "NEWLINE", - "COMMENT", - "DEDENT", - "UNIMPORTANT_WS", - "ESCAPED_NL", - ) -) -PYTHON_CELL_MAGICS = frozenset( - ( - "capture", - "prun", - "pypy", - "python", - "python3", - "time", - "timeit", - ) -) +TRANSFORMED_MAGICS = frozenset(( + "get_ipython().run_cell_magic", + "get_ipython().system", + "get_ipython().getoutput", + "get_ipython().run_line_magic", +)) +TOKENS_TO_IGNORE = frozenset(( + "ENDMARKER", + "NL", + "NEWLINE", + "COMMENT", + "DEDENT", + "UNIMPORTANT_WS", + "ESCAPED_NL", +)) +PYTHON_CELL_MAGICS = frozenset(( + "capture", + "prun", + "pypy", + "python", + "python3", + "time", + "timeit", +)) TOKEN_HEX = secrets.token_hex diff --git a/src/black/linegen.py b/src/black/linegen.py index e2c961d7a01..8a2cd4710b9 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -817,25 +817,66 @@ def _first_right_hand_split( body_leaves.reverse() head_leaves.reverse() - if Preview.hug_parens_with_braces_and_square_brackets in line.mode: - is_unpacking = 1 if body_leaves[0].type in [token.STAR, token.DOUBLESTAR] else 0 - if ( - tail_leaves[0].type == token.RPAR - and tail_leaves[0].value - and tail_leaves[0].opening_bracket is head_leaves[-1] - and body_leaves[-1].type in [token.RBRACE, token.RSQB] - and body_leaves[-1].opening_bracket is body_leaves[is_unpacking] + body: Optional[Line] = None + if ( + Preview.hug_parens_with_braces_and_square_brackets in line.mode + and tail_leaves[0].value + and tail_leaves[0].opening_bracket is head_leaves[-1] + ): + inner_body_leaves = list(body_leaves) + hugged_opening_leaves: List[Leaf] = [] + hugged_closing_leaves: List[Leaf] = [] + is_unpacking = body_leaves[0].type in [token.STAR, token.DOUBLESTAR] + unpacking_offset: int = 1 if is_unpacking else 0 + while ( + len(inner_body_leaves) >= 2 + unpacking_offset + and inner_body_leaves[-1].type in CLOSING_BRACKETS + and inner_body_leaves[-1].opening_bracket + is inner_body_leaves[unpacking_offset] ): - head_leaves = head_leaves + body_leaves[: 1 + is_unpacking] - tail_leaves = body_leaves[-1:] + tail_leaves - body_leaves = body_leaves[1 + is_unpacking : -1] + if unpacking_offset: + hugged_opening_leaves.append(inner_body_leaves.pop(0)) + unpacking_offset = 0 + hugged_opening_leaves.append(inner_body_leaves.pop(0)) + hugged_closing_leaves.insert(0, inner_body_leaves.pop()) + + if hugged_opening_leaves and inner_body_leaves: + inner_body = bracket_split_build_line( + inner_body_leaves, + line, + hugged_opening_leaves[-1], + component=_BracketSplitComponent.body, + ) + if ( + line.mode.magic_trailing_comma + and inner_body_leaves[-1].type == token.COMMA + ): + should_hug = True + else: + line_length = line.mode.line_length - sum( + len(str(leaf)) + for leaf in hugged_opening_leaves + hugged_closing_leaves + ) + if is_line_short_enough( + inner_body, mode=replace(line.mode, line_length=line_length) + ): + # Do not hug if it fits on a single line. + should_hug = False + else: + should_hug = True + if should_hug: + body_leaves = inner_body_leaves + head_leaves.extend(hugged_opening_leaves) + tail_leaves = hugged_closing_leaves + tail_leaves + body = inner_body # No need to re-calculate the body again later. head = bracket_split_build_line( head_leaves, line, opening_bracket, component=_BracketSplitComponent.head ) - body = bracket_split_build_line( - body_leaves, line, opening_bracket, component=_BracketSplitComponent.body - ) + if body is None: + body = bracket_split_build_line( + body_leaves, line, opening_bracket, component=_BracketSplitComponent.body + ) tail = bracket_split_build_line( tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail ) diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 97b5b2e8dd1..9e5c9eb8546 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -128,6 +128,19 @@ def foo_square_brackets(request): func({"short line"}) func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) +func(("long line", "long long line", "long long long line", "long long long long line", "long long long long long line")) +func((("long line", "long long line", "long long long line", "long long long long line", "long long long long long line"))) +func([["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]) + +# Do not hug if the argument fits on a single line. +func({"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}) +func(("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")) +func(["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]) +func(**{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"}) +func(*("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----")) +array = [{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}] +array = [("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")] +array = [["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]] foooooooooooooooooooo( [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} @@ -137,6 +150,13 @@ def foo_square_brackets(request): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ) +nested_mapping = {"key": [{"a very long key 1": "with a very long value", "a very long key 2": "with a very long value"}]} +nested_array = [[["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]] +explicit_exploding = [[["short", "line",],],] +single_item_do_not_explode = Context({ + "version": get_docs_version(), +}) + foo(*["long long long long long line", "long long long long long line", "long long long long long line"]) foo(*[str(i) for i in range(100000000000000000000000000000000000000000000000000000000000)]) @@ -152,6 +172,9 @@ def foo_square_brackets(request): foo(**{x: y for x, y in enumerate(["long long long long line","long long long long line"])}) +# Edge case when deciding whether to hug the brackets without inner content. +very_very_very_long_variable = very_very_very_long_module.VeryVeryVeryVeryLongClassName([[]]) + for foo in ["a", "b"]: output.extend([ individual @@ -276,9 +299,9 @@ def foo_square_brackets(request): ) func([x for x in "short line"]) -func([ - x for x in "long line long line long line long line long line long line long line" -]) +func( + [x for x in "long line long line long line long line long line long line long line"] +) func([ x for x in [ @@ -295,15 +318,60 @@ def foo_square_brackets(request): "long long long long line", "long long long long long line", }) -func({ - { - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", - } -}) +func({{ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}}) +func(( + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +)) +func((( + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +))) +func([[ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +]]) + +# Do not hug if the argument fits on a single line. +func( + {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} +) +func( + ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") +) +func( + ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] +) +func( + **{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"} +) +func( + *("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----") +) +array = [ + {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} +] +array = [ + ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") +] +array = [ + ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] +] foooooooooooooooooooo( [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} @@ -313,6 +381,31 @@ def foo_square_brackets(request): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {x}, "a string", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ) +nested_mapping = { + "key": [{ + "a very long key 1": "with a very long value", + "a very long key 2": "with a very long value", + }] +} +nested_array = [[[ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +]]] +explicit_exploding = [ + [ + [ + "short", + "line", + ], + ], +] +single_item_do_not_explode = Context({ + "version": get_docs_version(), +}) + foo(*[ "long long long long long line", "long long long long long line", @@ -334,6 +427,11 @@ def foo_square_brackets(request): x: y for x, y in enumerate(["long long long long line", "long long long long line"]) }) +# Edge case when deciding whether to hug the brackets without inner content. +very_very_very_long_variable = very_very_very_long_module.VeryVeryVeryVeryLongClassName( + [[]] +) + for foo in ["a", "b"]: output.extend([ individual diff --git a/tests/data/cases/preview_long_strings__regression.py b/tests/data/cases/preview_long_strings__regression.py index 313d898cd83..5e76a8cf61c 100644 --- a/tests/data/cases/preview_long_strings__regression.py +++ b/tests/data/cases/preview_long_strings__regression.py @@ -611,14 +611,13 @@ def foo(): class A: def foo(): - XXXXXXXXXXXX.append( - ( - "xxx_xxxxxxxxxx(xxxxx={}, xxxx={}, xxxxx, xxxx_xxxx_xxxxxxxxxx={})" - .format(xxxxx, xxxx, xxxx_xxxx_xxxxxxxxxx), - my_var, - my_other_var, - ) - ) + XXXXXXXXXXXX.append(( + "xxx_xxxxxxxxxx(xxxxx={}, xxxx={}, xxxxx, xxxx_xxxx_xxxxxxxxxx={})".format( + xxxxx, xxxx, xxxx_xxxx_xxxxxxxxxx + ), + my_var, + my_other_var, + )) class A: From 80a166f2e115bda9f33d29a5ea313be2557dc7fd Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sat, 18 Nov 2023 12:09:55 -0800 Subject: [PATCH 08/33] Make black[d] install + test run with 3.12 (#4035) * Make black[d] install + test run with 3.12 - With aiohttp >= 3.9.0 we can now install all dependencies with 3.12 - Add actions to run 3.12 - Lint still needs to be 3.11 Test: - `python3.12 -m venv /tmp/tb --upgrade-deps` - `/tmp/tb/bin/pip install tox` - `/tmp/tb/bin/pip install .[d]` - `/tmp/tb/bin/tox -e py312` ``` py312: OK (37.61=setup[3.98]+cmd[3.83,0.36,19.54,6.46,3.00,0.44] seconds) congratulations :) (37.63 seconds) ``` * Move to pypy-3.9 --------- Co-authored-by: Jelle Zijlstra --- .github/workflows/doc.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/test.yml | 2 +- CHANGES.md | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 9a23e19cadd..fa3d87c70f5 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -26,7 +26,7 @@ jobs: - name: Set up latest Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "*" - name: Install dependencies run: | diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 1b5a50c0e0b..48c26452c54 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f33f2b814f..3f8928cc42a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.8"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.9"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/CHANGES.md b/CHANGES.md index 29f037b4767..9f7ab685afe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,7 +43,7 @@ ### Integrations - +- Enable 3.12 CI (#4035) ### Documentation diff --git a/pyproject.toml b/pyproject.toml index f8f5155e898..e63e0aea3ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ [tool.black] line-length = 88 -target-version = ['py37', 'py38'] +target-version = ['py38'] include = '\.pyi?$' extend-exclude = ''' /( From 96faa3b469298573be9d3e2d55982328ee5feef9 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sat, 18 Nov 2023 18:09:47 -0800 Subject: [PATCH 09/33] [docker] Build with 3.12 image (#4055) Test: ``` crl-m1:black cooper$ docker build --tag black_3_12 . ... => [stage-1 2/2] COPY --from=builder /opt/venv /opt/venv 0.2s => exporting to image 0.1s => => exporting layers 0.1s => => writing image sha256:bd66acc9d76d2c40d287b0684ce6601401631e0468204c4e6a81f8f1eebaf1dd 0.0s => => naming to docker.io/library/black_3_12 crl-m1:black cooper$ docker image ls | grep black_3_12 black_3_12 latest bd66acc9d76d 59 seconds ago 193MB ``` --- CHANGES.md | 1 + Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9f7ab685afe..b3beefdd80e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ ### Integrations - Enable 3.12 CI (#4035) +- Build docker images with 3.12 (#4055) ### Documentation diff --git a/Dockerfile b/Dockerfile index bfd9acccb99..ab961a2f491 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim AS builder +FROM python:3.12-slim AS builder RUN mkdir /src COPY . /src/ @@ -12,7 +12,7 @@ RUN . /opt/venv/bin/activate && pip install --no-cache-dir --upgrade pip setupto && cd /src && hatch build -t wheel \ && pip install --no-cache-dir dist/*-cp*[colorama,d,uvloop] -FROM python:3.11-slim +FROM python:3.12-slim # copy only Python packages to limit the image size COPY --from=builder /opt/venv /opt/venv From f23b845a295f1422a1989f5ea1560ad2e0fadcdd Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sat, 18 Nov 2023 18:11:50 -0800 Subject: [PATCH 10/33] [ci] Move 'lint' to 3.12 (#4053) - Add to run on MacOS + Windows too - Do not install [d] dependecies as blackd is not actually run / checked - Move to default GitHub action version - which is 3.12 today --- .github/workflows/lint.yml | 12 ++++++++---- tox.ini | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7fe1b04eb02..d1ad23bb2ab 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Lint +name: Lint + format ourselves on: [push, pull_request] @@ -11,7 +11,11 @@ jobs: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 @@ -26,12 +30,12 @@ jobs: - name: Set up latest Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "*" - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install -e '.[d]' + python -m pip install -e '.' python -m pip install tox - name: Run pre-commit hooks diff --git a/tox.ini b/tox.ini index 018cef993c0..8b4175d23fe 100644 --- a/tox.ini +++ b/tox.ini @@ -94,5 +94,5 @@ commands = setenv = PYTHONPATH = {toxinidir}/src skip_install = True commands = - pip install -e .[d] + pip install -e . black --check {toxinidir}/src {toxinidir}/tests From 30c6bb3651634ebf66177c85b27e7e277316eab7 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 19 Nov 2023 10:44:00 -0800 Subject: [PATCH 11/33] [docker ci] Split up amd64 (x86_64) and arm64 builds (#4054) * [docker ci] Split up amd64 (x86_64) and arm64 builds - Lets run them seperately to cut down total time - Will also more clearly show if either arch has specific problems - Kept amd64 (x86_64) using qemu actions so if GitHub ever offers arm64 boxes it could stay working too Fixes #3971 * Add CHANGES entry --------- Co-authored-by: Jelle Zijlstra --- .../{docker.yml => docker_amd64.yml} | 8 +-- .github/workflows/docker_arm64.yml | 69 +++++++++++++++++++ CHANGES.md | 1 + 3 files changed, 74 insertions(+), 4 deletions(-) rename .github/workflows/{docker.yml => docker_amd64.yml} (92%) create mode 100644 .github/workflows/docker_arm64.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker_amd64.yml similarity index 92% rename from .github/workflows/docker.yml rename to .github/workflows/docker_amd64.yml index ee858236fcf..846cd8c74e3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker_amd64.yml @@ -1,4 +1,4 @@ -name: docker +name: docker amd64 (x86_64) on: push: @@ -39,7 +39,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} @@ -50,7 +50,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: pyfound/black:latest_release @@ -61,7 +61,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: pyfound/black:latest_prerelease diff --git a/.github/workflows/docker_arm64.yml b/.github/workflows/docker_arm64.yml new file mode 100644 index 00000000000..ddd554165af --- /dev/null +++ b/.github/workflows/docker_arm64.yml @@ -0,0 +1,69 @@ +name: docker arm64 + +on: + push: + branches: + - "main" + release: + types: [published] + +permissions: + contents: read + +jobs: + docker: + if: github.repository == 'psf/black' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Check + set version tag + run: + echo "GIT_TAG=$(git describe --candidates=0 --tags 2> /dev/null || echo + latest_non_release)" >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64 + push: true + tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} + + - name: Build and push latest_release tag + if: + ${{ github.event_name == 'release' && github.event.action == 'published' && + !github.event.release.prerelease }} + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64 + push: true + tags: pyfound/black:latest_release + + - name: Build and push latest_prerelease tag + if: + ${{ github.event_name == 'release' && github.event.action == 'published' && + github.event.release.prerelease }} + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64 + push: true + tags: pyfound/black:latest_prerelease + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/CHANGES.md b/CHANGES.md index b3beefdd80e..8d0f10a2f3a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ ### Integrations - Enable 3.12 CI (#4035) +- Build docker images in parallel (#4054) - Build docker images with 3.12 (#4055) ### Documentation From ec4a1525ee7f0daf05a2ab709123fbb0fe69e4e2 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Sun, 19 Nov 2023 11:28:00 -0800 Subject: [PATCH 12/33] [docker ci] Revert "parallel" builds in seperate actions (#4057) - Broke tagging images together - Saved only a few mins - x86_64 build is fast, time is all spent on cross compile of arm64 - Also remove evil copy pasta ... which is nice Was worth an attempt. --- .../{docker_amd64.yml => docker.yml} | 8 +-- .github/workflows/docker_arm64.yml | 69 ------------------- 2 files changed, 4 insertions(+), 73 deletions(-) rename .github/workflows/{docker_amd64.yml => docker.yml} (92%) delete mode 100644 .github/workflows/docker_arm64.yml diff --git a/.github/workflows/docker_amd64.yml b/.github/workflows/docker.yml similarity index 92% rename from .github/workflows/docker_amd64.yml rename to .github/workflows/docker.yml index 846cd8c74e3..ee858236fcf 100644 --- a/.github/workflows/docker_amd64.yml +++ b/.github/workflows/docker.yml @@ -1,4 +1,4 @@ -name: docker amd64 (x86_64) +name: docker on: push: @@ -39,7 +39,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} @@ -50,7 +50,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true tags: pyfound/black:latest_release @@ -61,7 +61,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true tags: pyfound/black:latest_prerelease diff --git a/.github/workflows/docker_arm64.yml b/.github/workflows/docker_arm64.yml deleted file mode 100644 index ddd554165af..00000000000 --- a/.github/workflows/docker_arm64.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: docker arm64 - -on: - push: - branches: - - "main" - release: - types: [published] - -permissions: - contents: read - -jobs: - docker: - if: github.repository == 'psf/black' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Check + set version tag - run: - echo "GIT_TAG=$(git describe --candidates=0 --tags 2> /dev/null || echo - latest_non_release)" >> $GITHUB_ENV - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/arm64 - push: true - tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} - - - name: Build and push latest_release tag - if: - ${{ github.event_name == 'release' && github.event.action == 'published' && - !github.event.release.prerelease }} - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/arm64 - push: true - tags: pyfound/black:latest_release - - - name: Build and push latest_prerelease tag - if: - ${{ github.event_name == 'release' && github.event.action == 'published' && - github.event.release.prerelease }} - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/arm64 - push: true - tags: pyfound/black:latest_prerelease - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} From 89e28ea66f50d4281cb9f624e31566aed9d5aab1 Mon Sep 17 00:00:00 2001 From: tungol Date: Mon, 20 Nov 2023 20:44:33 -0800 Subject: [PATCH 13/33] Permit standalone form feed characters at the module level (#4021) Co-authored-by: Stephen Morton Co-authored-by: Jelle Zijlstra --- CHANGES.md | 2 +- .../reference/reference_functions.rst | 4 +- docs/the_black_code_style/future_style.md | 11 + src/black/comments.py | 39 ++- src/black/linegen.py | 25 +- src/black/lines.py | 14 +- src/black/mode.py | 1 + src/black/nodes.py | 7 + src/black/output.py | 23 +- src/blib2to3/pgen2/driver.py | 2 + tests/data/cases/preview_form_feeds.py | 225 ++++++++++++++++++ 11 files changed, 318 insertions(+), 35 deletions(-) create mode 100644 tests/data/cases/preview_form_feeds.py diff --git a/CHANGES.md b/CHANGES.md index 8d0f10a2f3a..4c3fbf1afc8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ ### Preview style - +- Standalone form feed characters at the module level are no longer removed (#4021) - Additional cases of immediately nested tuples, lists, and dictionaries are now indented less (#4012) diff --git a/docs/contributing/reference/reference_functions.rst b/docs/contributing/reference/reference_functions.rst index dd92e37a7d4..ebadf6975a7 100644 --- a/docs/contributing/reference/reference_functions.rst +++ b/docs/contributing/reference/reference_functions.rst @@ -149,7 +149,7 @@ Utilities .. autofunction:: black.numerics.normalize_numeric_literal -.. autofunction:: black.linegen.normalize_prefix +.. autofunction:: black.comments.normalize_trailing_prefix .. autofunction:: black.strings.normalize_string_prefix @@ -168,3 +168,5 @@ Utilities .. autofunction:: black.strings.sub_twice .. autofunction:: black.nodes.whitespace + +.. autofunction:: black.nodes.make_simple_prefix diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 428bd87ab50..f55ea5f60a9 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -296,3 +296,14 @@ s = ( # Top comment # Bottom comment ) ``` + +======= + +### Form feed characters + +_Black_ will now retain form feed characters on otherwise empty lines at the module +level. Only one form feed is retained for a group of consecutive empty lines. Where +there are two empty lines in a row, the form feed will be placed on the second line. + +_Black_ already retained form feed literals inside a comment or inside a string. This +remains the case. diff --git a/src/black/comments.py b/src/black/comments.py index 862fc7607cc..8a0e925fdc0 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -10,6 +10,7 @@ WHITESPACE, container_of, first_leaf_of, + make_simple_prefix, preceding_leaf, syms, ) @@ -44,6 +45,7 @@ class ProtoComment: value: str # content of the comment newlines: int # how many newlines before the comment consumed: int # how many characters of the original leaf's prefix did we consume + form_feed: bool # is there a form feed before the comment def generate_comments(leaf: LN) -> Iterator[Leaf]: @@ -65,8 +67,12 @@ def generate_comments(leaf: LN) -> Iterator[Leaf]: Inline comments are emitted as regular token.COMMENT leaves. Standalone are emitted with a fake STANDALONE_COMMENT token identifier. """ + total_consumed = 0 for pc in list_comments(leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER): - yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines) + total_consumed = pc.consumed + prefix = make_simple_prefix(pc.newlines, pc.form_feed) + yield Leaf(pc.type, pc.value, prefix=prefix) + normalize_trailing_prefix(leaf, total_consumed) @lru_cache(maxsize=4096) @@ -79,11 +85,14 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: consumed = 0 nlines = 0 ignored_lines = 0 - for index, line in enumerate(re.split("\r?\n", prefix)): - consumed += len(line) + 1 # adding the length of the split '\n' - line = line.lstrip() + form_feed = False + for index, full_line in enumerate(re.split("\r?\n", prefix)): + consumed += len(full_line) + 1 # adding the length of the split '\n' + line = full_line.lstrip() if not line: nlines += 1 + if "\f" in full_line: + form_feed = True if not line.startswith("#"): # Escaped newlines outside of a comment are not really newlines at # all. We treat a single-line comment following an escaped newline @@ -99,13 +108,33 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]: comment = make_comment(line) result.append( ProtoComment( - type=comment_type, value=comment, newlines=nlines, consumed=consumed + type=comment_type, + value=comment, + newlines=nlines, + consumed=consumed, + form_feed=form_feed, ) ) + form_feed = False nlines = 0 return result +def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None: + """Normalize the prefix that's left over after generating comments. + + Note: don't use backslashes for formatting or you'll lose your voting rights. + """ + remainder = leaf.prefix[total_consumed:] + if "\\" not in remainder: + nl_count = remainder.count("\n") + form_feed = "\f" in remainder and remainder.endswith("\n") + leaf.prefix = make_simple_prefix(nl_count, form_feed) + return + + leaf.prefix = "" + + def make_comment(content: str) -> str: """Return a consistently formatted comment from the given `content` string. diff --git a/src/black/linegen.py b/src/black/linegen.py index 8a2cd4710b9..7fbbe290d7e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -149,7 +149,8 @@ def visit_default(self, node: LN) -> Iterator[Line]: self.current_line.append(comment) yield from self.line() - normalize_prefix(node, inside_brackets=any_open_brackets) + if any_open_brackets: + node.prefix = "" if self.mode.string_normalization and node.type == token.STRING: node.value = normalize_string_prefix(node.value) node.value = normalize_string_quotes(node.value) @@ -1035,8 +1036,6 @@ def bracket_split_build_line( result.inside_brackets = True result.depth += 1 if leaves: - # Since body is a new indent level, remove spurious leading whitespace. - normalize_prefix(leaves[0], inside_brackets=True) # Ensure a trailing comma for imports and standalone function arguments, but # be careful not to add one after any comments or within type annotations. no_commas = ( @@ -1106,7 +1105,7 @@ def split_wrapper( line: Line, features: Collection[Feature], mode: Mode ) -> Iterator[Line]: for split_line in split_func(line, features, mode): - normalize_prefix(split_line.leaves[0], inside_brackets=True) + split_line.leaves[0].prefix = "" yield split_line return split_wrapper @@ -1250,24 +1249,6 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: yield current_line -def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None: - """Leave existing extra newlines if not `inside_brackets`. Remove everything - else. - - Note: don't use backslashes for formatting or you'll lose your voting rights. - """ - if not inside_brackets: - spl = leaf.prefix.split("#") - if "\\" not in spl[0]: - nl_count = spl[-1].count("\n") - if len(spl) > 1: - nl_count -= 1 - leaf.prefix = "\n" * nl_count - return - - leaf.prefix = "" - - def normalize_invisible_parens( # noqa: C901 node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature] ) -> None: diff --git a/src/black/lines.py b/src/black/lines.py index 3ade0a5f4a5..ec6145ff848 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -31,6 +31,7 @@ is_type_comment, is_type_ignore_comment, is_with_or_async_with_stmt, + make_simple_prefix, replace_child, syms, whitespace, @@ -520,12 +521,12 @@ class LinesBlock: before: int = 0 content_lines: List[str] = field(default_factory=list) after: int = 0 + form_feed: bool = False def all_lines(self) -> List[str]: empty_line = str(Line(mode=self.mode)) - return ( - [empty_line * self.before] + self.content_lines + [empty_line * self.after] - ) + prefix = make_simple_prefix(self.before, self.form_feed, empty_line) + return [prefix] + self.content_lines + [empty_line * self.after] @dataclass @@ -550,6 +551,12 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: This is for separating `def`, `async def` and `class` with extra empty lines (two on module-level). """ + form_feed = ( + Preview.allow_form_feeds in self.mode + and current_line.depth == 0 + and bool(current_line.leaves) + and "\f\n" in current_line.leaves[0].prefix + ) before, after = self._maybe_empty_lines(current_line) previous_after = self.previous_block.after if self.previous_block else 0 before = ( @@ -575,6 +582,7 @@ def maybe_empty_lines(self, current_line: Line) -> LinesBlock: original_line=current_line, before=before, after=after, + form_feed=form_feed, ) # Maintain the semantic_leading_comment state. diff --git a/src/black/mode.py b/src/black/mode.py index 1aa5cbecc86..04038f49627 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -194,6 +194,7 @@ class Preview(Enum): allow_empty_first_line_before_new_block_or_comment = auto() single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() + allow_form_feeds = auto() class Deprecated(UserWarning): diff --git a/src/black/nodes.py b/src/black/nodes.py index 9251b0defb0..de53f8e36a3 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -407,6 +407,13 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no return SPACE +def make_simple_prefix(nl_count: int, form_feed: bool, empty_line: str = "\n") -> str: + """Generate a normalized prefix string.""" + if form_feed: + return (empty_line * (nl_count - 1)) + "\f" + empty_line + return empty_line * nl_count + + def preceding_leaf(node: Optional[LN]) -> Optional[Leaf]: """Return the first leaf that precedes `node`, if any.""" while node: diff --git a/src/black/output.py b/src/black/output.py index f4c17f28ea4..7c7dd0fe14e 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -4,8 +4,9 @@ """ import json +import re import tempfile -from typing import Any, Optional +from typing import Any, List, Optional from click import echo, style from mypy_extensions import mypyc_attr @@ -55,12 +56,28 @@ def ipynb_diff(a: str, b: str, a_name: str, b_name: str) -> str: return "".join(diff_lines) +_line_pattern = re.compile(r"(.*?(?:\r\n|\n|\r|$))") + + +def _splitlines_no_ff(source: str) -> List[str]: + """Split a string into lines ignoring form feed and other chars. + + This mimics how the Python parser splits source code. + + A simplified version of the function with the same name in Lib/ast.py + """ + result = [match[0] for match in _line_pattern.finditer(source)] + if result[-1] == "": + result.pop(-1) + return result + + def diff(a: str, b: str, a_name: str, b_name: str) -> str: """Return a unified diff string between strings `a` and `b`.""" import difflib - a_lines = a.splitlines(keepends=True) - b_lines = b.splitlines(keepends=True) + a_lines = _splitlines_no_ff(a) + b_lines = _splitlines_no_ff(b) diff_lines = [] for line in difflib.unified_diff( a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5 diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index e629843f8b9..be3984437a8 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -222,6 +222,8 @@ def _partially_consume_prefix(self, prefix: str, column: int) -> Tuple[str, str] elif char == "\n": # unexpected empty line current_column = 0 + elif char == "\f": + current_column = 0 else: # indent is finished wait_for_nl = True diff --git a/tests/data/cases/preview_form_feeds.py b/tests/data/cases/preview_form_feeds.py new file mode 100644 index 00000000000..2d8653a1f04 --- /dev/null +++ b/tests/data/cases/preview_form_feeds.py @@ -0,0 +1,225 @@ +# flags: --preview + + +# Warning! This file contains form feeds (ASCII 0x0C, often represented by \f or ^L). +# These may be invisible in your editor: ensure you can see them before making changes here. + +# There's one at the start that'll get stripped + +# Comment and statement processing is different enough that we'll test variations of both +# contexts here + +# + + +# + + +# + + + +# + + + +# + + + +# + + +# + + + +# + +# + +# + + \ +# +pass + +pass + + +pass + + +pass + + + +pass + + + +pass + + + +pass + + +pass + + + +pass + +pass + +pass + + +# form feed after a dedent +def foo(): + pass + +pass + + +# form feeds are prohibited inside blocks, or on a line with nonwhitespace + def bar( a = 1 ,b : bool = False ) : + + + pass + + +class Baz: + + def __init__(self): + pass + + + def something(self): + pass + + + +# +pass +pass # +a = 1 + # + pass + a = 1 + +a = [ + +] + +# as internal whitespace of a comment is allowed but why +"form feed literal in a string is okay " + +# form feeds at the very end get removed. + + + +# output + +# Warning! This file contains form feeds (ASCII 0x0C, often represented by \f or ^L). +# These may be invisible in your editor: ensure you can see them before making changes here. + +# There's one at the start that'll get stripped + +# Comment and statement processing is different enough that we'll test variations of both +# contexts here + +# + + +# + + +# + + +# + + +# + + +# + + +# + + +# + +# + +# + +# +pass + +pass + + +pass + + +pass + + +pass + + +pass + + +pass + + +pass + + +pass + +pass + +pass + + +# form feed after a dedent +def foo(): + pass + + +pass + + +# form feeds are prohibited inside blocks, or on a line with nonwhitespace +def bar(a=1, b: bool = False): + pass + + +class Baz: + def __init__(self): + pass + + def something(self): + pass + + +# +pass +pass # +a = 1 +# +pass +a = 1 + +a = [] + +# as internal whitespace of a comment is allowed but why +"form feed literal in a string is okay " + +# form feeds at the very end get removed. From a8062983cd1f8ac8859297c870847906b10cf6a2 Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Mon, 20 Nov 2023 20:45:39 -0800 Subject: [PATCH 14/33] Disable the stability check with --line-ranges for now. (#4034) Co-authored-by: Jelle Zijlstra --- CHANGES.md | 3 ++ docs/usage_and_configuration/the_basics.md | 6 ++++ src/black/__init__.py | 9 ++++-- .../data/cases/line_ranges_diff_edge_case.py | 28 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/data/cases/line_ranges_diff_edge_case.py diff --git a/CHANGES.md b/CHANGES.md index 4c3fbf1afc8..18bab5131e6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,9 @@ +- `--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) + ### Packaging - Upgrade to mypy 1.6.1 (#4049) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 546fdc474e8..0c1a4d3b5a1 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -196,6 +196,12 @@ Example: `black --line-ranges=1-10 --line-ranges=21-30 test.py` will format line This option is mainly for editor integrations, such as "Format Selection". +```{note} +Due to [#4052](https://github.com/psf/black/issues/4052), `--line-ranges` might format +extra lines outside of the ranges when ther are unformatted lines with the exact +content. It also disables _Black_'s formatting stability check in `--safe` mode. +``` + #### `--color` / `--no-color` Show (or do not show) colored diff. Only applies when `--diff` is given. diff --git a/src/black/__init__.py b/src/black/__init__.py index 2455e8648fc..b33beeeeb23 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1465,11 +1465,16 @@ def assert_stable( src: str, dst: str, mode: Mode, *, lines: Collection[Tuple[int, int]] = () ) -> None: """Raise AssertionError if `dst` reformats differently the second time.""" + if lines: + # Formatting specified lines requires `adjusted_lines` to map original lines + # to the formatted lines before re-formatting the previously formatted result. + # Due to less-ideal diff algorithm, some edge cases produce incorrect new line + # ranges. Hence for now, we skip the stable check. + # See https://github.com/psf/black/issues/4033 for context. + return # We shouldn't call format_str() here, because that formats the string # twice and may hide a bug where we bounce back and forth between two # versions. - if lines: - lines = adjusted_lines(lines, src, dst) newdst = _format_str_once(dst, mode=mode, lines=lines) if dst != newdst: log = dump_to_file( diff --git a/tests/data/cases/line_ranges_diff_edge_case.py b/tests/data/cases/line_ranges_diff_edge_case.py new file mode 100644 index 00000000000..f5cb2d0bb5f --- /dev/null +++ b/tests/data/cases/line_ranges_diff_edge_case.py @@ -0,0 +1,28 @@ +# flags: --line-ranges=10-11 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# Reproducible example for https://github.com/psf/black/issues/4033. +# This can be fixed in the future if we use a better diffing algorithm, or make Black +# perform formatting in a single pass. + +print ( "format me" ) +print ( "format me" ) +print ( "format me" ) +print ( "format me" ) +print ( "format me" ) + +# output +# flags: --line-ranges=10-11 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. + +# Reproducible example for https://github.com/psf/black/issues/4033. +# This can be fixed in the future if we use a better diffing algorithm, or make Black +# perform formatting in a single pass. + +print ( "format me" ) +print("format me") +print("format me") +print("format me") +print("format me") From be336bb67fb6c12667836f7fba4993f9be9c61dd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 20 Nov 2023 22:33:16 -0800 Subject: [PATCH 15/33] Run lint job on Ubuntu only (#4061) --- .github/workflows/lint.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d1ad23bb2ab..9c7aca8f869 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,11 +11,7 @@ jobs: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From fb5e5d2be6367a0402a60da94f139dfb8943ed37 Mon Sep 17 00:00:00 2001 From: Henri Holopainen Date: Thu, 23 Nov 2023 05:11:49 +0200 Subject: [PATCH 16/33] Prefer more equal signs before a break when splitting chained assignments (#4010) Fixes #4007 --- CHANGES.md | 2 +- src/black/linegen.py | 64 ++++++++++++++------ src/black/lines.py | 5 ++ tests/data/cases/preview_prefer_rhs_split.py | 21 +++++++ 4 files changed, 71 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 18bab5131e6..6a8b97c75eb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,8 +12,8 @@ ### Preview style +- Prefer more equal signs before a break when splitting chained assignments (#4010) - Standalone form feed characters at the module level are no longer removed (#4021) - - Additional cases of immediately nested tuples, lists, and dictionaries are now indented less (#4012) diff --git a/src/black/linegen.py b/src/black/linegen.py index 7fbbe290d7e..7152568783e 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -910,24 +910,32 @@ def _maybe_split_omitting_optional_parens( try: # The RHSResult Omitting Optional Parens. rhs_oop = _first_right_hand_split(line, omit=omit) - if not ( + prefer_splitting_rhs_mode = ( Preview.prefer_splitting_right_hand_side_of_assignments in line.mode - # the split is right after `=` - and len(rhs.head.leaves) >= 2 - and rhs.head.leaves[-2].type == token.EQUAL - # the left side of assignment contains brackets - and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1]) - # the left side of assignment is short enough (the -1 is for the ending - # optional paren) - and is_line_short_enough( - rhs.head, mode=replace(mode, line_length=mode.line_length - 1) + ) + is_split_right_after_equal = ( + len(rhs.head.leaves) >= 2 and rhs.head.leaves[-2].type == token.EQUAL + ) + rhs_head_contains_brackets = any( + leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1] + ) + # the -1 is for the ending optional paren + rhs_head_short_enough = is_line_short_enough( + rhs.head, mode=replace(mode, line_length=mode.line_length - 1) + ) + rhs_head_explode_blocked_by_magic_trailing_comma = ( + rhs.head.magic_trailing_comma is None + ) + if ( + not ( + prefer_splitting_rhs_mode + and is_split_right_after_equal + and rhs_head_contains_brackets + and rhs_head_short_enough + and rhs_head_explode_blocked_by_magic_trailing_comma ) - # the left side of assignment won't explode further because of magic - # trailing comma - and rhs.head.magic_trailing_comma is None - # the split by omitting optional parens isn't preferred by some other - # reason - and not _prefer_split_rhs_oop(rhs_oop, mode) + # the omit optional parens split is preferred by some other reason + or _prefer_split_rhs_oop_over_rhs(rhs_oop, rhs, mode) ): yield from _maybe_split_omitting_optional_parens( rhs_oop, line, mode, features=features, omit=omit @@ -935,8 +943,12 @@ def _maybe_split_omitting_optional_parens( return except CannotSplit as e: - if not ( - can_be_split(rhs.body) or is_line_short_enough(rhs.body, mode=mode) + # For chained assignments we want to use the previous successful split + if line.is_chained_assignment: + pass + + elif not can_be_split(rhs.body) and not is_line_short_enough( + rhs.body, mode=mode ): raise CannotSplit( "Splitting failed, body is still too long and can't be split." @@ -960,10 +972,22 @@ def _maybe_split_omitting_optional_parens( yield result -def _prefer_split_rhs_oop(rhs_oop: RHSResult, mode: Mode) -> bool: +def _prefer_split_rhs_oop_over_rhs( + rhs_oop: RHSResult, rhs: RHSResult, mode: Mode +) -> bool: """ - Returns whether we should prefer the result from a split omitting optional parens. + Returns whether we should prefer the result from a split omitting optional parens + (rhs_oop) over the original (rhs). """ + # If we have multiple targets, we prefer more `=`s on the head vs pushing them to + # the body + rhs_head_equal_count = [leaf.type for leaf in rhs.head.leaves].count(token.EQUAL) + rhs_oop_head_equal_count = [leaf.type for leaf in rhs_oop.head.leaves].count( + token.EQUAL + ) + if rhs_head_equal_count > 1 and rhs_head_equal_count > rhs_oop_head_equal_count: + return False + has_closing_bracket_after_assign = False for leaf in reversed(rhs_oop.head.leaves): if leaf.type == token.EQUAL: diff --git a/src/black/lines.py b/src/black/lines.py index ec6145ff848..6e33ee57eab 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -209,6 +209,11 @@ def is_triple_quoted_string(self) -> bool: return True return False + @property + def is_chained_assignment(self) -> bool: + """Is the line a chained assignment""" + return [leaf.type for leaf in self.leaves].count(token.EQUAL) > 1 + @property def opens_block(self) -> bool: """Does this line open a new level of indentation.""" diff --git a/tests/data/cases/preview_prefer_rhs_split.py b/tests/data/cases/preview_prefer_rhs_split.py index c732c33b53a..28d89c368c0 100644 --- a/tests/data/cases/preview_prefer_rhs_split.py +++ b/tests/data/cases/preview_prefer_rhs_split.py @@ -84,3 +84,24 @@ ) or ( isinstance(some_other_var, BaseClass) and table.something != table.some_other_thing ) + +# Multiple targets +a = b = ( + ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +) + +a = b = c = d = e = f = g = ( + hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh +) = i = j = ( + kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +) + +a = ( + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) = c + +a = ( + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) = ( + cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +) = ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd From 69d49c5a6fe064eb290dd6445745fdeb2643f54f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 24 Nov 2023 14:19:54 +0000 Subject: [PATCH 17/33] Bump mypy to 1.7.1 (#4069) --- .pre-commit-config.yaml | 2 +- CHANGES.md | 2 +- pyproject.toml | 4 ++-- src/blib2to3/pgen2/tokenize.py | 7 ++----- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c153746b621..2896489d724 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: exclude: ^src/blib2to3/ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.1 hooks: - id: mypy exclude: ^docs/conf.py diff --git a/CHANGES.md b/CHANGES.md index 6a8b97c75eb..f6fe69bac68 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,7 +26,7 @@ ### Packaging -- Upgrade to mypy 1.6.1 (#4049) +- Upgrade to mypy 1.7.1 (#4049) (#4069) ### Parser diff --git a/pyproject.toml b/pyproject.toml index e63e0aea3ef..6b681e8226a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,7 @@ macos-max-compat = true enable-by-default = false dependencies = [ "hatch-mypyc>=0.16.0", - "mypy==1.6.1", + "mypy==1.7.1", "click==8.1.3", # avoid https://github.com/pallets/click/issues/2558 ] require-runtime-dependencies = true @@ -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.6.1' 'click==8.1.3'", + "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'", """sed -i '' -e "600,700s/'10_16'/os.environ['MACOSX_DEPLOYMENT_TARGET'].replace('.', '_')/" $(python -c 'import hatchling.builders.wheel as h; print(h.__file__)') """, ] diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index d0607f4b1e1..b04b18ba870 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -39,7 +39,6 @@ Set, Tuple, Union, - cast, ) from blib2to3.pgen2.grammar import Grammar @@ -262,11 +261,9 @@ def add_whitespace(self, start: Coord) -> None: def untokenize(self, iterable: Iterable[TokenInfo]) -> str: for t in iterable: if len(t) == 2: - self.compat(cast(Tuple[int, str], t), iterable) + self.compat(t, iterable) break - tok_type, token, start, end, line = cast( - Tuple[int, str, Coord, Coord, str], t - ) + tok_type, token, start, end, line = t self.add_whitespace(start) self.tokens.append(token) self.prev_row, self.prev_col = end From a0e270d0f246387202e676b25abbf7a02ddcbc71 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 24 Nov 2023 18:05:59 +0000 Subject: [PATCH 18/33] Build mypycified wheels for Python 3.12 (#4070) --- .github/workflows/pypi_upload.yml | 2 +- CHANGES.md | 1 + pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 07273f09508..bbdcdf17a8f 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -73,7 +73,7 @@ jobs: | pyp 'json.dumps({"only": x, "os": "ubuntu-latest"})' } | pyp 'json.dumps(list(map(json.loads, lines)))' > /tmp/matrix env: - CIBW_BUILD: "cp38-* cp311-*" + CIBW_BUILD: "cp38-* cp312-*" CIBW_ARCHS_LINUX: x86_64 - id: set-matrix run: echo "include=$(cat /tmp/matrix)" | tee -a $GITHUB_OUTPUT diff --git a/CHANGES.md b/CHANGES.md index f6fe69bac68..e9ffd6bb9f5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,7 @@ ### Packaging - Upgrade to mypy 1.7.1 (#4049) (#4069) +- Faster compiled wheels are now available for CPython 3.12 (#4070) ### Parser diff --git a/pyproject.toml b/pyproject.toml index 6b681e8226a..1098412981a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ build-verbosity = 1 # - Architecture (64-bit only): amd64 / x86_64, universal2, and arm64 # - OS: Linux (no musl), Windows, and macOS build = "cp3*" -skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp*", "cp312-*"] +skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp*"] # This is the bare minimum needed to run the test suite. Pulling in the full # test_requirements.txt would download a bunch of other packages not necessary # here and would slow down the testing step a fair bit. From 66ec056e39da957d3c82da5b7a86ef228606cfe6 Mon Sep 17 00:00:00 2001 From: exag Date: Mon, 4 Dec 2023 14:47:30 +0900 Subject: [PATCH 19/33] Fix minor typos in docstrings (#4085) --- src/black/numerics.py | 2 +- src/black/ranges.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/black/numerics.py b/src/black/numerics.py index 67ac8595fcc..3040de06fde 100644 --- a/src/black/numerics.py +++ b/src/black/numerics.py @@ -14,7 +14,7 @@ def format_hex(text: str) -> str: def format_scientific_notation(text: str) -> str: - """Formats a numeric string utilizing scentific notation""" + """Formats a numeric string utilizing scientific notation""" before, after = text.split("e") sign = "" if after.startswith("-"): diff --git a/src/black/ranges.py b/src/black/ranges.py index b0c312e6274..59e19242d47 100644 --- a/src/black/ranges.py +++ b/src/black/ranges.py @@ -172,7 +172,7 @@ class _TopLevelStatementsVisitor(Visitor[None]): A node visitor that converts unchanged top-level statements to STANDALONE_COMMENT. - This is used in addition to _convert_unchanged_lines_by_flatterning, to + This is used in addition to _convert_unchanged_line_by_line, to speed up formatting when there are unchanged top-level classes/functions/statements. """ @@ -302,7 +302,7 @@ def _convert_node_to_standalone_comment(node: LN) -> None: index = node.remove() if index is not None: # Remove the '\n', as STANDALONE_COMMENT will have '\n' appended when - # genearting the formatted code. + # generating the formatted code. value = str(node)[:-1] parent.insert_child( index, From 3416b2c82d51f27ce55c31ef0bfe4a9e21816611 Mon Sep 17 00:00:00 2001 From: Riyazuddin Khan Date: Mon, 4 Dec 2023 23:40:03 +0530 Subject: [PATCH 20/33] Fix: --line-ranges dedents a # fmt: off in the middle of a decorator (#4084) Fixes #4068 --- CHANGES.md | 3 ++- src/black/__init__.py | 2 +- src/black/comments.py | 23 ++++++++++++++---- .../cases/line_ranges_fmt_off_decorator.py | 24 +++++++++++++++++-- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e9ffd6bb9f5..f17cd7fdc9d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,8 @@ ### Stable style - +- 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) ### Preview style diff --git a/src/black/__init__.py b/src/black/__init__.py index b33beeeeb23..04f6d8c58de 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1180,7 +1180,7 @@ def _format_str_once( for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS} if supports_feature(versions, feature) } - normalize_fmt_off(src_node, mode) + normalize_fmt_off(src_node, mode, lines) if lines: # This should be called after normalize_fmt_off. convert_unchanged_lines(src_node, lines) diff --git a/src/black/comments.py b/src/black/comments.py index 8a0e925fdc0..25413121199 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -1,7 +1,7 @@ import re from dataclasses import dataclass from functools import lru_cache -from typing import Final, Iterator, List, Optional, Union +from typing import Collection, Final, Iterator, List, Optional, Tuple, Union from black.mode import Mode, Preview from black.nodes import ( @@ -161,14 +161,18 @@ def make_comment(content: str) -> str: return "#" + content -def normalize_fmt_off(node: Node, mode: Mode) -> None: +def normalize_fmt_off( + node: Node, mode: Mode, lines: Collection[Tuple[int, int]] +) -> None: """Convert content between `# fmt: off`/`# fmt: on` into standalone comments.""" try_again = True while try_again: - try_again = convert_one_fmt_off_pair(node, mode) + try_again = convert_one_fmt_off_pair(node, mode, lines) -def convert_one_fmt_off_pair(node: Node, mode: Mode) -> bool: +def convert_one_fmt_off_pair( + node: Node, mode: Mode, lines: Collection[Tuple[int, int]] +) -> bool: """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. Returns True if a pair was converted. @@ -213,7 +217,18 @@ def convert_one_fmt_off_pair(node: Node, mode: Mode) -> bool: prefix[:previous_consumed] + "\n" * comment.newlines ) hidden_value = "".join(str(n) for n in ignored_nodes) + comment_lineno = leaf.lineno - comment.newlines 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 + ): + # keeping indentation of comment by preserving original whitespaces. + fmt_off_prefix = prefix.split(comment.value)[0] + if "\n" in fmt_off_prefix: + fmt_off_prefix = fmt_off_prefix.split("\n")[-1] + standalone_comment_prefix += fmt_off_prefix hidden_value = comment.value + "\n" + hidden_value if _contains_fmt_skip_comment(comment.value, mode): hidden_value += " " + comment.value diff --git a/tests/data/cases/line_ranges_fmt_off_decorator.py b/tests/data/cases/line_ranges_fmt_off_decorator.py index 14aa1dda02d..065bf4328d7 100644 --- a/tests/data/cases/line_ranges_fmt_off_decorator.py +++ b/tests/data/cases/line_ranges_fmt_off_decorator.py @@ -1,4 +1,4 @@ -# flags: --line-ranges=12-12 +# flags: --line-ranges=12-12 --line-ranges=21-21 # NOTE: If you need to modify this file, pay special attention to the --line-ranges= # flag above as it's formatting specifically these lines. @@ -11,9 +11,19 @@ class MyClass: def method(): print ( "str" ) + @decor( + a=1, + # fmt: off + b=(2, 3), + # fmt: on + ) + def func(): + pass + + # output -# flags: --line-ranges=12-12 +# flags: --line-ranges=12-12 --line-ranges=21-21 # NOTE: If you need to modify this file, pay special attention to the --line-ranges= # flag above as it's formatting specifically these lines. @@ -25,3 +35,13 @@ class MyClass: # fmt: on def method(): print("str") + + @decor( + a=1, + # fmt: off + b=(2, 3), + # fmt: on + ) + def func(): + pass + From 50d5756e8e63b17e4523f096f312011273ce640f Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:19:24 +0100 Subject: [PATCH 21/33] fix crash in preview mode with --line-length=1 (#4086) --- CHANGES.md | 1 + src/black/linegen.py | 2 +- .../return_annotation_brackets_crash_line_length_1.py | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/data/cases/return_annotation_brackets_crash_line_length_1.py diff --git a/CHANGES.md b/CHANGES.md index f17cd7fdc9d..8f0b75e7f10 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ - Standalone form feed characters at the module level are no longer removed (#4021) - Additional cases of immediately nested tuples, lists, and dictionaries are now indented less (#4012) +- Fix crash in preview mode when using a short `--line-length` (#4086) ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index 7152568783e..073672a5ae7 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -744,7 +744,7 @@ def left_hand_split( if leaf.type in OPENING_BRACKETS: matching_bracket = leaf current_leaves = body_leaves - if not matching_bracket: + if not matching_bracket or not tail_leaves: raise CannotSplit("No brackets found") head = bracket_split_build_line( diff --git a/tests/data/cases/return_annotation_brackets_crash_line_length_1.py b/tests/data/cases/return_annotation_brackets_crash_line_length_1.py new file mode 100644 index 00000000000..9d96b4ab97a --- /dev/null +++ b/tests/data/cases/return_annotation_brackets_crash_line_length_1.py @@ -0,0 +1,9 @@ +# flags: --preview --minimum-version=3.10 --line-length=1 + +def foo() -> tuple[int, int,]: + ... +# output +def foo() -> tuple[ + int, + int, +]: ... From e4ae213f06050e7f76ebcf01578c002e412dafdc Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:17:33 +0100 Subject: [PATCH 22/33] test preview cases with line-length 1 unless explicitly skipped (#4087) * Add new flag for tests, --no-preview-line-length-1, to be used for test cases known to not work in preview mode with line-length=1. Also split out the problematic cases in three cases to separate files. Removed now redundant file which explicitly tested preview annotations with line-length=1 * mode.preview -> preview_mode, mark pep_572_remove_parens as failing with ll1 --- tests/data/cases/comment_type_hint.py | 3 + tests/data/cases/comments2.py | 4 - tests/data/cases/fmtskip2.py | 5 +- tests/data/cases/pep_572_remove_parens.py | 2 +- ..._parens_with_braces_and_square_brackets.py | 96 ---------------- ..._with_braces_and_square_brackets_no_ll1.py | 106 ++++++++++++++++++ ...annotation_brackets_crash_line_length_1.py | 9 -- tests/test_format.py | 2 + tests/util.py | 54 ++++++--- 9 files changed, 154 insertions(+), 127 deletions(-) create mode 100644 tests/data/cases/comment_type_hint.py create mode 100644 tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py delete mode 100644 tests/data/cases/return_annotation_brackets_crash_line_length_1.py diff --git a/tests/data/cases/comment_type_hint.py b/tests/data/cases/comment_type_hint.py new file mode 100644 index 00000000000..2992da88d90 --- /dev/null +++ b/tests/data/cases/comment_type_hint.py @@ -0,0 +1,3 @@ +# flags: --no-preview-line-length-1 +# split out from comments2 as it does not work with line-length=1, losing the comment +a = "type comment with trailing space" # type: str diff --git a/tests/data/cases/comments2.py b/tests/data/cases/comments2.py index 1487dc4b6e2..261c5e9f0a0 100644 --- a/tests/data/cases/comments2.py +++ b/tests/data/cases/comments2.py @@ -155,8 +155,6 @@ def _init_host(self, parsed) -> None: pass -a = "type comment with trailing space" # type: str - ####################### ### SECTION COMMENT ### ####################### @@ -335,8 +333,6 @@ def _init_host(self, parsed) -> None: pass -a = "type comment with trailing space" # type: str - ####################### ### SECTION COMMENT ### ####################### diff --git a/tests/data/cases/fmtskip2.py b/tests/data/cases/fmtskip2.py index e6248117aa9..0189d4e642d 100644 --- a/tests/data/cases/fmtskip2.py +++ b/tests/data/cases/fmtskip2.py @@ -1,9 +1,12 @@ +# flags: --no-preview-line-length-1 +# l2 loses the comment with line-length=1 in preview mode l1 = ["This list should be broken up", "into multiple lines", "because it is way too long"] l2 = ["But this list shouldn't", "even though it also has", "way too many characters in it"] # fmt: skip l3 = ["I have", "trailing comma", "so I should be braked",] # output +# l2 loses the comment with line-length=1 in preview mode l1 = [ "This list should be broken up", "into multiple lines", @@ -14,4 +17,4 @@ "I have", "trailing comma", "so I should be braked", -] \ No newline at end of file +] diff --git a/tests/data/cases/pep_572_remove_parens.py b/tests/data/cases/pep_572_remove_parens.py index 24f1ac29168..88774d81649 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 +# flags: --minimum-version=3.8 --no-preview-line-length-1 if (foo := 0): pass diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 9e5c9eb8546..47a6a0bcae6 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -125,23 +125,6 @@ def foo_square_brackets(request): func([x for x in "long line long line long line long line long line long line long line"]) func([x for x in [x for x in "long line long line long line long line long line long line long line"]]) -func({"short line"}) -func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) -func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) -func(("long line", "long long line", "long long long line", "long long long long line", "long long long long long line")) -func((("long line", "long long line", "long long long line", "long long long long line", "long long long long long line"))) -func([["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]) - -# Do not hug if the argument fits on a single line. -func({"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}) -func(("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")) -func(["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]) -func(**{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"}) -func(*("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----")) -array = [{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}] -array = [("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")] -array = [["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]] - foooooooooooooooooooo( [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} ) @@ -151,14 +134,11 @@ def foo_square_brackets(request): ) nested_mapping = {"key": [{"a very long key 1": "with a very long value", "a very long key 2": "with a very long value"}]} -nested_array = [[["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]] explicit_exploding = [[["short", "line",],],] single_item_do_not_explode = Context({ "version": get_docs_version(), }) -foo(*["long long long long long line", "long long long long long line", "long long long long long line"]) - foo(*[str(i) for i in range(100000000000000000000000000000000000000000000000000000000000)]) foo( @@ -310,69 +290,6 @@ def foo_square_brackets(request): ] ]) -func({"short line"}) -func({ - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -}) -func({{ - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -}}) -func(( - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -)) -func((( - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -))) -func([[ - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -]]) - -# Do not hug if the argument fits on a single line. -func( - {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} -) -func( - ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") -) -func( - ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] -) -func( - **{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"} -) -func( - *("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----") -) -array = [ - {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} -] -array = [ - ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") -] -array = [ - ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] -] - foooooooooooooooooooo( [{c: n + 1 for c in range(256)} for n in range(100)] + [{}], {size} ) @@ -387,13 +304,6 @@ def foo_square_brackets(request): "a very long key 2": "with a very long value", }] } -nested_array = [[[ - "long line", - "long long line", - "long long long line", - "long long long long line", - "long long long long long line", -]]] explicit_exploding = [ [ [ @@ -406,12 +316,6 @@ def foo_square_brackets(request): "version": get_docs_version(), }) -foo(*[ - "long long long long long line", - "long long long long long line", - "long long long long long line", -]) - foo(*[ str(i) for i in range(100000000000000000000000000000000000000000000000000000000000) ]) diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py new file mode 100644 index 00000000000..fdebdf69c20 --- /dev/null +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py @@ -0,0 +1,106 @@ +# flags: --preview --no-preview-line-length-1 +# split out from preview_hug_parens_with_brackes_and_square_brackets, as it produces +# different code on the second pass with line-length 1 in many cases. +# Seems to be about whether the last string in a sequence gets wrapped in parens or not. +foo(*["long long long long long line", "long long long long long line", "long long long long long line"]) +func({"short line"}) +func({"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}) +func({{"long line", "long long line", "long long long line", "long long long long line", "long long long long long line"}}) +func(("long line", "long long line", "long long long line", "long long long long line", "long long long long long line")) +func((("long line", "long long line", "long long long line", "long long long long line", "long long long long long line"))) +func([["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]) + + +# Do not hug if the argument fits on a single line. +func({"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}) +func(("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")) +func(["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]) +func(**{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"}) +func(*("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----")) +array = [{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"}] +array = [("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line")] +array = [["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"]] + +nested_array = [[["long line", "long long line", "long long long line", "long long long long line", "long long long long long line"]]] + +# output + +# split out from preview_hug_parens_with_brackes_and_square_brackets, as it produces +# different code on the second pass with line-length 1 in many cases. +# Seems to be about whether the last string in a sequence gets wrapped in parens or not. +foo(*[ + "long long long long long line", + "long long long long long line", + "long long long long long line", +]) +func({"short line"}) +func({ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}) +func({{ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +}}) +func(( + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +)) +func((( + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +))) +func([[ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +]]) + + +# Do not hug if the argument fits on a single line. +func( + {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} +) +func( + ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") +) +func( + ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] +) +func( + **{"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit---"} +) +func( + *("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit----") +) +array = [ + {"fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"} +] +array = [ + ("fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line") +] +array = [ + ["fit line", "fit line", "fit line", "fit line", "fit line", "fit line", "fit line"] +] + +nested_array = [[[ + "long line", + "long long line", + "long long long line", + "long long long long line", + "long long long long long line", +]]] diff --git a/tests/data/cases/return_annotation_brackets_crash_line_length_1.py b/tests/data/cases/return_annotation_brackets_crash_line_length_1.py deleted file mode 100644 index 9d96b4ab97a..00000000000 --- a/tests/data/cases/return_annotation_brackets_crash_line_length_1.py +++ /dev/null @@ -1,9 +0,0 @@ -# flags: --preview --minimum-version=3.10 --line-length=1 - -def foo() -> tuple[int, int,]: - ... -# output -def foo() -> tuple[ - int, - int, -]: ... diff --git a/tests/test_format.py b/tests/test_format.py index 6c2eca8c618..9162c585c08 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -30,6 +30,7 @@ def check_file(subdir: str, filename: str, *, data: bool = True) -> None: fast=args.fast, minimum_version=args.minimum_version, lines=args.lines, + no_preview_line_length_1=args.no_preview_line_length_1, ) if args.minimum_version is not None: major, minor = args.minimum_version @@ -42,6 +43,7 @@ def check_file(subdir: str, filename: str, *, data: bool = True) -> None: fast=args.fast, minimum_version=args.minimum_version, lines=args.lines, + no_preview_line_length_1=args.no_preview_line_length_1, ) diff --git a/tests/util.py b/tests/util.py index c8699d335ab..9ea30e62fe3 100644 --- a/tests/util.py +++ b/tests/util.py @@ -46,6 +46,7 @@ class TestCaseArgs: fast: bool = False minimum_version: Optional[Tuple[int, int]] = None lines: Collection[Tuple[int, int]] = () + no_preview_line_length_1: bool = False def _assert_format_equal(expected: str, actual: str) -> None: @@ -96,6 +97,7 @@ def assert_format( fast: bool = False, minimum_version: Optional[Tuple[int, int]] = None, lines: Collection[Tuple[int, int]] = (), + no_preview_line_length_1: bool = False, ) -> None: """Convenience function to check that Black formats as expected. @@ -124,21 +126,28 @@ def assert_format( f"Black crashed formatting this case in {text} mode." ) from e # Similarly, setting line length to 1 is a good way to catch - # stability bugs. But only in non-preview mode because preview mode - # currently has a lot of line length 1 bugs. - try: - _assert_format_inner( - source, - None, - replace(mode, preview=False, line_length=1), - fast=fast, - minimum_version=minimum_version, - lines=lines, - ) - except Exception as e: - raise FormatFailure( - "Black crashed formatting this case with line-length set to 1." - ) from e + # stability bugs. Some tests are known to be broken in preview mode with line length + # of 1 though, and have marked that with a flag --no-preview-line-length-1 + preview_modes = [False] + if not no_preview_line_length_1: + preview_modes.append(True) + + for preview_mode in preview_modes: + + try: + _assert_format_inner( + source, + None, + replace(mode, preview=preview_mode, line_length=1), + fast=fast, + minimum_version=minimum_version, + lines=lines, + ) + except Exception as e: + text = "preview" if preview_mode else "non-preview" + raise FormatFailure( + f"Black crashed formatting this case in {text} mode with line-length=1." + ) from e def _assert_format_inner( @@ -246,6 +255,15 @@ def get_flags_parser() -> argparse.ArgumentParser: ), ) parser.add_argument("--line-ranges", action="append") + parser.add_argument( + "--no-preview-line-length-1", + default=False, + action="store_true", + help=( + "Don't run in preview mode with --line-length=1, as that's known to cause a" + " crash" + ), + ) return parser @@ -266,7 +284,11 @@ def parse_mode(flags_line: str) -> TestCaseArgs: else: lines = [] return TestCaseArgs( - mode=mode, fast=args.fast, minimum_version=args.minimum_version, lines=lines + mode=mode, + fast=args.fast, + minimum_version=args.minimum_version, + lines=lines, + no_preview_line_length_1=args.no_preview_line_length_1, ) From 50e287cecea41ee32bd66ab1eee4827f6b8312ce Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:38:57 -0600 Subject: [PATCH 23/33] docs: Clarify include/exclude documentation (#4072) --- docs/usage_and_configuration/the_basics.md | 22 +++++++-------- src/black/__init__.py | 33 +++++++++++----------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 0c1a4d3b5a1..3739bcaefa1 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -241,24 +241,17 @@ Because of our [stability policy](../the_black_code_style/index.md), this will g stable formatting, but still allow you to take advantage of improvements that do not affect formatting. -#### `--include` - -A regular expression that matches files and directories that should be included on -recursive searches. An empty value means all files are included regardless of the name. -Use forward slashes for directories on all platforms (Windows, too). Exclusions are -calculated first, inclusions later. - #### `--exclude` A regular expression that matches files and directories that should be excluded on recursive searches. An empty value means no paths are excluded. Use forward slashes for -directories on all platforms (Windows, too). Exclusions are calculated first, inclusions -later. +directories on all platforms (Windows, too). By default, Black also ignores all paths +listed in `.gitignore`. Changing this value will override all default exclusions. #### `--extend-exclude` -Like `--exclude`, but adds additional files and directories on top of the excluded ones. -Useful if you simply want to add to the default. +Like `--exclude`, but adds additional files and directories on top of the default values +instead of overriding them. #### `--force-exclude` @@ -271,6 +264,13 @@ programmatically on changed files, such as in a pre-commit hook or editor plugin The name of the file when passing it through stdin. Useful to make sure Black will respect the `--force-exclude` option on some editors that rely on using stdin. +#### `--include` + +A regular expression that matches files and directories that should be included on +recursive searches. An empty value means all files are included regardless of the name. +Use forward slashes for directories on all platforms (Windows, too). Overrides all +exclusions, including from `.gitignore` and command line options. + #### `-W`, `--workers` When _Black_ formats multiple files, it may use a process pool to speed up formatting. diff --git a/src/black/__init__.py b/src/black/__init__.py index 04f6d8c58de..e7dac895a6a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -344,19 +344,6 @@ def validate_regex( " either a major version number or an exact version." ), ) -@click.option( - "--include", - type=str, - default=DEFAULT_INCLUDES, - callback=validate_regex, - help=( - "A regular expression that matches files and directories that should be" - " included on recursive searches. An empty value means all files are included" - " regardless of the name. Use forward slashes for directories on all platforms" - " (Windows, too). Exclusions are calculated first, inclusions later." - ), - show_default=True, -) @click.option( "--exclude", type=str, @@ -365,8 +352,8 @@ def validate_regex( "A regular expression that matches files and directories that should be" " excluded on recursive searches. An empty value means no paths are excluded." " Use forward slashes for directories on all platforms (Windows, too)." - " Exclusions are calculated first, inclusions later. [default:" - f" {DEFAULT_EXCLUDES}]" + " By default, Black also ignores all paths listed in .gitignore. Changing this" + f" value will override all default exclusions. [default: {DEFAULT_EXCLUDES}]" ), show_default=False, ) @@ -376,7 +363,7 @@ def validate_regex( callback=validate_regex, help=( "Like --exclude, but adds additional files and directories on top of the" - " excluded ones. (Useful if you simply want to add to the default)" + " default values instead of overriding them." ), ) @click.option( @@ -398,6 +385,20 @@ def validate_regex( "editors that rely on using stdin." ), ) +@click.option( + "--include", + type=str, + default=DEFAULT_INCLUDES, + callback=validate_regex, + help=( + "A regular expression that matches files and directories that should be" + " included on recursive searches. An empty value means all files are included" + " regardless of the name. Use forward slashes for directories on all platforms" + " (Windows, too). Overrides all exclusions, including from .gitignore and" + " command line options." + ), + show_default=True, +) @click.option( "-W", "--workers", From 432d9050c3d1e35a36ffc97d4a9e0e0c9e5e4ecc Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:32:06 -0600 Subject: [PATCH 24/33] docs: Unify option descriptions between `--help` and `the_basics.md` (#4076) --- docs/usage_and_configuration/the_basics.md | 45 +++++++------ src/black/__init__.py | 75 +++++++++++++--------- 2 files changed, 69 insertions(+), 51 deletions(-) diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 3739bcaefa1..73c0d1323e3 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -35,6 +35,10 @@ are deliberately limited and rarely added. Note that all command-line options listed above can also be configured using a `pyproject.toml` file (more on that below). +#### `-h`, `--help` + +Show available command-line options and exit. + #### `-c`, `--code` Format the code passed in as a string. @@ -109,6 +113,10 @@ useful when piping source on standard input. When processing Jupyter Notebooks, add the given magic to the list of known python- magics. Useful for formatting cells with custom python magics. +#### `-x, --skip-source-first-line` + +Skip the first line of the source code. + #### `-S, --skip-string-normalization` By default, _Black_ uses double quotes for all strings and normalizes string prefixes, @@ -132,7 +140,7 @@ functionality in the next major release. Read more about #### `--check` -Passing `--check` will make _Black_ exit with: +Don't write the files back, just return the status. _Black_ will exit with: - code 0 if nothing would change; - code 1 if some files would be reformatted; or @@ -162,8 +170,8 @@ $ echo $? #### `--diff` -Passing `--diff` will make _Black_ print out diffs that indicate what changes _Black_ -would've made. They are printed to stdout so capturing them is simple. +Don't write the files back, just output a diff to indicate what changes _Black_ would've +made. They are printed to stdout so capturing them is simple. If you'd like colored diffs, you can enable them with `--color`. @@ -179,6 +187,10 @@ All done! ✨ 🍰 ✨ 1 file would be reformatted. ``` +#### `--color` / `--no-color` + +Show (or do not show) colored diff. Only applies when `--diff` is given. + ### `--line-ranges` When specified, _Black_ will try its best to only format these lines. @@ -202,10 +214,6 @@ extra lines outside of the ranges when ther are unformatted lines with the exact content. It also disables _Black_'s formatting stability check in `--safe` mode. ``` -#### `--color` / `--no-color` - -Show (or do not show) colored diff. Only applies when `--diff` is given. - #### `--fast` / `--safe` By default, _Black_ performs [an AST safety check](labels/ast-changes) after formatting @@ -256,7 +264,7 @@ instead of overriding them. #### `--force-exclude` Like `--exclude`, but files and directories matching this regex will be excluded even -when they are passed explicitly as arguments. This is useful when invoking _Black_ +when they are passed explicitly as arguments. This is useful when invoking Black programmatically on changed files, such as in a pre-commit hook or editor plugin. #### `--stdin-filename` @@ -275,12 +283,12 @@ exclusions, including from `.gitignore` and command line options. When _Black_ formats multiple files, it may use a process pool to speed up formatting. This option controls the number of parallel workers. This can also be specified via the -`BLACK_NUM_WORKERS` environment variable. +`BLACK_NUM_WORKERS` environment variable. Defaults to the number of CPUs in the system. #### `-q`, `--quiet` -Passing `-q` / `--quiet` will cause _Black_ to stop emitting all non-critical output. -Error messages will still be emitted (which can silenced by `2>/dev/null`). +Stop emitting all non-critical output. Error messages will still be emitted (which can +silenced by `2>/dev/null`). ```console $ black src/ -q @@ -289,9 +297,9 @@ error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio #### `-v`, `--verbose` -Passing `-v` / `--verbose` will cause _Black_ to also emit messages about files that -were not changed or were ignored due to exclusion patterns. If _Black_ is using a -configuration file, a blue message detailing which one it is using will be emitted. +Emit messages about files that were not changed or were ignored due to exclusion +patterns. If _Black_ is using a configuration file, a message detailing which one it is +using will be emitted. ```console $ black src/ -v @@ -321,10 +329,6 @@ black, 23.11.0 Read configuration options from a configuration file. See [below](#configuration-via-a-file) for more details on the configuration file. -#### `-h`, `--help` - -Show available command-line options and exit. - ### Environment variable options _Black_ supports the following configuration via environment variables. @@ -355,7 +359,7 @@ All done! ✨ 🍰 ✨ use `--stdin-filename`. Useful to make sure _Black_ will respect the `--force-exclude` option on some editors that rely on using stdin. -You can also pass code as a string using the `-c` / `--code` option. +You can also pass code as a string using the `--code` option. ### Writeback and reporting @@ -435,8 +439,7 @@ refers to the path to your home directory. On Windows, this will be something li You can also explicitly specify the path to a particular file that you want with `--config`. In this situation _Black_ will not look for any other file. -If you're running with `--verbose`, you will see a blue message if a file was found and -used. +If you're running with `--verbose`, you will see a message if a file was found and used. Please note `blackd` will not use `pyproject.toml` configuration. diff --git a/src/black/__init__.py b/src/black/__init__.py index e7dac895a6a..5073fa748d5 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -235,25 +235,26 @@ def validate_regex( callback=target_version_option_callback, multiple=True, help=( - "Python versions that should be supported by Black's output. By default, Black" - " will try to infer this from the project metadata in pyproject.toml. If this" - " does not yield conclusive results, Black will use per-file auto-detection." + "Python versions that should be supported by Black's output. You should" + " include all versions that your code supports. By default, Black will infer" + " target versions from the project metadata in pyproject.toml. If this does" + " not yield conclusive results, Black will use per-file auto-detection." ), ) @click.option( "--pyi", is_flag=True, help=( - "Format all input files like typing stubs regardless of file extension (useful" - " when piping source on standard input)." + "Format all input files like typing stubs regardless of file extension. This" + " is useful when piping source on standard input." ), ) @click.option( "--ipynb", is_flag=True, help=( - "Format all input files like Jupyter Notebooks regardless of file extension " - "(useful when piping source on standard input)." + "Format all input files like Jupyter Notebooks regardless of file extension." + "This is useful when piping source on standard input." ), ) @click.option( @@ -310,14 +311,22 @@ def validate_regex( @click.option( "--diff", is_flag=True, - help="Don't write the files back, just output a diff for each file on stdout.", + help=( + "Don't write the files back, just output a diff to indicate what changes" + " Black would've made. They are printed to stdout so capturing them is simple." + ), +) +@click.option( + "--color/--no-color", + is_flag=True, + help="Show (or do not show) colored diff. Only applies when --diff is given.", ) @click.option( "--line-ranges", multiple=True, metavar="START-END", help=( - "When specified, _Black_ will try its best to only format these lines. This" + "When specified, Black will try its best to only format these lines. This" " option can be specified multiple times, and a union of the lines will be" " formatted. Each range must be specified as two integers connected by a `-`:" " `-`. The `` and `` integer indices are 1-based and" @@ -325,23 +334,24 @@ def validate_regex( ), default=(), ) -@click.option( - "--color/--no-color", - is_flag=True, - help="Show colored diff. Only applies when `--diff` is given.", -) @click.option( "--fast/--safe", is_flag=True, - help="If --fast given, skip temporary sanity checks. [default: --safe]", + help=( + "By default, Black performs an AST safety check after formatting your code." + " The --fast flag turns off this check and the --safe flag explicitly enables" + " it. [default: --safe]" + ), ) @click.option( "--required-version", type=str, help=( - "Require a specific version of Black to be running (useful for unifying results" - " across many environments e.g. with a pyproject.toml file). It can be" - " either a major version number or an exact version." + "Require a specific version of Black to be running. This is useful for" + " ensuring that all contributors to your project are using the same" + " version, because different versions of Black may format code a little" + " differently. This option can be set in a configuration file for consistent" + " results across environments." ), ) @click.option( @@ -371,8 +381,10 @@ def validate_regex( type=str, callback=validate_regex, help=( - "Like --exclude, but files and directories matching this regex will be " - "excluded even when they are passed explicitly as arguments." + "Like --exclude, but files and directories matching this regex will be excluded" + " even when they are passed explicitly as arguments. This is useful when" + " invoking Black programmatically on changed files, such as in a pre-commit" + " hook or editor plugin." ), ) @click.option( @@ -380,9 +392,9 @@ def validate_regex( type=str, is_eager=True, help=( - "The name of the file when passing it through stdin. Useful to make " - "sure Black will respect --force-exclude option on some " - "editors that rely on using stdin." + "The name of the file when passing it through stdin. Useful to make sure Black" + " will respect the --force-exclude option on some editors that rely on using" + " stdin." ), ) @click.option( @@ -405,8 +417,10 @@ def validate_regex( type=click.IntRange(min=1), default=None, help=( - "Number of parallel workers [default: BLACK_NUM_WORKERS environment variable " - "or number of CPUs in the system]" + "When Black formats multiple files, it may use a process pool to speed up" + " formatting. This option controls the number of parallel workers. This can" + " also be specified via the BLACK_NUM_WORKERS environment variable. Defaults" + " to the number of CPUs in the system." ), ) @click.option( @@ -414,8 +428,8 @@ def validate_regex( "--quiet", is_flag=True, help=( - "Don't emit non-error messages to stderr. Errors are still emitted; silence" - " those with 2>/dev/null." + "Stop emitting all non-critical output. Error messages will still be emitted" + " (which can silenced by 2>/dev/null)." ), ) @click.option( @@ -423,8 +437,9 @@ def validate_regex( "--verbose", is_flag=True, help=( - "Also emit messages to stderr about files that were not changed or were ignored" - " due to exclusion patterns." + "Emit messages about files that were not changed or were ignored due to" + " exclusion patterns. If Black is using a configuration file, a message" + " detailing which one it is using will be emitted." ), ) @click.version_option( @@ -455,7 +470,7 @@ def validate_regex( ), is_eager=True, callback=read_pyproject_toml, - help="Read configuration from FILE path.", + help="Read configuration options from a configuration file.", ) @click.pass_context def main( # noqa: C901 From e7e122e9ff27fc040a6e8ecd92f0e7603c87f92d Mon Sep 17 00:00:00 2001 From: cobalt <61329810+RedGuy12@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:44:15 -0600 Subject: [PATCH 25/33] docs: Move `fmt: off` docs (#4090) --- docs/the_black_code_style/current_style.md | 15 +++------------ docs/usage_and_configuration/the_basics.md | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index 2a5e10162f2..00bd81416dc 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -8,18 +8,9 @@ deliberately limited and rarely added. Previous formatting is taken into account little as possible, with rare exceptions like the magic trailing comma. The coding style used by _Black_ can be viewed as a strict subset of PEP 8. -_Black_ reformats entire files in place. It doesn't reformat lines that contain -`# fmt: skip` or blocks that start with `# fmt: off` and end with `# fmt: on`. -`# fmt: skip` can be mixed with other pragmas/comments either with multiple comments -(e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separated list (e.g. -`# fmt: skip; pylint; noqa`). `# fmt: on/off` must be on the same level of indentation -and in the same block, meaning no unindents beyond the initial indentation level between -them. It also recognizes [YAPF](https://github.com/google/yapf)'s block comments to the -same effect, as a courtesy for straddling code. - -The rest of this document describes the current formatting style. If you're interested -in trying out where the style is heading, see [future style](./future_style.md) and try -running `black --preview`. +This document describes the current formatting style. If you're interested in trying out +where the style is heading, see [future style](./future_style.md) and try running +`black --preview`. ### How _Black_ wraps lines diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 73c0d1323e3..eb92887f64f 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -12,7 +12,8 @@ _Black_ is a well-behaved Unix-style command-line tool: ## Usage -To get started right away with sensible defaults: +_Black_ will reformat entire files in place. To get started right away with sensible +defaults: ```sh black {source_file_or_directory} @@ -24,6 +25,17 @@ You can run _Black_ as a package if running it as a script doesn't work: python -m black {source_file_or_directory} ``` +### Ignoring sections + +Black will not reformat lines that contain `# fmt: skip` or blocks that start with +`# fmt: off` and end with `# fmt: on`. `# fmt: skip` can be mixed with other +pragmas/comments either with multiple comments (e.g. `# fmt: skip # pylint # noqa`) or +as a semicolon separated list (e.g. `# fmt: skip; pylint; noqa`). `# fmt: on/off` must +be on the same level of indentation and in the same block, meaning no unindents beyond +the initial indentation level between them. Black also recognizes +[YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a +courtesy for straddling code. + ### Command line options The CLI options of _Black_ can be displayed by running `black --help`. All options are @@ -191,7 +203,7 @@ All done! ✨ 🍰 ✨ Show (or do not show) colored diff. Only applies when `--diff` is given. -### `--line-ranges` +#### `--line-ranges` When specified, _Black_ will try its best to only format these lines. From 61b529b7d15400309379f36104885a1dfcd2d026 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 18:29:09 -0800 Subject: [PATCH 26/33] Allow empty lines at beginning of blocks (again) (#4060) --- CHANGES.md | 2 ++ src/black/lines.py | 14 +++++------- src/black/mode.py | 2 +- ...s.py => preview_allow_empty_first_line.py} | 22 +++++++++++++++++++ tests/data/cases/preview_form_feeds.py | 1 + 5 files changed, 31 insertions(+), 10 deletions(-) rename tests/data/cases/{preview_allow_empty_first_line_in_special_cases.py => preview_allow_empty_first_line.py} (87%) diff --git a/CHANGES.md b/CHANGES.md index 8f0b75e7f10..fa0d2494f67 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ - Standalone form feed characters at the module level are no longer removed (#4021) - Additional cases of immediately nested tuples, lists, and dictionaries are now indented less (#4012) +- 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) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 6e33ee57eab..4050f819757 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -689,18 +689,14 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: return 0, 1 return before, 1 + # In preview mode, always allow blank lines, except right before a function + # docstring is_empty_first_line_ok = ( - Preview.allow_empty_first_line_before_new_block_or_comment - in current_line.mode + Preview.allow_empty_first_line_in_block in current_line.mode and ( - # If it's a standalone comment - current_line.leaves[0].type == STANDALONE_COMMENT - # If it opens a new block - or current_line.opens_block - # If it's a triple quote comment (but not at the start of a funcdef) + not is_docstring(current_line.leaves[0]) or ( - is_docstring(current_line.leaves[0]) - and self.previous_line + self.previous_line and self.previous_line.leaves[0] and self.previous_line.leaves[0].parent and not is_funcdef(self.previous_line.leaves[0].parent) diff --git a/src/black/mode.py b/src/black/mode.py index 04038f49627..9df19618363 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -191,7 +191,7 @@ class Preview(Enum): accept_raw_docstrings = auto() fix_power_op_line_length = auto() hug_parens_with_braces_and_square_brackets = auto() - allow_empty_first_line_before_new_block_or_comment = auto() + allow_empty_first_line_in_block = auto() single_line_format_skip_with_multiple_comments = auto() long_case_block_line_splitting = auto() allow_form_feeds = auto() diff --git a/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py b/tests/data/cases/preview_allow_empty_first_line.py similarity index 87% rename from tests/data/cases/preview_allow_empty_first_line_in_special_cases.py rename to tests/data/cases/preview_allow_empty_first_line.py index 96c1433c110..3e14fa15250 100644 --- a/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py +++ b/tests/data/cases/preview_allow_empty_first_line.py @@ -51,6 +51,17 @@ def baz(): if x: a = 123 +def quux(): + + new_line = here + + +class Cls: + + def method(self): + + pass + # output def foo(): @@ -104,3 +115,14 @@ def baz(): # OK if x: a = 123 + + +def quux(): + + new_line = here + + +class Cls: + def method(self): + + pass diff --git a/tests/data/cases/preview_form_feeds.py b/tests/data/cases/preview_form_feeds.py index 2d8653a1f04..c236f177a95 100644 --- a/tests/data/cases/preview_form_feeds.py +++ b/tests/data/cases/preview_form_feeds.py @@ -198,6 +198,7 @@ def foo(): # form feeds are prohibited inside blocks, or on a line with nonwhitespace def bar(a=1, b: bool = False): + pass From ce28be2705ab29f184ec4a00aa3d23340630796d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 21:14:25 -0800 Subject: [PATCH 27/33] 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 28/33] 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 29/33] 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 30/33] 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 31/33] 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 32/33] 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 33/33] 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`