Merge branch 'main' into black23
This commit is contained in:
commit
45fd512519
2
.github/workflows/fuzz.yml
vendored
2
.github/workflows/fuzz.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
2
.github/workflows/pypi_upload.yml
vendored
2
.github/workflows/pypi_upload.yml
vendored
@ -59,7 +59,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build wheels via cibuildwheel
|
||||
uses: pypa/cibuildwheel@v2.11.2
|
||||
uses: pypa/cibuildwheel@v2.11.3
|
||||
env:
|
||||
CIBW_ARCHS_MACOS: "${{ matrix.macos_arch }}"
|
||||
|
||||
|
57
.github/workflows/test-311.yml
vendored
57
.github/workflows/test-311.yml
vendored
@ -1,57 +0,0 @@
|
||||
name: Test 3.11 without aiohttp extensions
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "*.md"
|
||||
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "*.md"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
main:
|
||||
# We want to run on external PRs, but not on our own internal PRs as they'll be run
|
||||
# by the push to the branch. Without this if check, checks are duplicated since
|
||||
# internal PRs match both the push and pull_request events.
|
||||
if:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name !=
|
||||
github.repository
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11.0-rc - 3.11"]
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade tox
|
||||
|
||||
- name: Run tests via tox
|
||||
run: |
|
||||
python -m tox -e 311
|
||||
|
||||
- name: Format ourselves
|
||||
run: |
|
||||
python -m pip install .
|
||||
python -m black --check src/
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.7", "pypy-3.8"]
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
|
@ -38,7 +38,7 @@ repos:
|
||||
- flake8-simplify
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.971
|
||||
rev: v0.991
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: ^docs/conf.py
|
||||
|
17
CHANGES.md
17
CHANGES.md
@ -10,11 +10,18 @@
|
||||
|
||||
<!-- Changes that affect Black's stable style -->
|
||||
|
||||
- Fix a crash when a colon line is marked between `# fmt: off` and `# fmt: on` (#3439)
|
||||
|
||||
### Preview style
|
||||
|
||||
<!-- Changes that affect Black's preview style -->
|
||||
|
||||
- Fix a crash in preview style with assert + parenthesized string (#3415)
|
||||
- Do not put the closing quotes in a docstring on a separate line, even if the line is
|
||||
too long (#3430)
|
||||
- Long values in dict literals are now wrapped in parentheses; correspondingly
|
||||
unnecessary parentheses around short values in dict literals are now removed; long
|
||||
string lambda values are now wrapped in parentheses (#3440)
|
||||
|
||||
### Configuration
|
||||
|
||||
@ -24,6 +31,9 @@
|
||||
|
||||
<!-- Changes to how Black is packaged, such as dependency requirements -->
|
||||
|
||||
- Upgrade mypyc from `0.971` to `0.991` so mypycified _Black_ can be built on armv7
|
||||
(#3380)
|
||||
|
||||
### Parser
|
||||
|
||||
<!-- Changes to the parser or to version autodetection -->
|
||||
@ -36,6 +46,9 @@
|
||||
|
||||
<!-- Changes to Black's terminal output and error messages -->
|
||||
|
||||
- Verbose logging now shows the values of `pyproject.toml` configuration variables
|
||||
(#3392)
|
||||
|
||||
### _Blackd_
|
||||
|
||||
<!-- Changes to blackd -->
|
||||
@ -44,6 +57,8 @@
|
||||
|
||||
<!-- For example, Docker, GitHub Actions, pre-commit, editors -->
|
||||
|
||||
- Move 3.11 CI to normal flow now all dependencies support 3.11 (#3446)
|
||||
|
||||
### Documentation
|
||||
|
||||
<!-- Major changes to documentation and policies. Small docs changes
|
||||
@ -60,6 +75,8 @@
|
||||
present) or as a single newline character (if a newline is present) (#3348)
|
||||
- Implicitly concatenated strings used as function args are now wrapped inside
|
||||
parentheses (#3307)
|
||||
- For assignment statements, prefer splitting the right hand side if the left hand side
|
||||
fits on a single line (#3368)
|
||||
- Correctly handle trailing commas that are inside a line's leading non-nested parens
|
||||
(#3370)
|
||||
|
||||
|
@ -39,7 +39,7 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the
|
||||
### Installation
|
||||
|
||||
_Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run.
|
||||
If you want to format Jupyter Notebooks, install with `pip install 'black[jupyter]'`.
|
||||
If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`.
|
||||
|
||||
If you can't wait for the latest _hotness_ and want to install from GitHub, use:
|
||||
|
||||
|
@ -17,7 +17,7 @@ Also, you can try out _Black_ online for minimal fuss on the
|
||||
## Installation
|
||||
|
||||
_Black_ can be installed by running `pip install black`. It requires Python 3.7+ to run.
|
||||
If you want to format Jupyter Notebooks, install with `pip install 'black[jupyter]'`.
|
||||
If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`.
|
||||
|
||||
If you can't wait for the latest _hotness_ and want to install from GitHub, use:
|
||||
|
||||
|
@ -6,4 +6,4 @@ Sphinx==5.3.0
|
||||
docutils==0.19
|
||||
sphinxcontrib-programoutput==0.17
|
||||
sphinx_copybutton==0.5.1
|
||||
furo==2022.9.29
|
||||
furo==2022.12.7
|
||||
|
@ -119,7 +119,7 @@ sources = ["src"]
|
||||
enable-by-default = false
|
||||
dependencies = [
|
||||
"hatch-mypyc>=0.13.0",
|
||||
"mypy==0.971",
|
||||
"mypy==0.991",
|
||||
# Required stubs to be removed when the packages support PEP 561 themselves
|
||||
"types-typed-ast>=1.4.2",
|
||||
]
|
||||
|
@ -511,6 +511,9 @@ def main( # noqa: C901
|
||||
out("Using configuration from project root.", fg="blue")
|
||||
else:
|
||||
out(f"Using configuration in '{config}'.", fg="blue")
|
||||
if ctx.default_map:
|
||||
for param, value in ctx.default_map.items():
|
||||
out(f"{param}: {value}")
|
||||
|
||||
error_msg = "Oh no! 💥 💔 💥"
|
||||
if (
|
||||
|
@ -226,7 +226,7 @@ def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]:
|
||||
|
||||
# fix for fmt: on in children
|
||||
if children_contains_fmt_on(container):
|
||||
for child in container.children:
|
||||
for index, child in enumerate(container.children):
|
||||
if isinstance(child, Leaf) and is_fmt_on(child):
|
||||
if child.type in CLOSING_BRACKETS:
|
||||
# This means `# fmt: on` is placed at a different bracket level
|
||||
@ -235,6 +235,16 @@ def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]:
|
||||
# The alternative is to fail the formatting.
|
||||
yield child
|
||||
return
|
||||
if (
|
||||
child.type == token.INDENT
|
||||
and index < len(container.children) - 1
|
||||
and children_contains_fmt_on(
|
||||
container.children[index + 1]
|
||||
)
|
||||
):
|
||||
# This means `# fmt: on` is placed right after an indentation
|
||||
# level, and we shouldn't swallow the previous INDENT token.
|
||||
return
|
||||
if children_contains_fmt_on(child):
|
||||
return
|
||||
yield child
|
||||
|
@ -64,7 +64,7 @@ def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool:
|
||||
if verbose or not quiet:
|
||||
msg = (
|
||||
"Skipping .ipynb files as Jupyter dependencies are not installed.\n"
|
||||
"You can fix this by running ``pip install black[jupyter]``"
|
||||
'You can fix this by running ``pip install "black[jupyter]"``'
|
||||
)
|
||||
out(msg)
|
||||
return False
|
||||
|
@ -2,6 +2,7 @@
|
||||
Generating lines of code.
|
||||
"""
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from functools import partial, wraps
|
||||
from typing import Collection, Iterator, List, Optional, Set, Union, cast
|
||||
@ -24,6 +25,7 @@
|
||||
from black.mode import Feature, Mode, Preview
|
||||
from black.nodes import (
|
||||
ASSIGNMENTS,
|
||||
BRACKETS,
|
||||
CLOSING_BRACKETS,
|
||||
OPENING_BRACKETS,
|
||||
RARROW,
|
||||
@ -179,6 +181,23 @@ def visit_stmt(
|
||||
|
||||
yield from self.visit(child)
|
||||
|
||||
def visit_dictsetmaker(self, node: Node) -> Iterator[Line]:
|
||||
if Preview.wrap_long_dict_values_in_parens in self.mode:
|
||||
for i, child in enumerate(node.children):
|
||||
if i == 0:
|
||||
continue
|
||||
if node.children[i - 1].type == token.COLON:
|
||||
if child.type == syms.atom and child.children[0].type == token.LPAR:
|
||||
if maybe_make_parens_invisible_in_atom(
|
||||
child,
|
||||
parent=node,
|
||||
remove_brackets_around_comma=False,
|
||||
):
|
||||
wrap_in_parentheses(node, child, visible=False)
|
||||
else:
|
||||
wrap_in_parentheses(node, child, visible=False)
|
||||
yield from self.visit_default(node)
|
||||
|
||||
def visit_funcdef(self, node: Node) -> Iterator[Line]:
|
||||
"""Visit function definition."""
|
||||
if Preview.annotation_parens not in self.mode:
|
||||
@ -381,19 +400,18 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
|
||||
# We need to find the length of the last line of the docstring
|
||||
# to find if we can add the closing quotes to the line without
|
||||
# exceeding the maximum line length.
|
||||
# If docstring is one line, then we need to add the length
|
||||
# of the indent, prefix, and starting quotes. Ending quotes are
|
||||
# handled later.
|
||||
# If docstring is one line, we don't put the closing quotes on a
|
||||
# separate line because it looks ugly (#3320).
|
||||
lines = docstring.splitlines()
|
||||
last_line_length = len(lines[-1]) if docstring else 0
|
||||
|
||||
if len(lines) == 1:
|
||||
last_line_length += len(indent) + len(prefix) + quote_len
|
||||
|
||||
# If adding closing quotes would cause the last line to exceed
|
||||
# the maximum line length then put a line break before the
|
||||
# closing quotes
|
||||
if last_line_length + quote_len > self.mode.line_length:
|
||||
if (
|
||||
len(lines) > 1
|
||||
and last_line_length + quote_len > self.mode.line_length
|
||||
):
|
||||
leaf.value = prefix + quote + docstring + "\n" + indent + quote
|
||||
else:
|
||||
leaf.value = prefix + quote + docstring + quote
|
||||
@ -604,6 +622,17 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator
|
||||
yield result
|
||||
|
||||
|
||||
@dataclass
|
||||
class _RHSResult:
|
||||
"""Intermediate split result from a right hand split."""
|
||||
|
||||
head: Line
|
||||
body: Line
|
||||
tail: Line
|
||||
opening_bracket: Leaf
|
||||
closing_bracket: Leaf
|
||||
|
||||
|
||||
def right_hand_split(
|
||||
line: Line,
|
||||
line_length: int,
|
||||
@ -618,6 +647,22 @@ def right_hand_split(
|
||||
|
||||
Note: running this function modifies `bracket_depth` on the leaves of `line`.
|
||||
"""
|
||||
rhs_result = _first_right_hand_split(line, omit=omit)
|
||||
yield from _maybe_split_omitting_optional_parens(
|
||||
rhs_result, line, line_length, features=features, omit=omit
|
||||
)
|
||||
|
||||
|
||||
def _first_right_hand_split(
|
||||
line: Line,
|
||||
omit: Collection[LeafID] = (),
|
||||
) -> _RHSResult:
|
||||
"""Split the line into head, body, tail starting with the last bracket pair.
|
||||
|
||||
Note: this function should not have side effects. It's relied upon by
|
||||
_maybe_split_omitting_optional_parens to get an opinion whether to prefer
|
||||
splitting on the right side of an assignment statement.
|
||||
"""
|
||||
tail_leaves: List[Leaf] = []
|
||||
body_leaves: List[Leaf] = []
|
||||
head_leaves: List[Leaf] = []
|
||||
@ -653,37 +698,71 @@ def right_hand_split(
|
||||
tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail
|
||||
)
|
||||
bracket_split_succeeded_or_raise(head, body, tail)
|
||||
return _RHSResult(head, body, tail, opening_bracket, closing_bracket)
|
||||
|
||||
|
||||
def _maybe_split_omitting_optional_parens(
|
||||
rhs: _RHSResult,
|
||||
line: Line,
|
||||
line_length: int,
|
||||
features: Collection[Feature] = (),
|
||||
omit: Collection[LeafID] = (),
|
||||
) -> Iterator[Line]:
|
||||
if (
|
||||
Feature.FORCE_OPTIONAL_PARENTHESES not in features
|
||||
# the opening bracket is an optional paren
|
||||
and opening_bracket.type == token.LPAR
|
||||
and not opening_bracket.value
|
||||
and rhs.opening_bracket.type == token.LPAR
|
||||
and not rhs.opening_bracket.value
|
||||
# the closing bracket is an optional paren
|
||||
and closing_bracket.type == token.RPAR
|
||||
and not closing_bracket.value
|
||||
and rhs.closing_bracket.type == token.RPAR
|
||||
and not rhs.closing_bracket.value
|
||||
# it's not an import (optional parens are the only thing we can split on
|
||||
# in this case; attempting a split without them is a waste of time)
|
||||
and not line.is_import
|
||||
# there are no standalone comments in the body
|
||||
and not body.contains_standalone_comments(0)
|
||||
and not rhs.body.contains_standalone_comments(0)
|
||||
# and we can actually remove the parens
|
||||
and can_omit_invisible_parens(body, line_length)
|
||||
and can_omit_invisible_parens(rhs.body, line_length)
|
||||
):
|
||||
omit = {id(closing_bracket), *omit}
|
||||
omit = {id(rhs.closing_bracket), *omit}
|
||||
try:
|
||||
yield from right_hand_split(line, line_length, features=features, omit=omit)
|
||||
return
|
||||
# The _RHSResult Omitting Optional Parens.
|
||||
rhs_oop = _first_right_hand_split(line, omit=omit)
|
||||
if not (
|
||||
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 assignement 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, line_length=line_length - 1)
|
||||
# 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, line_length=line_length)
|
||||
):
|
||||
yield from _maybe_split_omitting_optional_parens(
|
||||
rhs_oop, line, line_length, features=features, omit=omit
|
||||
)
|
||||
return
|
||||
|
||||
except CannotSplit as e:
|
||||
if not (
|
||||
can_be_split(body)
|
||||
or is_line_short_enough(body, line_length=line_length)
|
||||
can_be_split(rhs.body)
|
||||
or is_line_short_enough(rhs.body, line_length=line_length)
|
||||
):
|
||||
raise CannotSplit(
|
||||
"Splitting failed, body is still too long and can't be split."
|
||||
) from e
|
||||
|
||||
elif head.contains_multiline_strings() or tail.contains_multiline_strings():
|
||||
elif (
|
||||
rhs.head.contains_multiline_strings()
|
||||
or rhs.tail.contains_multiline_strings()
|
||||
):
|
||||
raise CannotSplit(
|
||||
"The current optional pair of parentheses is bound to fail to"
|
||||
" satisfy the splitting algorithm because the head or the tail"
|
||||
@ -691,13 +770,42 @@ def right_hand_split(
|
||||
" line."
|
||||
) from e
|
||||
|
||||
ensure_visible(opening_bracket)
|
||||
ensure_visible(closing_bracket)
|
||||
for result in (head, body, tail):
|
||||
ensure_visible(rhs.opening_bracket)
|
||||
ensure_visible(rhs.closing_bracket)
|
||||
for result in (rhs.head, rhs.body, rhs.tail):
|
||||
if result:
|
||||
yield result
|
||||
|
||||
|
||||
def _prefer_split_rhs_oop(rhs_oop: _RHSResult, line_length: int) -> bool:
|
||||
"""
|
||||
Returns whether we should prefer the result from a split omitting optional parens.
|
||||
"""
|
||||
has_closing_bracket_after_assign = False
|
||||
for leaf in reversed(rhs_oop.head.leaves):
|
||||
if leaf.type == token.EQUAL:
|
||||
break
|
||||
if leaf.type in CLOSING_BRACKETS:
|
||||
has_closing_bracket_after_assign = True
|
||||
break
|
||||
return (
|
||||
# contains matching brackets after the `=` (done by checking there is a
|
||||
# closing bracket)
|
||||
has_closing_bracket_after_assign
|
||||
or (
|
||||
# the split is actually from inside the optional parens (done by checking
|
||||
# the first line still contains the `=`)
|
||||
any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves)
|
||||
# the first line is short enough
|
||||
and is_line_short_enough(rhs_oop.head, line_length=line_length)
|
||||
)
|
||||
# contains unsplittable type ignore
|
||||
or rhs_oop.head.contains_unsplittable_type_ignore()
|
||||
or rhs_oop.body.contains_unsplittable_type_ignore()
|
||||
or rhs_oop.tail.contains_unsplittable_type_ignore()
|
||||
)
|
||||
|
||||
|
||||
def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None:
|
||||
"""Raise :exc:`CannotSplit` if the last left- or right-hand split failed.
|
||||
|
||||
@ -1287,8 +1395,10 @@ def run_transformer(
|
||||
|
||||
result.extend(transform_line(transformed_line, mode=mode, features=features))
|
||||
|
||||
features_set = set(features)
|
||||
if (
|
||||
transform.__class__.__name__ != "rhs"
|
||||
Feature.FORCE_OPTIONAL_PARENTHESES in features_set
|
||||
or transform.__class__.__name__ != "rhs"
|
||||
or not line.bracket_tracker.invisible
|
||||
or any(bracket.value for bracket in line.bracket_tracker.invisible)
|
||||
or line.contains_multiline_strings()
|
||||
@ -1305,7 +1415,7 @@ def run_transformer(
|
||||
|
||||
line_copy = line.clone()
|
||||
append_leaves(line_copy, line, line.leaves)
|
||||
features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES}
|
||||
features_fop = features_set | {Feature.FORCE_OPTIONAL_PARENTHESES}
|
||||
second_opinion = run_transformer(
|
||||
line_copy, transform, mode, features_fop, line_str=line_str
|
||||
)
|
||||
|
@ -151,7 +151,11 @@ class Preview(Enum):
|
||||
|
||||
annotation_parens = auto()
|
||||
long_docstring_quotes_on_newline = auto()
|
||||
# NOTE: string_processing requires wrap_long_dict_values_in_parens
|
||||
# for https://github.com/psf/black/issues/3117 to be fixed.
|
||||
string_processing = auto()
|
||||
skip_magic_trailing_comma_in_subscript = auto()
|
||||
wrap_long_dict_values_in_parens = auto()
|
||||
|
||||
|
||||
class Deprecated(UserWarning):
|
||||
|
@ -1638,6 +1638,8 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin):
|
||||
* The line is a dictionary key assignment where some valid key is being
|
||||
assigned the value of some string.
|
||||
OR
|
||||
* The line is an lambda expression and the value is a string.
|
||||
OR
|
||||
* The line starts with an "atom" string that prefers to be wrapped in
|
||||
parens. It's preferred to be wrapped when the string is surrounded by
|
||||
commas (or is the first/last child).
|
||||
@ -1683,7 +1685,7 @@ def do_splitter_match(self, line: Line) -> TMatchResult:
|
||||
or self._else_match(LL)
|
||||
or self._assert_match(LL)
|
||||
or self._assign_match(LL)
|
||||
or self._dict_match(LL)
|
||||
or self._dict_or_lambda_match(LL)
|
||||
or self._prefer_paren_wrap_match(LL)
|
||||
)
|
||||
|
||||
@ -1841,22 +1843,23 @@ def _assign_match(LL: List[Leaf]) -> Optional[int]:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _dict_match(LL: List[Leaf]) -> Optional[int]:
|
||||
def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]:
|
||||
"""
|
||||
Returns:
|
||||
string_idx such that @LL[string_idx] is equal to our target (i.e.
|
||||
matched) string, if this line matches the dictionary key assignment
|
||||
statement requirements listed in the 'Requirements' section of this
|
||||
classes' docstring.
|
||||
statement or lambda expression requirements listed in the
|
||||
'Requirements' section of this classes' docstring.
|
||||
OR
|
||||
None, otherwise.
|
||||
"""
|
||||
# If this line is apart of a dictionary key assignment...
|
||||
if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]:
|
||||
# If this line is a part of a dictionary key assignment or lambda expression...
|
||||
parent_types = [parent_type(LL[0]), parent_type(LL[0].parent)]
|
||||
if syms.dictsetmaker in parent_types or syms.lambdef in parent_types:
|
||||
is_valid_index = is_valid_index_factory(LL)
|
||||
|
||||
for i, leaf in enumerate(LL):
|
||||
# We MUST find a colon...
|
||||
# We MUST find a colon, it can either be dict's or lambda's colon...
|
||||
if leaf.type == token.COLON:
|
||||
idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
|
||||
|
||||
@ -1951,6 +1954,25 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
|
||||
f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
|
||||
)
|
||||
old_rpar_leaf = right_leaves.pop()
|
||||
elif right_leaves and right_leaves[-1].type == token.RPAR:
|
||||
# Special case for lambda expressions as dict's value, e.g.:
|
||||
# my_dict = {
|
||||
# "key": lambda x: f"formatted: {x},
|
||||
# }
|
||||
# After wrapping the dict's value with parentheses, the string is
|
||||
# followed by a RPAR but its opening bracket is lambda's, not
|
||||
# the string's:
|
||||
# "key": (lambda x: f"formatted: {x}),
|
||||
opening_bracket = right_leaves[-1].opening_bracket
|
||||
if opening_bracket is not None and opening_bracket in left_leaves:
|
||||
index = left_leaves.index(opening_bracket)
|
||||
if (
|
||||
index > 0
|
||||
and index < len(left_leaves) - 1
|
||||
and left_leaves[index - 1].type == token.COLON
|
||||
and left_leaves[index + 1].value == "lambda"
|
||||
):
|
||||
right_leaves.pop()
|
||||
|
||||
append_leaves(string_line, line, right_leaves)
|
||||
|
||||
|
@ -54,13 +54,11 @@ def single_quote_docstring_over_line_limit2():
|
||||
|
||||
|
||||
def docstring_almost_at_line_limit():
|
||||
"""long docstring.................................................................
|
||||
"""
|
||||
"""long docstring................................................................."""
|
||||
|
||||
|
||||
def docstring_almost_at_line_limit_with_prefix():
|
||||
f"""long docstring................................................................
|
||||
"""
|
||||
f"""long docstring................................................................"""
|
||||
|
||||
|
||||
def mulitline_docstring_almost_at_line_limit():
|
||||
|
90
tests/data/preview/long_dict_values.py
Normal file
90
tests/data/preview/long_dict_values.py
Normal file
@ -0,0 +1,90 @@
|
||||
my_dict = {
|
||||
"something_something":
|
||||
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
|
||||
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
|
||||
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t",
|
||||
}
|
||||
|
||||
my_dict = {
|
||||
"a key in my dict": a_very_long_variable * and_a_very_long_function_call() / 100000.0
|
||||
}
|
||||
|
||||
my_dict = {
|
||||
"a key in my dict": a_very_long_variable * and_a_very_long_function_call() * and_another_long_func() / 100000.0
|
||||
}
|
||||
|
||||
my_dict = {
|
||||
"a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value")
|
||||
}
|
||||
|
||||
{
|
||||
'xxxxxx':
|
||||
xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxx(
|
||||
xxxxxxxxxxxxxx={
|
||||
'x':
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=(
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
xxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx={
|
||||
'x': x.xx,
|
||||
'x': x.x,
|
||||
}))))
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
# output
|
||||
|
||||
|
||||
my_dict = {
|
||||
"something_something": (
|
||||
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
|
||||
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
|
||||
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
|
||||
),
|
||||
}
|
||||
|
||||
my_dict = {
|
||||
"a key in my dict": (
|
||||
a_very_long_variable * and_a_very_long_function_call() / 100000.0
|
||||
)
|
||||
}
|
||||
|
||||
my_dict = {
|
||||
"a key in my dict": (
|
||||
a_very_long_variable
|
||||
* and_a_very_long_function_call()
|
||||
* and_another_long_func()
|
||||
/ 100000.0
|
||||
)
|
||||
}
|
||||
|
||||
my_dict = {
|
||||
"a key in my dict": (
|
||||
MyClass.some_attribute.first_call()
|
||||
.second_call()
|
||||
.third_call(some_args="some value")
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
"xxxxxx": xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxx(
|
||||
xxxxxxxxxxxxxx={
|
||||
"x": xxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=(
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
xxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx={
|
||||
"x": x.xx,
|
||||
"x": x.x,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
}
|
@ -278,6 +278,15 @@ def foo():
|
||||
"........................................................................... \\N{LAO KO LA}"
|
||||
)
|
||||
|
||||
msg = lambda x: f"this is a very very very long lambda value {x} that doesn't fit on a single line"
|
||||
|
||||
dict_with_lambda_values = {
|
||||
"join": lambda j: (
|
||||
f"{j.__class__.__name__}({some_function_call(j.left)}, "
|
||||
f"{some_function_call(j.right)})"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# output
|
||||
|
||||
@ -362,9 +371,8 @@ def foo():
|
||||
"A %s %s"
|
||||
% ("formatted", "string"): (
|
||||
"This is a really really really long string that has to go inside of a"
|
||||
" dictionary. It is %s bad (#%d)."
|
||||
)
|
||||
% ("soooo", 2),
|
||||
" dictionary. It is %s bad (#%d)." % ("soooo", 2)
|
||||
),
|
||||
}
|
||||
|
||||
D5 = { # Test for https://github.com/psf/black/issues/3261
|
||||
@ -806,3 +814,17 @@ def foo():
|
||||
"..........................................................................."
|
||||
" \\N{LAO KO LA}"
|
||||
)
|
||||
|
||||
msg = (
|
||||
lambda x: (
|
||||
f"this is a very very very long lambda value {x} that doesn't fit on a single"
|
||||
" line"
|
||||
)
|
||||
)
|
||||
|
||||
dict_with_lambda_values = {
|
||||
"join": lambda j: (
|
||||
f"{j.__class__.__name__}({some_function_call(j.left)}, "
|
||||
f"{some_function_call(j.right)})"
|
||||
),
|
||||
}
|
||||
|
@ -524,6 +524,13 @@ async def foo(self):
|
||||
},
|
||||
)
|
||||
|
||||
# Regression test for https://github.com/psf/black/issues/3117.
|
||||
some_dict = {
|
||||
"something_something":
|
||||
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
|
||||
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t",
|
||||
}
|
||||
|
||||
|
||||
# output
|
||||
|
||||
@ -976,9 +983,9 @@ def xxxxxxx_xxxxxx(xxxx):
|
||||
)
|
||||
|
||||
|
||||
value.__dict__[
|
||||
key
|
||||
] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee
|
||||
value.__dict__[key] = (
|
||||
"test" # set some Thrift field to non-None in the struct aa bb cc dd ee
|
||||
)
|
||||
|
||||
RE_ONE_BACKSLASH = {
|
||||
"asdf_hjkl_jkl": re.compile(
|
||||
@ -1178,3 +1185,11 @@ async def foo(self):
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# Regression test for https://github.com/psf/black/issues/3117.
|
||||
some_dict = {
|
||||
"something_something": (
|
||||
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
|
||||
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
|
||||
),
|
||||
}
|
||||
|
85
tests/data/preview/prefer_rhs_split.py
Normal file
85
tests/data/preview/prefer_rhs_split.py
Normal file
@ -0,0 +1,85 @@
|
||||
first_item, second_item = (
|
||||
some_looooooooong_module.some_looooooooooooooong_function_name(
|
||||
first_argument, second_argument, third_argument
|
||||
)
|
||||
)
|
||||
|
||||
some_dict["with_a_long_key"] = (
|
||||
some_looooooooong_module.some_looooooooooooooong_function_name(
|
||||
first_argument, second_argument, third_argument
|
||||
)
|
||||
)
|
||||
|
||||
# Make sure it works when the RHS only has one pair of (optional) parens.
|
||||
first_item, second_item = (
|
||||
some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name
|
||||
)
|
||||
|
||||
some_dict["with_a_long_key"] = (
|
||||
some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name
|
||||
)
|
||||
|
||||
# Make sure chaining assignments work.
|
||||
first_item, second_item, third_item, forth_item = m["everything"] = (
|
||||
some_looooooooong_module.some_looooooooooooooong_function_name(
|
||||
first_argument, second_argument, third_argument
|
||||
)
|
||||
)
|
||||
|
||||
# Make sure when the RHS's first split at the non-optional paren fits,
|
||||
# we split there instead of the outer RHS optional paren.
|
||||
first_item, second_item = some_looooooooong_module.some_loooooog_function_name(
|
||||
first_argument, second_argument, third_argument
|
||||
)
|
||||
|
||||
(
|
||||
first_item,
|
||||
second_item,
|
||||
third_item,
|
||||
forth_item,
|
||||
fifth_item,
|
||||
last_item_very_loooooong,
|
||||
) = some_looooooooong_module.some_looooooooooooooong_function_name(
|
||||
first_argument, second_argument, third_argument
|
||||
)
|
||||
|
||||
(
|
||||
first_item,
|
||||
second_item,
|
||||
third_item,
|
||||
forth_item,
|
||||
fifth_item,
|
||||
last_item_very_loooooong,
|
||||
) = everyting = some_loooooog_function_name(
|
||||
first_argument, second_argument, third_argument
|
||||
)
|
||||
|
||||
|
||||
# Make sure unsplittable type ignore won't be moved.
|
||||
some_kind_of_table[some_key] = util.some_function( # type: ignore # noqa: E501
|
||||
some_arg
|
||||
).intersection(pk_cols)
|
||||
|
||||
some_kind_of_table[
|
||||
some_key
|
||||
] = lambda obj: obj.some_long_named_method() # type: ignore # noqa: E501
|
||||
|
||||
some_kind_of_table[
|
||||
some_key # type: ignore # noqa: E501
|
||||
] = lambda obj: obj.some_long_named_method()
|
||||
|
||||
|
||||
# Make when when the left side of assignement plus the opening paren "... = (" is
|
||||
# exactly line length limit + 1, it won't be split like that.
|
||||
xxxxxxxxx_yyy_zzzzzzzz[
|
||||
xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)
|
||||
] = 1
|
||||
|
||||
|
||||
# Right side of assignment contains un-nested pairs of inner parens.
|
||||
some_kind_of_instance.some_kind_of_map[a_key] = (
|
||||
isinstance(some_var, SomeClass)
|
||||
and table.something_and_something != table.something_else
|
||||
) or (
|
||||
isinstance(some_other_var, BaseClass) and table.something != table.some_other_thing
|
||||
)
|
38
tests/data/preview/prefer_rhs_split_reformatted.py
Normal file
38
tests/data/preview/prefer_rhs_split_reformatted.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Test cases separate from `prefer_rhs_split.py` that contains unformatted source.
|
||||
|
||||
# Left hand side fits in a single line but will still be exploded by the
|
||||
# magic trailing comma.
|
||||
first_value, (m1, m2,), third_value = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv(
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
|
||||
# Make when when the left side of assignement plus the opening paren "... = (" is
|
||||
# exactly line length limit + 1, it won't be split like that.
|
||||
xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1
|
||||
|
||||
|
||||
# output
|
||||
|
||||
|
||||
# Test cases separate from `prefer_rhs_split.py` that contains unformatted source.
|
||||
|
||||
# Left hand side fits in a single line but will still be exploded by the
|
||||
# magic trailing comma.
|
||||
(
|
||||
first_value,
|
||||
(
|
||||
m1,
|
||||
m2,
|
||||
),
|
||||
third_value,
|
||||
) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv(
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
|
||||
# Make when when the left side of assignement plus the opening paren "... = (" is
|
||||
# exactly line length limit + 1, it won't be split like that.
|
||||
xxxxxxxxx_yyy_zzzzzzzz[
|
||||
xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)
|
||||
] = 1
|
@ -64,7 +64,7 @@ async def call(param):
|
||||
print ( "This will be formatted" )
|
||||
|
||||
|
||||
# Regression test for https://github.com/psf/black/issues/2985
|
||||
# Regression test for https://github.com/psf/black/issues/2985.
|
||||
class Named(t.Protocol):
|
||||
# fmt: off
|
||||
@property
|
||||
@ -75,6 +75,15 @@ def this_will_be_formatted ( self, **kwargs ) -> Named: ...
|
||||
# fmt: on
|
||||
|
||||
|
||||
# Regression test for https://github.com/psf/black/issues/3436.
|
||||
if x:
|
||||
return x
|
||||
# fmt: off
|
||||
elif unformatted:
|
||||
# fmt: on
|
||||
will_be_formatted ()
|
||||
|
||||
|
||||
# output
|
||||
|
||||
|
||||
@ -144,7 +153,7 @@ async def call(param):
|
||||
print("This will be formatted")
|
||||
|
||||
|
||||
# Regression test for https://github.com/psf/black/issues/2985
|
||||
# Regression test for https://github.com/psf/black/issues/2985.
|
||||
class Named(t.Protocol):
|
||||
# fmt: off
|
||||
@property
|
||||
@ -156,3 +165,12 @@ def this_will_be_formatted(self, **kwargs) -> Named:
|
||||
...
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
# Regression test for https://github.com/psf/black/issues/3436.
|
||||
if x:
|
||||
return x
|
||||
# fmt: off
|
||||
elif unformatted:
|
||||
# fmt: on
|
||||
will_be_formatted()
|
||||
|
@ -17,7 +17,7 @@ def test_ipynb_diff_with_no_change_single() -> None:
|
||||
result = runner.invoke(main, [str(path)])
|
||||
expected_output = (
|
||||
"Skipping .ipynb files as Jupyter dependencies are not installed.\n"
|
||||
"You can fix this by running ``pip install black[jupyter]``\n"
|
||||
'You can fix this by running ``pip install "black[jupyter]"``\n'
|
||||
)
|
||||
assert expected_output in result.output
|
||||
|
||||
@ -32,6 +32,6 @@ def test_ipynb_diff_with_no_change_dir(tmp_path: pathlib.Path) -> None:
|
||||
result = runner.invoke(main, [str(tmp_path)])
|
||||
expected_output = (
|
||||
"Skipping .ipynb files as Jupyter dependencies are not installed.\n"
|
||||
"You can fix this by running ``pip install black[jupyter]``\n"
|
||||
'You can fix this by running ``pip install "black[jupyter]"``\n'
|
||||
)
|
||||
assert expected_output in result.output
|
||||
|
Loading…
Reference in New Issue
Block a user