]>
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,
help="If --fast given, skip temporary sanity checks. [default: --safe]",
)
@click.option(
"--fast/--safe",
is_flag=True,
help="If --fast given, skip temporary sanity checks. [default: --safe]",
)
+@click.option(
+ "-q",
+ "--quiet",
+ is_flag=True,
+ help=(
+ "Don't emit non-error messages to stderr. Errors are still emitted, "
+ "silence those with 2>/dev/null."
+ ),
+)
@click.version_option(version=__version__)
@click.argument(
"src",
@click.version_option(version=__version__)
@click.argument(
"src",
)
@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,
+ quiet: 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:
p = sources[0]
if len(sources) == 0:
ctx.exit(0)
elif len(sources) == 1:
p = sources[0]
- report = Report(check=check)
+ report = Report(check=check, quiet=quiet )
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 check, fas t, loop, executor
+ sources, line_length, write_back, fast, quie t, loop, executor
async def schedule_formatting(
sources: List[Path],
line_length: int,
async def schedule_formatting(
sources: List[Path],
line_length: int,
loop: BaseEventLoop,
executor: Executor,
) -> int:
loop: BaseEventLoop,
executor: Executor,
) -> int:
`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
loop.add_signal_handler(signal.SIGTERM, cancel, _task_values)
await asyncio.wait(tasks.values())
cancelled = []
loop.add_signal_handler(signal.SIGTERM, cancel, _task_values)
await asyncio.wait(tasks.values())
cancelled = []
- report = Report(check=not write_back )
+ report = Report(check=write_back is WriteBack.NO, quiet=quiet )
for src, task in tasks.items():
if not task.done():
report.failed(src, "timed out, cancelling")
for src, task in tasks.items():
if not task.done():
report.failed(src, "timed out, cancelling")
report.done(src, task.result())
if cancelled:
await asyncio.gather(*cancelled, loop=loop, return_exceptions=True)
report.done(src, task.result())
if cancelled:
await asyncio.gather(*cancelled, loop=loop, return_exceptions=True)
- click.echo(str(report))
+ if not quiet:
+ click.echo(str(report))
return report.return_code
def format_file_in_place(
return report.return_code
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
class Report:
"""Provides a reformatting counter. Can be rendered with `str(report)`."""
check: bool = False
class Report:
"""Provides a reformatting counter. Can be rendered with `str(report)`."""
check: bool = False
change_count: int = 0
same_count: int = 0
failure_count: int = 0
change_count: int = 0
same_count: int = 0
failure_count: int = 0
"""Increment the counter for successful reformatting. Write out a message."""
if changed:
reformatted = "would reformat" if self.check else "reformatted"
"""Increment the counter for successful reformatting. Write out a message."""
if changed:
reformatted = "would reformat" if self.check else "reformatted"
- out(f"{reformatted} {src}")
+ if not self.quiet:
+ out(f"{reformatted} {src}")
self.change_count += 1
else:
self.change_count += 1
else:
- out(f"{src} already well formatted, good job.", bold=False)
+ if not self.quiet:
+ out(f"{src} already well formatted, good job.", bold=False)
self.same_count += 1
def failed(self, src: Path, message: str) -> None:
self.same_count += 1
def failed(self, src: Path, message: str) -> None:
) 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")