Compare commits
No commits in common. "7987951e246b4e76cc5225b2ccd89b5519a25ac8" and "950ec38c119916868833cb9896dab324c5d8eadd" have entirely different histories.
7987951e24
...
950ec38c11
10
.github/workflows/diff_shades.yml
vendored
10
.github/workflows/diff_shades.yml
vendored
@ -34,8 +34,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: >
|
||||
python scripts/diff_shades_gha_helper.py config ${{ github.event_name }}
|
||||
${{ matrix.mode }}
|
||||
python scripts/diff_shades_gha_helper.py config ${{ github.event_name }} ${{ matrix.mode }}
|
||||
|
||||
analysis:
|
||||
name: analysis / ${{ matrix.mode }}
|
||||
@ -131,9 +130,10 @@ jobs:
|
||||
- name: Generate summary file (PR only)
|
||||
if: github.event_name == 'pull_request' && matrix.mode == 'preview-changes'
|
||||
run: >
|
||||
python helper.py comment-body ${{ matrix.baseline-analysis }}
|
||||
${{ matrix.target-analysis }} ${{ matrix.baseline-sha }}
|
||||
${{ matrix.target-sha }} ${{ github.event.pull_request.number }}
|
||||
python helper.py comment-body
|
||||
${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }}
|
||||
${{ matrix.baseline-sha }} ${{ matrix.target-sha }}
|
||||
${{ github.event.pull_request.number }}
|
||||
|
||||
- name: Upload summary file (PR only)
|
||||
if: github.event_name == 'pull_request' && matrix.mode == 'preview-changes'
|
||||
|
2
.github/workflows/pypi_upload.yml
vendored
2
.github/workflows/pypi_upload.yml
vendored
@ -92,7 +92,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Keep cibuildwheel version in sync with above
|
||||
- uses: pypa/cibuildwheel@v2.23.3
|
||||
- uses: pypa/cibuildwheel@v2.22.0
|
||||
with:
|
||||
only: ${{ matrix.only }}
|
||||
|
||||
|
@ -24,12 +24,12 @@ repos:
|
||||
additional_dependencies: *version_check_dependencies
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 6.0.1
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.2.0
|
||||
rev: 7.1.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
@ -48,9 +48,7 @@ repos:
|
||||
- types-PyYAML
|
||||
- types-atheris
|
||||
- tomli >= 0.2.6, < 2.0.0
|
||||
- click >= 8.2.0
|
||||
# Click is intentionally out-of-sync with pyproject.toml
|
||||
# v8.2 has breaking changes. We work around them at runtime, but we need the newer stubs.
|
||||
- click >= 8.1.0, != 8.1.4, != 8.1.5
|
||||
- packaging >= 22.0
|
||||
- platformdirs >= 2.1.0
|
||||
- pytokens >= 0.1.10
|
||||
@ -66,11 +64,11 @@ repos:
|
||||
args: ["--python-version=3.10"]
|
||||
additional_dependencies: *mypy_deps
|
||||
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.5.3
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [markdown, yaml, json]
|
||||
types_or: [css, javascript, html, json, yaml]
|
||||
exclude: \.github/workflows/diff_shades\.yml
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
|
36
CHANGES.md
36
CHANGES.md
@ -9,24 +9,19 @@
|
||||
### Stable style
|
||||
|
||||
<!-- Changes that affect Black's stable style -->
|
||||
|
||||
- Fix crash while formatting a long `del` statement containing tuples (#4628)
|
||||
- Fix crash while formatting expressions using the walrus operator in complex `with`
|
||||
statements (#4630)
|
||||
- Fix crash while formatting expressions using the walrus operator in complex
|
||||
`with` statements (#4630)
|
||||
- Handle `# fmt: skip` followed by a comment at the end of file (#4635)
|
||||
- Fix crash when a tuple appears in the `as` clause of a `with` statement (#4634)
|
||||
- Fix crash when tuple is used as a context manager inside a `with` statement (#4646)
|
||||
- Fix crash on a `\\r\n` (#4673)
|
||||
- Fix crash on `await ...` (where `...` is a literal `Ellipsis`) (#4676)
|
||||
- Remove support for pre-python 3.7 `await/async` as soft keywords/variable names
|
||||
(#4676)
|
||||
- Fix crash when a tuple appears in the `as` clause of a `with` statement
|
||||
(#4634)
|
||||
|
||||
### Preview style
|
||||
|
||||
<!-- Changes that affect Black's preview style -->
|
||||
|
||||
- Fix a bug where one-liner functions/conditionals marked with `# fmt: skip` would still
|
||||
be formatted (#4552)
|
||||
- Fix a bug where one-liner functions/conditionals marked with `# fmt: skip`
|
||||
would still be formatted (#4552)
|
||||
|
||||
### Configuration
|
||||
|
||||
@ -41,8 +36,8 @@
|
||||
<!-- Changes to the parser or to version autodetection -->
|
||||
|
||||
- Rewrite tokenizer to improve performance and compliance (#4536)
|
||||
- Fix bug where certain unusual expressions (e.g., lambdas) were not accepted in type
|
||||
parameter bounds and defaults. (#4602)
|
||||
- Fix bug where certain unusual expressions (e.g., lambdas) were not accepted
|
||||
in type parameter bounds and defaults. (#4602)
|
||||
|
||||
### Performance
|
||||
|
||||
@ -61,8 +56,8 @@
|
||||
<!-- For example, Docker, GitHub Actions, pre-commit, editors -->
|
||||
|
||||
- Fix the version check in the vim file to reject Python 3.8 (#4567)
|
||||
- Enhance GitHub Action `psf/black` to read Black version from an additional section in
|
||||
pyproject.toml: `[project.dependency-groups]` (#4606)
|
||||
- Enhance GitHub Action `psf/black` to read Black version from an additional
|
||||
section in pyproject.toml: `[project.dependency-groups]` (#4606)
|
||||
|
||||
### Documentation
|
||||
|
||||
@ -73,8 +68,8 @@
|
||||
|
||||
### Highlights
|
||||
|
||||
This release introduces the new 2025 stable style (#4558), stabilizing the following
|
||||
changes:
|
||||
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)
|
||||
@ -82,16 +77,15 @@ changes:
|
||||
- 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)
|
||||
- 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)
|
||||
- Generic function definitions are now formatted more elegantly: parameters are split
|
||||
over multiple lines first instead of type parameter definitions (#4553)
|
||||
- Generic function definitions are now formatted more elegantly: parameters are
|
||||
split over multiple lines first instead of type parameter definitions (#4553)
|
||||
|
||||
### Stable style
|
||||
|
||||
|
@ -137,8 +137,8 @@ SQLAlchemy, Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtuale
|
||||
pandas, Pillow, Twisted, LocalStack, every Datadog Agent Integration, Home Assistant,
|
||||
Zulip, Kedro, OpenOA, FLORIS, ORBIT, WOMBAT, and many more.
|
||||
|
||||
The following organizations use _Black_: Dropbox, KeepTruckin, Lyft, Mozilla, Quora,
|
||||
Duolingo, QuantumBlack, Tesla, Archer Aviation.
|
||||
The following organizations use _Black_: Dropbox, KeepTruckin, Lyft, Mozilla,
|
||||
Quora, Duolingo, QuantumBlack, Tesla, Archer Aviation.
|
||||
|
||||
Are we missing anyone? Let us know.
|
||||
|
||||
|
@ -29,8 +29,8 @@ frequently than monthly nets rapidly diminishing returns.
|
||||
**You must have `write` permissions for the _Black_ repository to cut a release.**
|
||||
|
||||
The 10,000 foot view of the release process is that you prepare a release PR and then
|
||||
publish a [GitHub Release]. This triggers [release automation](#release-workflows) that
|
||||
builds all release artifacts and publishes them to the various platforms we publish to.
|
||||
publish a [GitHub Release]. This triggers [release automation](#release-workflows) that builds
|
||||
all release artifacts and publishes them to the various platforms we publish to.
|
||||
|
||||
We now have a `scripts/release.py` script to help with cutting the release PRs.
|
||||
|
||||
@ -96,9 +96,8 @@ In the end, use your best judgement and ask other maintainers for their thoughts
|
||||
|
||||
## Release workflows
|
||||
|
||||
All of _Black_'s release automation uses [GitHub Actions]. All workflows are therefore
|
||||
configured using YAML files in the `.github/workflows` directory of the _Black_
|
||||
repository.
|
||||
All of _Black_'s release automation uses [GitHub Actions]. All workflows are therefore configured
|
||||
using YAML files in the `.github/workflows` directory of the _Black_ repository.
|
||||
|
||||
They are triggered by the publication of a [GitHub Release].
|
||||
|
||||
|
@ -93,8 +93,6 @@ Support for formatting Python 2 code was removed in version 22.0. While we've ma
|
||||
plans to stop supporting older Python 3 minor versions immediately, their support might
|
||||
also be removed some time in the future without a deprecation period.
|
||||
|
||||
`await`/`async` as soft keywords/indentifiers are no longer supported as of 25.2.0.
|
||||
|
||||
Runtime support for 3.6 was removed in version 22.10.0, for 3.7 in version 23.7.0, and
|
||||
for 3.8 in version 24.10.0.
|
||||
|
||||
|
@ -38,9 +38,9 @@ the `pyproject.toml` file. `version` can be any
|
||||
or just the version number if you want an exact version. To read the version from the
|
||||
`pyproject.toml` file instead, set `use_pyproject` to `true`. This will first look into
|
||||
the `tool.black.required-version` field, then the `dependency-groups` table, then the
|
||||
`project.dependencies` array and finally the `project.optional-dependencies` table. The
|
||||
action defaults to the latest release available on PyPI. Only versions available from
|
||||
PyPI are supported, so no commit SHAs or branch names.
|
||||
`project.dependencies` array and finally the `project.optional-dependencies` table.
|
||||
The action defaults to the latest release available on PyPI. Only versions available
|
||||
from PyPI are supported, so no commit SHAs or branch names.
|
||||
|
||||
If you want to include Jupyter Notebooks, _Black_ must be installed with the `jupyter`
|
||||
extra. Installing the extra and including Jupyter Notebook files can be configured via
|
||||
|
@ -5,11 +5,14 @@
|
||||
a coverage-guided fuzzer I'm working on.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import hypothesmith
|
||||
from hypothesis import HealthCheck, given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
import black
|
||||
from blib2to3.pgen2.tokenize import TokenError
|
||||
|
||||
|
||||
# This test uses the Hypothesis and Hypothesmith libraries to generate random
|
||||
@ -42,7 +45,23 @@ def test_idempotent_any_syntatically_valid_python(
|
||||
compile(src_contents, "<string>", "exec") # else the bug is in hypothesmith
|
||||
|
||||
# Then format the code...
|
||||
try:
|
||||
dst_contents = black.format_str(src_contents, mode=mode)
|
||||
except black.InvalidInput:
|
||||
# This is a bug - if it's valid Python code, as above, Black should be
|
||||
# able to cope with it. See issues #970, #1012
|
||||
# TODO: remove this try-except block when issues are resolved.
|
||||
return
|
||||
except TokenError as e:
|
||||
if ( # Special-case logic for backslashes followed by newlines or end-of-input
|
||||
e.args[0] == "EOF in multi-line statement"
|
||||
and re.search(r"\\($|\r?\n)", src_contents) is not None
|
||||
):
|
||||
# This is a bug - if it's valid Python code, as above, Black should be
|
||||
# able to cope with it. See issue #1012.
|
||||
# TODO: remove this block when the issue is resolved.
|
||||
return
|
||||
raise
|
||||
|
||||
# And check that we got equivalent and stable output.
|
||||
black.assert_equivalent(src_contents, dst_contents)
|
||||
|
@ -77,7 +77,7 @@ def blackify(base_branch: str, black_command: str, logger: logging.Logger) -> in
|
||||
git("commit", "--allow-empty", "-aqC", commit)
|
||||
|
||||
for commit in commits:
|
||||
git("branch", "-qD", f"{commit}-black")
|
||||
git("branch", "-qD", "%s-black" % commit)
|
||||
|
||||
return 0
|
||||
|
||||
|
@ -40,7 +40,6 @@
|
||||
ensure_visible,
|
||||
fstring_to_string,
|
||||
get_annotation_type,
|
||||
has_sibling_with_type,
|
||||
is_arith_like,
|
||||
is_async_stmt_or_funcdef,
|
||||
is_atom_with_invisible_parens,
|
||||
@ -1629,11 +1628,6 @@ def maybe_make_parens_invisible_in_atom(
|
||||
or is_empty_tuple(node)
|
||||
or is_one_tuple(node)
|
||||
or (is_tuple(node) and parent.type == syms.asexpr_test)
|
||||
or (
|
||||
is_tuple(node)
|
||||
and parent.type == syms.with_stmt
|
||||
and has_sibling_with_type(node, token.COMMA)
|
||||
)
|
||||
or (is_yield(node) and parent.type != syms.expr_stmt)
|
||||
or (
|
||||
# This condition tries to prevent removing non-optional brackets
|
||||
|
@ -1058,21 +1058,3 @@ def furthest_ancestor_with_last_leaf(leaf: Leaf) -> LN:
|
||||
while node.parent and node.parent.children and node is node.parent.children[-1]:
|
||||
node = node.parent
|
||||
return node
|
||||
|
||||
|
||||
def has_sibling_with_type(node: LN, type: int) -> bool:
|
||||
# Check previous siblings
|
||||
sibling = node.prev_sibling
|
||||
while sibling is not None:
|
||||
if sibling.type == type:
|
||||
return True
|
||||
sibling = sibling.prev_sibling
|
||||
|
||||
# Check next siblings
|
||||
sibling = node.next_sibling
|
||||
while sibling is not None:
|
||||
if sibling.type == type:
|
||||
return True
|
||||
sibling = sibling.next_sibling
|
||||
|
||||
return False
|
||||
|
@ -28,16 +28,16 @@ def escape(m: re.Match[str]) -> str:
|
||||
if tail.startswith("x"):
|
||||
hexes = tail[1:]
|
||||
if len(hexes) < 2:
|
||||
raise ValueError(f"invalid hex string escape ('\\{tail}')")
|
||||
raise ValueError("invalid hex string escape ('\\%s')" % tail)
|
||||
try:
|
||||
i = int(hexes, 16)
|
||||
except ValueError:
|
||||
raise ValueError(f"invalid hex string escape ('\\{tail}')") from None
|
||||
raise ValueError("invalid hex string escape ('\\%s')" % tail) from None
|
||||
else:
|
||||
try:
|
||||
i = int(tail, 8)
|
||||
except ValueError:
|
||||
raise ValueError(f"invalid octal string escape ('\\{tail}')") from None
|
||||
raise ValueError("invalid octal string escape ('\\%s')" % tail) from None
|
||||
return chr(i)
|
||||
|
||||
|
||||
|
@ -89,12 +89,18 @@ def backtrack(self) -> Iterator[None]:
|
||||
self.parser.is_backtracking = is_backtracking
|
||||
|
||||
def add_token(self, tok_type: int, tok_val: str, raw: bool = False) -> None:
|
||||
func: Callable[..., Any]
|
||||
if raw:
|
||||
func = self.parser._addtoken
|
||||
else:
|
||||
func = self.parser.addtoken
|
||||
|
||||
for ilabel in self.ilabels:
|
||||
with self.switch_to(ilabel):
|
||||
args = [tok_type, tok_val, self.context]
|
||||
if raw:
|
||||
self.parser._addtoken(ilabel, tok_type, tok_val, self.context)
|
||||
else:
|
||||
self.parser.addtoken(tok_type, tok_val, self.context)
|
||||
args.insert(0, ilabel)
|
||||
func(*args)
|
||||
|
||||
def determine_route(
|
||||
self, value: Optional[str] = None, force: bool = False
|
||||
|
@ -140,7 +140,7 @@ def calcfirst(self, name: str) -> None:
|
||||
if label in self.first:
|
||||
fset = self.first[label]
|
||||
if fset is None:
|
||||
raise ValueError(f"recursion for rule {name!r}")
|
||||
raise ValueError("recursion for rule %r" % name)
|
||||
else:
|
||||
self.calcfirst(label)
|
||||
fset = self.first[label]
|
||||
@ -155,8 +155,8 @@ def calcfirst(self, name: str) -> None:
|
||||
for symbol in itsfirst:
|
||||
if symbol in inverse:
|
||||
raise ValueError(
|
||||
f"rule {name} is ambiguous; {symbol} is in the first sets of"
|
||||
f" {label} as well as {inverse[symbol]}"
|
||||
"rule %s is ambiguous; %s is in the first sets of %s as well"
|
||||
" as %s" % (name, symbol, label, inverse[symbol])
|
||||
)
|
||||
inverse[symbol] = label
|
||||
self.first[name] = totalset
|
||||
@ -237,16 +237,16 @@ def dump_nfa(self, name: str, start: "NFAState", finish: "NFAState") -> None:
|
||||
j = len(todo)
|
||||
todo.append(next)
|
||||
if label is None:
|
||||
print(f" -> {j}")
|
||||
print(" -> %d" % j)
|
||||
else:
|
||||
print(f" {label} -> {j}")
|
||||
print(" %s -> %d" % (label, j))
|
||||
|
||||
def dump_dfa(self, name: str, dfa: Sequence["DFAState"]) -> None:
|
||||
print("Dump of DFA for", name)
|
||||
for i, state in enumerate(dfa):
|
||||
print(" State", i, state.isfinal and "(final)" or "")
|
||||
for label, next in sorted(state.arcs.items()):
|
||||
print(f" {label} -> {dfa.index(next)}")
|
||||
print(" %s -> %d" % (label, dfa.index(next)))
|
||||
|
||||
def simplify_dfa(self, dfa: list["DFAState"]) -> None:
|
||||
# This is not theoretically optimal, but works well enough.
|
||||
@ -330,12 +330,15 @@ def parse_atom(self) -> tuple["NFAState", "NFAState"]:
|
||||
return a, z
|
||||
else:
|
||||
self.raise_error(
|
||||
f"expected (...) or NAME or STRING, got {self.type}/{self.value}"
|
||||
"expected (...) or NAME or STRING, got %s/%s", self.type, self.value
|
||||
)
|
||||
raise AssertionError
|
||||
|
||||
def expect(self, type: int, value: Optional[Any] = None) -> str:
|
||||
if self.type != type or (value is not None and self.value != value):
|
||||
self.raise_error(f"expected {type}/{value}, got {self.type}/{self.value}")
|
||||
self.raise_error(
|
||||
"expected %s/%s, got %s/%s", type, value, self.type, self.value
|
||||
)
|
||||
value = self.value
|
||||
self.gettoken()
|
||||
return value
|
||||
@ -347,7 +350,12 @@ def gettoken(self) -> None:
|
||||
self.type, self.value, self.begin, self.end, self.line = tup
|
||||
# print token.tok_name[self.type], repr(self.value)
|
||||
|
||||
def raise_error(self, msg: str) -> NoReturn:
|
||||
def raise_error(self, msg: str, *args: Any) -> NoReturn:
|
||||
if args:
|
||||
try:
|
||||
msg = msg % args
|
||||
except Exception:
|
||||
msg = " ".join([msg] + list(map(str, args)))
|
||||
raise SyntaxError(
|
||||
msg, (str(self.filename), self.end[0], self.end[1], self.line)
|
||||
)
|
||||
|
@ -113,17 +113,7 @@ def transform_whitespace(
|
||||
and prev_token.type not in (TokenType.nl, TokenType.newline)
|
||||
):
|
||||
token_str = source[token.start_index : token.end_index]
|
||||
if token_str.startswith("\\\r\n"):
|
||||
return pytokens.Token(
|
||||
TokenType.nl,
|
||||
token.start_index,
|
||||
token.start_index + 3,
|
||||
token.start_line,
|
||||
token.start_col,
|
||||
token.start_line,
|
||||
token.start_col + 3,
|
||||
)
|
||||
elif token_str.startswith("\\\n") or token_str.startswith("\\\r"):
|
||||
if token_str.startswith("\\\n"):
|
||||
return pytokens.Token(
|
||||
TokenType.nl,
|
||||
token.start_index,
|
||||
@ -138,13 +128,20 @@ def transform_whitespace(
|
||||
|
||||
|
||||
def tokenize(source: str, grammar: Optional[Grammar] = None) -> Iterator[TokenInfo]:
|
||||
async_keywords = False if grammar is None else grammar.async_keywords
|
||||
|
||||
lines = source.split("\n")
|
||||
lines += [""] # For newline tokens in files that don't end in a newline
|
||||
line, column = 1, 0
|
||||
|
||||
token_iterator = pytokens.tokenize(source)
|
||||
is_async = False
|
||||
current_indent = 0
|
||||
async_indent = 0
|
||||
|
||||
prev_token: Optional[pytokens.Token] = None
|
||||
try:
|
||||
for token in pytokens.tokenize(source):
|
||||
for token in token_iterator:
|
||||
token = transform_whitespace(token, source, prev_token)
|
||||
|
||||
line, column = token.start_line, token.start_col
|
||||
@ -159,18 +156,58 @@ def tokenize(source: str, grammar: Optional[Grammar] = None) -> Iterator[TokenIn
|
||||
prev_token = token
|
||||
continue
|
||||
|
||||
if token.type == TokenType.indent:
|
||||
current_indent += 1
|
||||
if token.type == TokenType.dedent:
|
||||
current_indent -= 1
|
||||
if is_async and current_indent < async_indent:
|
||||
is_async = False
|
||||
|
||||
source_line = lines[token.start_line - 1]
|
||||
|
||||
if token.type == TokenType.identifier and token_str in ("async", "await"):
|
||||
# Black uses `async` and `await` token types just for those two keywords
|
||||
while True:
|
||||
next_token = next(token_iterator)
|
||||
next_str = source[next_token.start_index : next_token.end_index]
|
||||
next_token = transform_whitespace(next_token, next_str, token)
|
||||
if next_token.type == TokenType.whitespace:
|
||||
continue
|
||||
break
|
||||
|
||||
next_token_type = TOKEN_TYPE_MAP[next_token.type]
|
||||
next_line = lines[next_token.start_line - 1]
|
||||
|
||||
if token_str == "async" and (
|
||||
async_keywords
|
||||
or (next_token_type == NAME and next_str in ("def", "for"))
|
||||
):
|
||||
is_async = True
|
||||
async_indent = current_indent + 1
|
||||
current_token_type = ASYNC
|
||||
elif token_str == "await" and (async_keywords or is_async):
|
||||
current_token_type = AWAIT
|
||||
else:
|
||||
current_token_type = TOKEN_TYPE_MAP[token.type]
|
||||
|
||||
yield (
|
||||
ASYNC if token_str == "async" else AWAIT,
|
||||
current_token_type,
|
||||
token_str,
|
||||
(token.start_line, token.start_col),
|
||||
(token.end_line, token.end_col),
|
||||
source_line,
|
||||
)
|
||||
elif token.type == TokenType.op and token_str == "...":
|
||||
yield (
|
||||
next_token_type,
|
||||
next_str,
|
||||
(next_token.start_line, next_token.start_col),
|
||||
(next_token.end_line, next_token.end_col),
|
||||
next_line,
|
||||
)
|
||||
prev_token = token
|
||||
continue
|
||||
|
||||
if token.type == TokenType.op and token_str == "...":
|
||||
# Black doesn't have an ellipsis token yet, yield 3 DOTs instead
|
||||
assert token.start_line == token.end_line
|
||||
assert token.end_col == token.start_col + 3
|
||||
@ -185,7 +222,9 @@ def tokenize(source: str, grammar: Optional[Grammar] = None) -> Iterator[TokenIn
|
||||
(token.end_line, end_col),
|
||||
source_line,
|
||||
)
|
||||
else:
|
||||
prev_token = token
|
||||
continue
|
||||
|
||||
yield (
|
||||
TOKEN_TYPE_MAP[token.type],
|
||||
token_str,
|
||||
@ -206,7 +245,9 @@ def printtoken(
|
||||
) -> None: # for testing
|
||||
(srow, scol) = srow_col
|
||||
(erow, ecol) = erow_col
|
||||
print(f"{srow},{scol}-{erow},{ecol}:\t{tok_name[type]}\t{token!r}")
|
||||
print(
|
||||
"%d,%d-%d,%d:\t%s\t%s" % (srow, scol, erow, ecol, tok_name[type], repr(token))
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": # testing
|
||||
|
@ -268,7 +268,11 @@ def __init__(
|
||||
def __repr__(self) -> str:
|
||||
"""Return a canonical string representation."""
|
||||
assert self.type is not None
|
||||
return f"{self.__class__.__name__}({type_repr(self.type)}, {self.children!r})"
|
||||
return "{}({}, {!r})".format(
|
||||
self.__class__.__name__,
|
||||
type_repr(self.type),
|
||||
self.children,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
@ -417,9 +421,10 @@ def __repr__(self) -> str:
|
||||
from .pgen2.token import tok_name
|
||||
|
||||
assert self.type is not None
|
||||
return (
|
||||
f"{self.__class__.__name__}({tok_name.get(self.type, self.type)},"
|
||||
f" {self.value!r})"
|
||||
return "{}({}, {!r})".format(
|
||||
self.__class__.__name__,
|
||||
tok_name.get(self.type, self.type),
|
||||
self.value,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
@ -522,7 +527,7 @@ def __repr__(self) -> str:
|
||||
args = [type_repr(self.type), self.content, self.name]
|
||||
while args and args[-1] is None:
|
||||
del args[-1]
|
||||
return f"{self.__class__.__name__}({', '.join(map(repr, args))})"
|
||||
return "{}({})".format(self.__class__.__name__, ", ".join(map(repr, args)))
|
||||
|
||||
def _submatch(self, node, results=None) -> bool:
|
||||
raise NotImplementedError
|
||||
|
@ -89,26 +89,6 @@ async def func():
|
||||
with (x, y) as z:
|
||||
pass
|
||||
|
||||
|
||||
# don't remove the brackets here, it changes the meaning of the code.
|
||||
# even though the code will always trigger a runtime error
|
||||
with (name_5, name_4), name_5:
|
||||
pass
|
||||
|
||||
|
||||
def test_tuple_as_contextmanager():
|
||||
from contextlib import nullcontext
|
||||
|
||||
try:
|
||||
with (nullcontext(),nullcontext()),nullcontext():
|
||||
pass
|
||||
except TypeError:
|
||||
# test passed
|
||||
pass
|
||||
else:
|
||||
# this should be a type error
|
||||
assert False
|
||||
|
||||
# output
|
||||
|
||||
|
||||
@ -202,23 +182,3 @@ async def func():
|
||||
# don't remove the brackets here, it changes the meaning of the code.
|
||||
with (x, y) as z:
|
||||
pass
|
||||
|
||||
|
||||
# don't remove the brackets here, it changes the meaning of the code.
|
||||
# even though the code will always trigger a runtime error
|
||||
with (name_5, name_4), name_5:
|
||||
pass
|
||||
|
||||
|
||||
def test_tuple_as_contextmanager():
|
||||
from contextlib import nullcontext
|
||||
|
||||
try:
|
||||
with (nullcontext(), nullcontext()), nullcontext():
|
||||
pass
|
||||
except TypeError:
|
||||
# test passed
|
||||
pass
|
||||
else:
|
||||
# this should be a type error
|
||||
assert False
|
||||
|
@ -10,7 +10,6 @@ def g():
|
||||
|
||||
|
||||
async def func():
|
||||
await ...
|
||||
if test:
|
||||
out_batched = [
|
||||
i
|
||||
@ -43,7 +42,6 @@ def g():
|
||||
|
||||
|
||||
async def func():
|
||||
await ...
|
||||
if test:
|
||||
out_batched = [
|
||||
i
|
||||
|
@ -119,7 +119,7 @@ def __init__(self) -> None:
|
||||
if Version(imp_version("click")) >= Version("8.2.0"):
|
||||
super().__init__()
|
||||
else:
|
||||
super().__init__(mix_stderr=False) # type: ignore
|
||||
super().__init__(mix_stderr=False)
|
||||
|
||||
|
||||
def invokeBlack(
|
||||
@ -422,6 +422,21 @@ def test_skip_magic_trailing_comma(self) -> None:
|
||||
)
|
||||
self.assertEqual(expected, actual, msg)
|
||||
|
||||
@patch("black.dump_to_file", dump_to_stderr)
|
||||
def test_async_as_identifier(self) -> None:
|
||||
source_path = get_case_path("miscellaneous", "async_as_identifier")
|
||||
_, source, expected = read_data_from_file(source_path)
|
||||
actual = fs(source)
|
||||
self.assertFormatEqual(expected, actual)
|
||||
major, minor = sys.version_info[:2]
|
||||
if major < 3 or (major <= 3 and minor < 7):
|
||||
black.assert_equivalent(source, actual)
|
||||
black.assert_stable(source, actual, DEFAULT_MODE)
|
||||
# ensure black can parse this when the target is 3.6
|
||||
self.invokeBlack([str(source_path), "--target-version", "py36"])
|
||||
# but not on 3.7, because async/await is no longer an identifier
|
||||
self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
|
||||
|
||||
@patch("black.dump_to_file", dump_to_stderr)
|
||||
def test_python37(self) -> None:
|
||||
source_path = get_case_path("cases", "python37")
|
||||
@ -434,6 +449,8 @@ def test_python37(self) -> None:
|
||||
black.assert_stable(source, actual, DEFAULT_MODE)
|
||||
# ensure black can parse this when the target is 3.7
|
||||
self.invokeBlack([str(source_path), "--target-version", "py37"])
|
||||
# but not on 3.6, because we use async as a reserved keyword
|
||||
self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
|
||||
|
||||
def test_tab_comment_indentation(self) -> None:
|
||||
contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
|
||||
@ -1890,8 +1907,7 @@ def test_code_option_safe(self) -> None:
|
||||
args = ["--safe", "--code", code]
|
||||
result = CliRunner().invoke(black.main, args)
|
||||
|
||||
assert error_msg == result.output
|
||||
assert result.exit_code == 123
|
||||
self.compare_results(result, error_msg, 123)
|
||||
|
||||
def test_code_option_fast(self) -> None:
|
||||
"""Test that the code option ignores errors when the sanity checks fail."""
|
||||
@ -2048,26 +2064,6 @@ def test_lines_with_leading_tabs_expanded(self) -> None:
|
||||
assert lines_with_leading_tabs_expanded("\t\tx") == [f"{tab}{tab}x"]
|
||||
assert lines_with_leading_tabs_expanded("\tx\n y") == [f"{tab}x", " y"]
|
||||
|
||||
def test_backslash_carriage_return(self) -> None:
|
||||
# These tests are here instead of in the normal cases because
|
||||
# of git's newline normalization and because it's hard to
|
||||
# get `\r` vs `\r\n` vs `\n` to display properly in editors
|
||||
assert black.format_str("x=\\\r\n1", mode=black.FileMode()) == "x = 1\n"
|
||||
assert black.format_str("x=\\\n1", mode=black.FileMode()) == "x = 1\n"
|
||||
assert black.format_str("x=\\\r1", mode=black.FileMode()) == "x = 1\n"
|
||||
assert (
|
||||
black.format_str("class A\\\r\n:...", mode=black.FileMode())
|
||||
== "class A: ...\n"
|
||||
)
|
||||
assert (
|
||||
black.format_str("class A\\\n:...", mode=black.FileMode())
|
||||
== "class A: ...\n"
|
||||
)
|
||||
assert (
|
||||
black.format_str("class A\\\r:...", mode=black.FileMode())
|
||||
== "class A: ...\n"
|
||||
)
|
||||
|
||||
|
||||
class TestCaching:
|
||||
def test_get_cache_dir(
|
||||
|
24
tox.ini
24
tox.ini
@ -13,16 +13,18 @@ skip_install = True
|
||||
recreate = True
|
||||
deps =
|
||||
-r{toxinidir}/test_requirements.txt
|
||||
; parallelization is disabled on CI because pytest-dev/pytest-xdist#620 occurs too frequently
|
||||
; local runs can stay parallelized since they aren't rolling the dice so many times as like on CI
|
||||
commands =
|
||||
pip install -e .[d]
|
||||
coverage erase
|
||||
pytest tests --run-optional no_jupyter \
|
||||
--numprocesses auto \
|
||||
!ci: --numprocesses auto \
|
||||
--cov {posargs}
|
||||
pip install -e .[jupyter]
|
||||
pytest tests --run-optional jupyter \
|
||||
-m jupyter \
|
||||
--numprocesses auto \
|
||||
!ci: --numprocesses auto \
|
||||
--cov --cov-append {posargs}
|
||||
coverage report
|
||||
|
||||
@ -32,15 +34,20 @@ skip_install = True
|
||||
recreate = True
|
||||
deps =
|
||||
-r{toxinidir}/test_requirements.txt
|
||||
; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317
|
||||
; this seems to cause tox to wait forever
|
||||
; remove this when pypy releases the bugfix
|
||||
commands =
|
||||
pip install -e .[d]
|
||||
pytest tests \
|
||||
--run-optional no_jupyter \
|
||||
--numprocesses auto
|
||||
!ci: --numprocesses auto \
|
||||
ci: --numprocesses 1
|
||||
pip install -e .[jupyter]
|
||||
pytest tests --run-optional jupyter \
|
||||
-m jupyter \
|
||||
--numprocesses auto
|
||||
!ci: --numprocesses auto \
|
||||
ci: --numprocesses 1
|
||||
|
||||
[testenv:{,ci-}311]
|
||||
setenv =
|
||||
@ -52,17 +59,22 @@ deps =
|
||||
; We currently need > aiohttp 3.8.1 that is on PyPI for 3.11
|
||||
git+https://github.com/aio-libs/aiohttp
|
||||
-r{toxinidir}/test_requirements.txt
|
||||
; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317
|
||||
; this seems to cause tox to wait forever
|
||||
; remove this when pypy releases the bugfix
|
||||
commands =
|
||||
pip install -e .[d]
|
||||
coverage erase
|
||||
pytest tests \
|
||||
--run-optional no_jupyter \
|
||||
--numprocesses auto \
|
||||
!ci: --numprocesses auto \
|
||||
ci: --numprocesses 1 \
|
||||
--cov {posargs}
|
||||
pip install -e .[jupyter]
|
||||
pytest tests --run-optional jupyter \
|
||||
-m jupyter \
|
||||
--numprocesses auto \
|
||||
!ci: --numprocesses auto \
|
||||
ci: --numprocesses 1 \
|
||||
--cov --cov-append {posargs}
|
||||
coverage report
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user