Skip to content

Commit

Permalink
Prepare the 2025 stable style (#4558)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Jan 25, 2025
1 parent e58baf1 commit c0b92f3
Show file tree
Hide file tree
Showing 28 changed files with 72 additions and 156 deletions.
19 changes: 17 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,29 @@

<!-- Include any especially major or disruptive changes here -->

This release introduces the new 2025 stable style (#4558), stabilizing
the following changes:

- Normalize casing of Unicode escape characters in strings to lowercase (#2916)
- Fix inconsistencies in whether certain strings are detected as docstrings (#4095)
- Consistently add trailing commas to typed function parameters (#4164)
- Remove redundant parentheses in if guards for case blocks (#4214)
- Add parentheses to if clauses in case blocks when the line is too long (#4269)
- Whitespace before `# fmt: skip` comments is no longer normalized (#4146)
- Fix line length computation for certain expressions that involve the power operator (#4154)
- Check if there is a newline before the terminating quotes of a docstring (#4185)
- Fix type annotation spacing between `*` and more complex type variable tuple (#4440)

The following changes were not in any previous release:

- Remove parentheses around sole list items (#4312)

### Stable style

<!-- Changes that affect Black's stable style -->

- Fix formatting cells in IPython notebooks with magic methods and starting or trailing
empty lines (#4484)

- Fix crash when formatting `with` statements containing tuple generators/unpacking
(#4538)

Expand All @@ -22,7 +38,6 @@

- Fix/remove string merging changing f-string quotes on f-strings with internal quotes
(#4498)
- Remove parentheses around sole list items (#4312)
- Collapse multiple empty lines after an import into one (#4489)
- Prevent `string_processing` and `wrap_long_dict_values_in_parens` from removing
parentheses around long dictionary values (#4377)
Expand Down
5 changes: 5 additions & 0 deletions docs/the_black_code_style/current_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ exception of [capital "R" prefixes](#rstrings-and-rstrings), unicode literal mar
(`u`) are removed because they are meaningless in Python 3, and in the case of multiple
characters "r" is put first as in spoken language: "raw f-string".

Another area where Python allows multiple ways to format a string is escape sequences.
For example, `"\uabcd"` and `"\uABCD"` evaluate to the same string. _Black_ normalizes
such escape sequences to lowercase, but uses uppercase for `\N` named character escapes,
such as `"\N{MEETEI MAYEK LETTER HUK}"`.

The main reason to standardize on a single form of quotes is aesthetics. Having one kind
of quotes everywhere reduces reader distraction. It will also enable a future version of
_Black_ to merge consecutive string literals that ended up on the same line (see
Expand Down
21 changes: 0 additions & 21 deletions docs/the_black_code_style/future_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,6 @@ demoted from the `--preview` to the `--unstable` style, users can use the

Currently, the following features are included in the preview style:

- `hex_codes_in_unicode_sequences`: normalize casing of Unicode escape characters in
strings
- `unify_docstring_detection`: fix inconsistencies in whether certain strings are
detected as docstrings
- `no_normalize_fmt_skip_whitespace`: whitespace before `# fmt: skip` comments is no
longer normalized
- `typed_params_trailing_comma`: consistently add trailing commas to typed function
parameters
- `is_simple_lookup_for_doublestar_expression`: fix line length computation for certain
expressions that involve the power operator
- `docstring_check_for_newline`: checks if there is a newline before the terminating
quotes of a docstring
- `remove_redundant_guard_parens`: Removes redundant parentheses in `if` guards for
`case` blocks.
- `parens_for_long_if_clauses_in_case_block`: Adds parentheses to `if` clauses in `case`
blocks when the line is too long
- `pep646_typed_star_arg_type_var_tuple`: fix type annotation spacing between * and more
complex type variable tuple (i.e. `def fn(*args: *tuple[*Ts, T]) -> None: pass`)
- `remove_lone_list_item_parens`: remove redundant parentheses around lone list items
(depends on unstable `hug_parens_with_braces_and_square_brackets` feature in some
cases)
- `always_one_newline_after_import`: Always force one blank line after import
statements, except when the line after the import is a comment or an import statement

Expand Down
8 changes: 2 additions & 6 deletions src/black/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from functools import lru_cache
from typing import Final, Optional, Union

from black.mode import Mode, Preview
from black.mode import Mode
from black.nodes import (
CLOSING_BRACKETS,
STANDALONE_COMMENT,
Expand Down Expand Up @@ -235,11 +235,7 @@ def convert_one_fmt_off_pair(
standalone_comment_prefix += fmt_off_prefix
hidden_value = comment.value + "\n" + hidden_value
if is_fmt_skip:
hidden_value += (
comment.leading_whitespace
if Preview.no_normalize_fmt_skip_whitespace in mode
else " "
) + comment.value
hidden_value += comment.leading_whitespace + comment.value
if hidden_value.endswith("\n"):
# That happens when one of the `ignored_nodes` ended with a NEWLINE
# leaf (possibly followed by a DEDENT).
Expand Down
25 changes: 17 additions & 8 deletions src/black/handle_ipynb_magics.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
"time",
"timeit",
))
TOKEN_HEX = secrets.token_hex


@dataclasses.dataclass(frozen=True)
Expand Down Expand Up @@ -160,7 +159,7 @@ def mask_cell(src: str) -> tuple[str, list[Replacement]]:
becomes
"25716f358c32750e"
b"25716f358c32750"
'foo'
The replacements are returned, along with the transformed code.
Expand Down Expand Up @@ -192,6 +191,18 @@ def mask_cell(src: str) -> tuple[str, list[Replacement]]:
return transformed, replacements


def create_token(n_chars: int) -> str:
"""Create a randomly generated token that is n_chars characters long."""
assert n_chars > 0
n_bytes = max(n_chars // 2 - 1, 1)
token = secrets.token_hex(n_bytes)
if len(token) + 3 > n_chars:
token = token[:-1]
# We use a bytestring so that the string does not get interpreted
# as a docstring.
return f'b"{token}"'


def get_token(src: str, magic: str) -> str:
"""Return randomly generated token to mask IPython magic with.
Expand All @@ -201,21 +212,19 @@ def get_token(src: str, magic: str) -> str:
not already present anywhere else in the cell.
"""
assert magic
nbytes = max(len(magic) // 2 - 1, 1)
token = TOKEN_HEX(nbytes)
n_chars = len(magic)
token = create_token(n_chars)
counter = 0
while token in src:
token = TOKEN_HEX(nbytes)
token = create_token(n_chars)
counter += 1
if counter > 100:
raise AssertionError(
"INTERNAL ERROR: Black was not able to replace IPython magic. "
"Please report a bug on https://github.com/psf/black/issues. "
f"The magic might be helpful: {magic}"
) from None
if len(token) + 2 < len(magic):
token = f"{token}."
return f'"{token}"'
return token


def replace_cell_magics(src: str) -> tuple[str, list[Replacement]]:
Expand Down
31 changes: 7 additions & 24 deletions src/black/linegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,10 +414,9 @@ def foo(a: (int), b: (float) = 7): ...
yield from self.visit_default(node)

def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
if Preview.hex_codes_in_unicode_sequences in self.mode:
normalize_unicode_escape_sequences(leaf)
normalize_unicode_escape_sequences(leaf)

if is_docstring(leaf, self.mode) and not re.search(r"\\\s*\n", leaf.value):
if is_docstring(leaf) and not re.search(r"\\\s*\n", leaf.value):
# We're ignoring docstrings with backslash newline escapes because changing
# indentation of those changes the AST representation of the code.
if self.mode.string_normalization:
Expand Down Expand Up @@ -488,10 +487,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
and len(indent) + quote_len <= self.mode.line_length
and not has_trailing_backslash
):
if (
Preview.docstring_check_for_newline in self.mode
and leaf.value[-1 - quote_len] == "\n"
):
if leaf.value[-1 - quote_len] == "\n":
leaf.value = prefix + quote + docstring + quote
else:
leaf.value = prefix + quote + docstring + "\n" + indent + quote
Expand All @@ -511,10 +507,7 @@ def visit_NUMBER(self, leaf: Leaf) -> Iterator[Line]:

def visit_atom(self, node: Node) -> Iterator[Line]:
"""Visit any atom"""
if (
Preview.remove_lone_list_item_parens in self.mode
and len(node.children) == 3
):
if len(node.children) == 3:
first = node.children[0]
last = node.children[-1]
if (first.type == token.LSQB and last.type == token.RSQB) or (
Expand Down Expand Up @@ -602,8 +595,7 @@ def __post_init__(self) -> None:
# PEP 634
self.visit_match_stmt = self.visit_match_case
self.visit_case_block = self.visit_match_case
if Preview.remove_redundant_guard_parens in self.mode:
self.visit_guard = partial(v, keywords=Ø, parens={"if"})
self.visit_guard = partial(v, keywords=Ø, parens={"if"})


def _hugging_power_ops_line_to_string(
Expand Down Expand Up @@ -1132,12 +1124,7 @@ def _ensure_trailing_comma(
return False
# Don't add commas if we already have any commas
if any(
leaf.type == token.COMMA
and (
Preview.typed_params_trailing_comma not in original.mode
or not is_part_of_annotation(leaf)
)
for leaf in leaves
leaf.type == token.COMMA and not is_part_of_annotation(leaf) for leaf in leaves
):
return False

Expand Down Expand Up @@ -1418,11 +1405,7 @@ def normalize_invisible_parens( # noqa: C901
)

# Add parentheses around if guards in case blocks
if (
isinstance(child, Node)
and child.type == syms.guard
and Preview.parens_for_long_if_clauses_in_case_block in mode
):
if isinstance(child, Node) and child.type == syms.guard:
normalize_invisible_parens(
child, parens_after={"if"}, mode=mode, features=features
)
Expand Down
4 changes: 1 addition & 3 deletions src/black/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,7 @@ def _is_triple_quoted_string(self) -> bool:
@property
def is_docstring(self) -> bool:
"""Is the line a docstring?"""
if Preview.unify_docstring_detection not in self.mode:
return self._is_triple_quoted_string
return bool(self) and is_docstring(self.leaves[0], self.mode)
return bool(self) and is_docstring(self.leaves[0])

@property
def is_chained_assignment(self) -> bool:
Expand Down
12 changes: 0 additions & 12 deletions src/black/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,24 +196,12 @@ def supports_feature(target_versions: set[TargetVersion], feature: Feature) -> b
class Preview(Enum):
"""Individual preview style features."""

hex_codes_in_unicode_sequences = 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()
hug_parens_with_braces_and_square_brackets = auto()
unify_docstring_detection = auto()
no_normalize_fmt_skip_whitespace = auto()
wrap_long_dict_values_in_parens = auto()
multiline_string_handling = auto()
typed_params_trailing_comma = auto()
is_simple_lookup_for_doublestar_expression = auto()
docstring_check_for_newline = auto()
remove_redundant_guard_parens = auto()
parens_for_long_if_clauses_in_case_block = auto()
# NOTE: remove_lone_list_item_parens requires
# hug_parens_with_braces_and_square_brackets to remove parens in some cases
remove_lone_list_item_parens = auto()
pep646_typed_star_arg_type_var_tuple = auto()
always_one_newline_after_import = auto()


Expand Down
15 changes: 4 additions & 11 deletions src/black/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from mypy_extensions import mypyc_attr

from black.cache import CACHE_DIR
from black.mode import Mode, Preview
from black.mode import Mode
from black.strings import get_string_prefix, has_triple_quotes
from blib2to3 import pygram
from blib2to3.pgen2 import token
Expand Down Expand Up @@ -244,13 +244,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no
elif (
prevp.type == token.STAR
and parent_type(prevp) == syms.star_expr
and (
parent_type(prevp.parent) == syms.subscriptlist
or (
Preview.pep646_typed_star_arg_type_var_tuple in mode
and parent_type(prevp.parent) == syms.tname_star
)
)
and parent_type(prevp.parent) in (syms.subscriptlist, syms.tname_star)
):
# No space between typevar tuples or unpacking them.
return NO
Expand Down Expand Up @@ -551,7 +545,7 @@ def is_arith_like(node: LN) -> bool:
}


def is_docstring(node: NL, mode: Mode) -> bool:
def is_docstring(node: NL) -> bool:
if isinstance(node, Leaf):
if node.type != token.STRING:
return False
Expand All @@ -561,8 +555,7 @@ def is_docstring(node: NL, mode: Mode) -> bool:
return False

if (
Preview.unify_docstring_detection in mode
and node.parent
node.parent
and node.parent.type == syms.simple_stmt
and not node.parent.prev_sibling
and node.parent.parent
Expand Down
10 changes: 0 additions & 10 deletions src/black/resources/black.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,10 @@
"type": "array",
"items": {
"enum": [
"hex_codes_in_unicode_sequences",
"string_processing",
"hug_parens_with_braces_and_square_brackets",
"unify_docstring_detection",
"no_normalize_fmt_skip_whitespace",
"wrap_long_dict_values_in_parens",
"multiline_string_handling",
"typed_params_trailing_comma",
"is_simple_lookup_for_doublestar_expression",
"docstring_check_for_newline",
"remove_redundant_guard_parens",
"parens_for_long_if_clauses_in_case_block",
"remove_lone_list_item_parens",
"pep646_typed_star_arg_type_var_tuple",
"always_one_newline_after_import"
]
},
Expand Down
Loading

0 comments on commit c0b92f3

Please sign in to comment.