from multiprocessing import Manager
import os
from pathlib import Path
+import re
import tokenize
import signal
import sys
from blib2to3.pgen2 import driver, token
from blib2to3.pgen2.parse import ParseError
-__version__ = "18.3a4"
+__version__ = "18.4a0"
DEFAULT_LINE_LENGTH = 88
# types
syms = pygram.python_symbols
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",
check: bool,
diff: bool,
fast: bool,
+ quiet: bool,
src: List[str],
) -> None:
"""The uncompromising code formatter."""
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:
return_code = loop.run_until_complete(
schedule_formatting(
- sources, line_length, write_back, fast, loop, executor
+ sources, line_length, write_back, fast, quiet, loop, executor
)
)
finally:
line_length: int,
write_back: WriteBack,
fast: bool,
+ quiet: bool,
loop: BaseEventLoop,
executor: Executor,
) -> int:
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")
report.done(src, task.result())
if cancelled:
await asyncio.gather(*cancelled, loop=loop, return_exceptions=True)
- else:
+ elif not quiet:
out("All done! ✨ 🍰 ✨")
- click.echo(str(report))
+ if not quiet:
+ click.echo(str(report))
return report.return_code
leaf.opening_bracket = opening_bracket
leaf.bracket_depth = self.depth
if self.depth == 0:
- after_delim = is_split_after_delimiter(leaf, self.previous)
- before_delim = is_split_before_delimiter(leaf, self.previous)
- if after_delim > before_delim:
- self.delimiters[id(leaf)] = after_delim
- elif before_delim > after_delim and self.previous is not None:
- self.delimiters[id(self.previous)] = before_delim
+ delim = is_split_before_delimiter(leaf, self.previous)
+ if delim and self.previous is not None:
+ self.delimiters[id(self.previous)] = delim
+ else:
+ delim = is_split_after_delimiter(leaf, self.previous)
+ if delim:
+ self.delimiters[id(leaf)] = delim
if leaf.type in OPENING_BRACKETS:
self.bracket_match[self.depth, BRACKET[leaf.type]] = leaf
self.depth += 1
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
if leaf.type == token.COMMA:
return COMMA_PRIORITY
- if (
- leaf.type in VARARGS
- and leaf.parent
- and leaf.parent.type in {syms.argument, syms.typedargslist}
- ):
- return MATH_PRIORITY
-
return 0
Higher numbers are higher priority.
"""
+ if (
+ leaf.type in VARARGS
+ and leaf.parent
+ and leaf.parent.type in {syms.argument, syms.typedargslist}
+ ):
+ # * and ** might also be MATH_OPERATORS but in this case they are not.
+ # Don't treat them as a delimiter.
+ return 0
+
if (
leaf.type in MATH_OPERATORS
and leaf.parent
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)
nlines = 0
if first_quote_pos == -1:
return # There's an internal error
+ prefix = leaf.value[:first_quote_pos]
body = leaf.value[first_quote_pos + len(orig_quote):-len(orig_quote)]
- new_body = body.replace(f"\\{orig_quote}", orig_quote).replace(
- new_quote, f"\\{new_quote}"
- )
+ unescaped_new_quote = re.compile(r"(([^\\]|^)(\\\\)*)" + new_quote)
+ escaped_orig_quote = re.compile(r"\\(\\\\)*" + orig_quote)
+ if "r" in prefix.casefold():
+ if unescaped_new_quote.search(body):
+ # There's at least one unescaped new_quote in this raw string
+ # so converting is impossible
+ return
+
+ # Do not introduce or remove backslashes in raw strings
+ new_body = body
+ else:
+ new_body = escaped_orig_quote.sub(f"\\1{orig_quote}", body)
+ new_body = unescaped_new_quote.sub(f"\\1\\\\{new_quote}", new_body)
if new_quote == '"""' and new_body[-1] == '"':
# edge case:
new_body = new_body[:-1] + '\\"'
if new_escape_count == orig_escape_count and orig_quote == '"':
return # Prefer double quotes
- prefix = leaf.value[:first_quote_pos]
leaf.value = f"{prefix}{new_quote}{new_body}{new_quote}"
class Report:
"""Provides a reformatting counter. Can be rendered with `str(report)`."""
check: bool = False
+ quiet: bool = False
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"
- out(f"{reformatted} {src}")
+ if not self.quiet:
+ out(f"{reformatted} {src}")
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: