]>
git.madduck.net Git - etc/vim.git/blobdiff - black.py
madduck's git repository
Every one of the projects in this repository is available at the canonical
URL git://git.madduck.net/madduck/pub/<projectpath> — see
each project's metadata for the exact URL.
All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@ git. madduck. net .
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
SSH access, as well as push access can be individually
arranged .
If you use my repositories frequently, consider adding the following
snippet to ~/.gitconfig and using the third clone URL listed for each
project:
[url "git://git.madduck.net/madduck/"]
insteadOf = madduck:
import asyncio
from asyncio.base_events import BaseEventLoop
from concurrent.futures import Executor, ProcessPoolExecutor
import asyncio
from asyncio.base_events import BaseEventLoop
from concurrent.futures import Executor, ProcessPoolExecutor
from functools import partial, wraps
import keyword
import logging
from functools import partial, wraps
import keyword
import logging
+from multiprocessing import Manager
import os
from pathlib import Path
import tokenize
import signal
import sys
from typing import (
import os
from pathlib import Path
import tokenize
import signal
import sys
from typing import (
from blib2to3.pgen2 import driver, token
from blib2to3.pgen2.parse import ParseError
from blib2to3.pgen2 import driver, token
from blib2to3.pgen2.parse import ParseError
DEFAULT_LINE_LENGTH = 88
# types
syms = pygram.python_symbols
DEFAULT_LINE_LENGTH = 88
# types
syms = pygram.python_symbols
"""Found a comment like `# fmt: off` in the file."""
"""Found a comment like `# fmt: off` in the file."""
+class WriteBack(Enum):
+ NO = 0
+ YES = 1
+ DIFF = 2
+
+
@click.command()
@click.option(
"-l",
@click.command()
@click.option(
"-l",
"--check",
is_flag=True,
help=(
"--check",
is_flag=True,
help=(
- "Don't write back the files , just return the status. Return code 0 "
+ "Don't write the files back , just return the status. Return code 0 "
"means nothing would change. Return code 1 means some files would be "
"reformatted. Return code 123 means there was an internal error."
),
)
"means nothing would change. Return code 1 means some files would be "
"reformatted. Return code 123 means there was an internal error."
),
)
+@click.option(
+ "--diff",
+ is_flag=True,
+ help="Don't write the files back, just output a diff for each file on stdout.",
+)
@click.option(
"--fast/--safe",
is_flag=True,
@click.option(
"--fast/--safe",
is_flag=True,
)
@click.pass_context
def main(
)
@click.pass_context
def main(
- ctx: click.Context, line_length: int, check: bool, fast: bool, src: List[str]
+ ctx: click.Context,
+ line_length: int,
+ check: bool,
+ diff: bool,
+ fast: bool,
+ src: List[str],
) -> None:
"""The uncompromising code formatter."""
sources: List[Path] = []
) -> None:
"""The uncompromising code formatter."""
sources: List[Path] = []
sources.append(Path("-"))
else:
err(f"invalid path: {s}")
sources.append(Path("-"))
else:
err(f"invalid path: {s}")
+ if check and diff:
+ exc = click.ClickException("Options --check and --diff are mutually exclusive")
+ exc.exit_code = 2
+ raise exc
+
+ if check:
+ write_back = WriteBack.NO
+ elif diff:
+ write_back = WriteBack.DIFF
+ else:
+ write_back = WriteBack.YES
if len(sources) == 0:
ctx.exit(0)
elif len(sources) == 1:
if len(sources) == 0:
ctx.exit(0)
elif len(sources) == 1:
try:
if not p.is_file() and str(p) == "-":
changed = format_stdin_to_stdout(
try:
if not p.is_file() and str(p) == "-":
changed = format_stdin_to_stdout(
- line_length=line_length, fast=fast, write_back=not che ck
+ line_length=line_length, fast=fast, write_back=write_ba ck
)
else:
changed = format_file_in_place(
)
else:
changed = format_file_in_place(
- p, line_length=line_length, fast=fast, write_back=not che ck
+ p, line_length=line_length, fast=fast, write_back=write_ba ck
)
report.done(p, changed)
except Exception as exc:
)
report.done(p, changed)
except Exception as exc:
try:
return_code = loop.run_until_complete(
schedule_formatting(
try:
return_code = loop.run_until_complete(
schedule_formatting(
- sources, line_length, not che ck, fast, loop, executor
+ sources, line_length, write_ba ck, fast, loop, executor
async def schedule_formatting(
sources: List[Path],
line_length: int,
async def schedule_formatting(
sources: List[Path],
line_length: int,
fast: bool,
loop: BaseEventLoop,
executor: Executor,
fast: bool,
loop: BaseEventLoop,
executor: Executor,
`line_length`, `write_back`, and `fast` options are passed to
:func:`format_file_in_place`.
"""
`line_length`, `write_back`, and `fast` options are passed to
:func:`format_file_in_place`.
"""
+ lock = None
+ if write_back == WriteBack.DIFF:
+ # For diff output, we need locks to ensure we don't interleave output
+ # from different processes.
+ manager = Manager()
+ lock = manager.Lock()
tasks = {
src: loop.run_in_executor(
tasks = {
src: loop.run_in_executor(
- executor, format_file_in_place, src, line_length, fast, write_back
+ executor, format_file_in_place, src, line_length, fast, write_back, lock
def format_file_in_place(
def format_file_in_place(
- src: Path, line_length: int, fast: bool, write_back: bool = False
+ src: Path,
+ line_length: int,
+ fast: bool,
+ write_back: WriteBack = WriteBack.NO,
+ lock: Any = None, # multiprocessing.Manager().Lock() is some crazy proxy
) -> bool:
"""Format file under `src` path. Return True if changed.
) -> bool:
"""Format file under `src` path. Return True if changed.
with tokenize.open(src) as src_buffer:
src_contents = src_buffer.read()
try:
with tokenize.open(src) as src_buffer:
src_contents = src_buffer.read()
try:
- contents = format_file_contents(
+ dst_ contents = format_file_contents(
src_contents, line_length=line_length, fast=fast
)
except NothingChanged:
return False
src_contents, line_length=line_length, fast=fast
)
except NothingChanged:
return False
+ if write_back == write_back.YES :
with open(src, "w", encoding=src_buffer.encoding) as f:
with open(src, "w", encoding=src_buffer.encoding) as f:
+ f.write(dst_contents)
+ elif write_back == write_back.DIFF:
+ src_name = f"{src.name} (original)"
+ dst_name = f"{src.name} (formatted)"
+ diff_contents = diff(src_contents, dst_contents, src_name, dst_name)
+ if lock:
+ lock.acquire()
+ try:
+ sys.stdout.write(diff_contents)
+ finally:
+ if lock:
+ lock.release()
return True
def format_stdin_to_stdout(
return True
def format_stdin_to_stdout(
- line_length: int, fast: bool, write_back: bool = False
+ line_length: int, fast: bool, write_back: WriteBack = WriteBack.NO
) -> bool:
"""Format file on stdin. Return True if changed.
If `write_back` is True, write reformatted code back to stdout.
`line_length` and `fast` arguments are passed to :func:`format_file_contents`.
"""
) -> bool:
"""Format file on stdin. Return True if changed.
If `write_back` is True, write reformatted code back to stdout.
`line_length` and `fast` arguments are passed to :func:`format_file_contents`.
"""
- contents = sys.stdin.read()
- contents = format_file_contents(contents , line_length=line_length, fast=fast)
+ dst = format_file_contents(src , line_length=line_length, fast=fast)
return True
except NothingChanged:
return True
except NothingChanged:
- if write_back:
- sys.stdout.write(contents)
+ if write_back == WriteBack.YES:
+ sys.stdout.write(dst)
+ elif write_back == WriteBack.DIFF:
+ src_name = "<stdin> (original)"
+ dst_name = "<stdin> (formatted)"
+ sys.stdout.write(diff(src, dst, src_name, dst_name))
def format_file_contents(
def format_file_contents(
yield from self.line()
yield from self.visit(node)
yield from self.line()
yield from self.visit(node)
+ if node.type == token.ENDMARKER:
+ # somebody decided not to put a final `# fmt: on`
+ yield from self.line()
+
def __attrs_post_init__(self) -> None:
"""You are in a twisty little maze of passages."""
v = self.visit_stmt
def __attrs_post_init__(self) -> None:
"""You are in a twisty little maze of passages."""
v = self.visit_stmt
raise FormatOn(consumed)
if comment in {"# fmt: off", "# yapf: disable"}:
raise FormatOn(consumed)
if comment in {"# fmt: off", "# yapf: disable"}:
- raise FormatOff(consumed)
+ if comment_type == STANDALONE_COMMENT:
+ raise FormatOff(consumed)
+
+ prev = preceding_leaf(leaf)
+ if not prev or prev.type in WHITESPACE: # standalone comment in disguise
+ raise FormatOff(consumed)
current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
if current_line:
if (
current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
if current_line:
if (
- delimiter_priority == COMMA_PRIORITY
+ trailing_comma_safe
+ and delimiter_priority == COMMA_PRIORITY
and current_line.leaves[-1].type != token.COMMA
and current_line.leaves[-1].type != token.COMMA
- and trailing_comma_safe
+ and current_line.leaves[-1].type != STANDALONE_COMMENT
):
current_line.append(Leaf(token.COMMA, ","))
yield current_line
):
current_line.append(Leaf(token.COMMA, ","))
yield current_line
def normalize_string_quotes(leaf: Leaf) -> None:
def normalize_string_quotes(leaf: Leaf) -> None:
+ """Prefer double quotes but only if it doesn't cause more escaping.
+
+ Adds or removes backslashes as appropriate. Doesn't parse and fix
+ strings nested in f-strings (yet).
+
+ Note: Mutates its argument.
+ """
value = leaf.value.lstrip("furbFURB")
if value[:3] == '"""':
return
value = leaf.value.lstrip("furbFURB")
if value[:3] == '"""':
return
) as f:
for lines in output:
f.write(lines)
) as f:
for lines in output:
f.write(lines)
+ if lines and lines[-1] != "\n":
+ f.write("\n")