Normalize string quotes (#75)

* Normalize string quotes

Convert single-quoted strings to double-quoted. Convert triple single-quoted strings to triple double-quoted. Do not touch any strings where conversion would increase the number of backslashes.

Fixes #51.

* reformat Black itself
This commit is contained in:
Zsolt Dollenstein 2018-03-31 19:21:25 +01:00 committed by Łukasz Langa
parent 4dfec562ed
commit 80bd2b3134
14 changed files with 350 additions and 266 deletions

253
black.py
View File

@ -47,9 +47,9 @@
Priority = int
Index = int
LN = Union[Leaf, Node]
SplitFunc = Callable[['Line', bool], Iterator['Line']]
SplitFunc = Callable[["Line", bool], Iterator["Line"]]
out = partial(click.secho, bold=True, err=True)
err = partial(click.secho, fg='red', err=True)
err = partial(click.secho, fg="red", err=True)
class NothingChanged(UserWarning):
@ -94,15 +94,15 @@ class FormatOff(FormatError):
@click.command()
@click.option(
'-l',
'--line-length',
"-l",
"--line-length",
type=int,
default=DEFAULT_LINE_LENGTH,
help='How many character per line to allow.',
help="How many character per line to allow.",
show_default=True,
)
@click.option(
'--check',
"--check",
is_flag=True,
help=(
"Don't write back the files, just return the status. Return code 0 "
@ -111,13 +111,13 @@ class FormatOff(FormatError):
),
)
@click.option(
'--fast/--safe',
"--fast/--safe",
is_flag=True,
help='If --fast given, skip temporary sanity checks. [default: --safe]',
help="If --fast given, skip temporary sanity checks. [default: --safe]",
)
@click.version_option(version=__version__)
@click.argument(
'src',
"src",
nargs=-1,
type=click.Path(
exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True
@ -136,17 +136,17 @@ def main(
elif p.is_file():
# if a file was explicitly given, we don't care about its extension
sources.append(p)
elif s == '-':
sources.append(Path('-'))
elif s == "-":
sources.append(Path("-"))
else:
err(f'invalid path: {s}')
err(f"invalid path: {s}")
if len(sources) == 0:
ctx.exit(0)
elif len(sources) == 1:
p = sources[0]
report = Report(check=check)
try:
if not p.is_file() and str(p) == '-':
if not p.is_file() and str(p) == "-":
changed = format_stdin_to_stdout(
line_length=line_length, fast=fast, write_back=not check
)
@ -202,7 +202,7 @@ async def schedule_formatting(
report = Report(check=not write_back)
for src, task in tasks.items():
if not task.done():
report.failed(src, 'timed out, cancelling')
report.failed(src, "timed out, cancelling")
task.cancel()
cancelled.append(task)
elif task.cancelled():
@ -214,7 +214,7 @@ async def schedule_formatting(
if cancelled:
await asyncio.gather(*cancelled, loop=loop, return_exceptions=True)
else:
out('All done! ✨ 🍰 ✨')
out("All done! ✨ 🍰 ✨")
click.echo(str(report))
return report.return_code
@ -272,7 +272,7 @@ def format_file_contents(
valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
`line_length` is passed to :func:`format_str`.
"""
if src_contents.strip() == '':
if src_contents.strip() == "":
raise NothingChanged
dst_contents = format_str(src_contents, line_length=line_length)
@ -319,8 +319,8 @@ def format_str(src_contents: str, line_length: int) -> FileContent:
def lib2to3_parse(src_txt: str) -> Node:
"""Given a string with source, return the lib2to3 Node."""
grammar = pygram.python_grammar_no_print_statement
if src_txt[-1] != '\n':
nl = '\r\n' if '\r\n' in src_txt[:1024] else '\n'
if src_txt[-1] != "\n":
nl = "\r\n" if "\r\n" in src_txt[:1024] else "\n"
src_txt += nl
for grammar in GRAMMARS:
drv = driver.Driver(grammar, pytree.convert)
@ -350,7 +350,7 @@ def lib2to3_unparse(node: Node) -> str:
return code
T = TypeVar('T')
T = TypeVar("T")
class Visitor(Generic[T]):
@ -370,7 +370,7 @@ def visit(self, node: LN) -> Iterator[T]:
name = token.tok_name[node.type]
else:
name = type_repr(node.type)
yield from getattr(self, f'visit_{name}', self.visit_default)(node)
yield from getattr(self, f"visit_{name}", self.visit_default)(node)
def visit_default(self, node: LN) -> Iterator[T]:
"""Default `visit_*()` implementation. Recurses to children of `node`."""
@ -384,24 +384,24 @@ class DebugVisitor(Visitor[T]):
tree_depth: int = 0
def visit_default(self, node: LN) -> Iterator[T]:
indent = ' ' * (2 * self.tree_depth)
indent = " " * (2 * self.tree_depth)
if isinstance(node, Node):
_type = type_repr(node.type)
out(f'{indent}{_type}', fg='yellow')
out(f"{indent}{_type}", fg="yellow")
self.tree_depth += 1
for child in node.children:
yield from self.visit(child)
self.tree_depth -= 1
out(f'{indent}/{_type}', fg='yellow', bold=False)
out(f"{indent}/{_type}", fg="yellow", bold=False)
else:
_type = token.tok_name.get(node.type, str(node.type))
out(f'{indent}{_type}', fg='blue', nl=False)
out(f"{indent}{_type}", fg="blue", nl=False)
if node.prefix:
# We don't have to handle prefixes for `Node` objects since
# that delegates to the first child anyway.
out(f' {node.prefix!r}', fg='green', bold=False, nl=False)
out(f' {node.value!r}', fg='blue', bold=False)
out(f" {node.prefix!r}", fg="green", bold=False, nl=False)
out(f" {node.value!r}", fg="blue", bold=False)
@classmethod
def show(cls, code: str) -> None:
@ -415,7 +415,7 @@ def show(cls, code: str) -> None:
KEYWORDS = set(keyword.kwlist)
WHITESPACE = {token.DEDENT, token.INDENT, token.NEWLINE}
FLOW_CONTROL = {'return', 'raise', 'break', 'continue'}
FLOW_CONTROL = {"return", "raise", "break", "continue"}
STATEMENT = {
syms.if_stmt,
syms.while_stmt,
@ -427,7 +427,7 @@ def show(cls, code: str) -> None:
syms.classdef,
}
STANDALONE_COMMENT = 153
LOGIC_OPERATORS = {'and', 'or'}
LOGIC_OPERATORS = {"and", "or"}
COMPARATORS = {
token.LESS,
token.GREATER,
@ -500,14 +500,14 @@ def mark(self, leaf: Leaf) -> None:
self.delimiters[id(self.previous)] = STRING_PRIORITY
elif (
leaf.type == token.NAME
and leaf.value == 'for'
and leaf.value == "for"
and leaf.parent
and leaf.parent.type in {syms.comp_for, syms.old_comp_for}
):
self.delimiters[id(self.previous)] = COMPREHENSION_PRIORITY
elif (
leaf.type == token.NAME
and leaf.value == 'if'
and leaf.value == "if"
and leaf.parent
and leaf.parent.type in {syms.comp_if, syms.old_comp_if}
):
@ -612,7 +612,7 @@ def is_class(self) -> bool:
return (
bool(self)
and self.leaves[0].type == token.NAME
and self.leaves[0].value == 'class'
and self.leaves[0].value == "class"
)
@property
@ -628,12 +628,12 @@ def is_def(self) -> bool:
except IndexError:
second_leaf = None
return (
(first_leaf.type == token.NAME and first_leaf.value == 'def')
(first_leaf.type == token.NAME and first_leaf.value == "def")
or (
first_leaf.type == token.ASYNC
and second_leaf is not None
and second_leaf.type == token.NAME
and second_leaf.value == 'def'
and second_leaf.value == "def"
)
)
@ -655,7 +655,7 @@ def is_yield(self) -> bool:
return (
bool(self)
and self.leaves[0].type == token.NAME
and self.leaves[0].value == 'yield'
and self.leaves[0].value == "yield"
)
@property
@ -722,7 +722,7 @@ def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool:
To avoid splitting on the comma in this situation, increase the depth of
tokens between `for` and `in`.
"""
if leaf.type == token.NAME and leaf.value == 'for':
if leaf.type == token.NAME and leaf.value == "for":
self.has_for = True
self.bracket_tracker.depth += 1
self._for_loop_variable = True
@ -732,7 +732,7 @@ def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool:
def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool:
"""See `maybe_increment_for_loop_variable` above for explanation."""
if self._for_loop_variable and leaf.type == token.NAME and leaf.value == 'in':
if self._for_loop_variable and leaf.type == token.NAME and leaf.value == "in":
self.bracket_tracker.depth -= 1
self._for_loop_variable = False
return True
@ -745,7 +745,7 @@ def append_comment(self, comment: Leaf) -> bool:
comment.type == STANDALONE_COMMENT
and self.bracket_tracker.any_open_brackets()
):
comment.prefix = ''
comment.prefix = ""
return False
if comment.type != token.COMMENT:
@ -754,7 +754,7 @@ def append_comment(self, comment: Leaf) -> bool:
after = len(self.leaves) - 1
if after == -1:
comment.type = STANDALONE_COMMENT
comment.prefix = ''
comment.prefix = ""
return False
else:
@ -786,17 +786,17 @@ def remove_trailing_comma(self) -> None:
def __str__(self) -> str:
"""Render the line."""
if not self:
return '\n'
return "\n"
indent = ' ' * self.depth
indent = " " * self.depth
leaves = iter(self.leaves)
first = next(leaves)
res = f'{first.prefix}{indent}{first.value}'
res = f"{first.prefix}{indent}{first.value}"
for leaf in leaves:
res += str(leaf)
for _, comment in self.comments:
res += str(comment)
return res + '\n'
return res + "\n"
def __bool__(self) -> bool:
"""Return True if the line has leaves or comments."""
@ -832,9 +832,9 @@ def __str__(self) -> str:
`depth` is not used for indentation in this case.
"""
if not self:
return '\n'
return "\n"
res = ''
res = ""
for leaf in self.leaves:
res += str(leaf)
return res
@ -888,9 +888,9 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
if current_line.leaves:
# Consume the first leaf's extra newlines.
first_leaf = current_line.leaves[0]
before = first_leaf.prefix.count('\n')
before = first_leaf.prefix.count("\n")
before = min(before, max_allowed)
first_leaf.prefix = ''
first_leaf.prefix = ""
else:
before = 0
depth = current_line.depth
@ -1009,6 +1009,8 @@ def visit_default(self, node: LN) -> Iterator[Line]:
else:
normalize_prefix(node, inside_brackets=any_open_brackets)
if node.type == token.STRING:
normalize_string_quotes(node)
if node.type not in WHITESPACE:
self.current_line.append(node)
yield from super().visit_default(node)
@ -1098,14 +1100,14 @@ def visit_unformatted(self, node: LN) -> Iterator[Line]:
def __attrs_post_init__(self) -> None:
"""You are in a twisty little maze of passages."""
v = self.visit_stmt
self.visit_if_stmt = partial(v, keywords={'if', 'else', 'elif'})
self.visit_while_stmt = partial(v, keywords={'while', 'else'})
self.visit_for_stmt = partial(v, keywords={'for', 'else'})
self.visit_try_stmt = partial(v, keywords={'try', 'except', 'else', 'finally'})
self.visit_except_clause = partial(v, keywords={'except'})
self.visit_funcdef = partial(v, keywords={'def'})
self.visit_with_stmt = partial(v, keywords={'with'})
self.visit_classdef = partial(v, keywords={'class'})
self.visit_if_stmt = partial(v, keywords={"if", "else", "elif"})
self.visit_while_stmt = partial(v, keywords={"while", "else"})
self.visit_for_stmt = partial(v, keywords={"for", "else"})
self.visit_try_stmt = partial(v, keywords={"try", "except", "else", "finally"})
self.visit_except_clause = partial(v, keywords={"except"})
self.visit_funcdef = partial(v, keywords={"def"})
self.visit_with_stmt = partial(v, keywords={"with"})
self.visit_classdef = partial(v, keywords={"class"})
self.visit_async_funcdef = self.visit_async_stmt
self.visit_decorated = self.visit_decorators
@ -1119,9 +1121,9 @@ def __attrs_post_init__(self) -> None:
def whitespace(leaf: Leaf) -> str: # noqa C901
"""Return whitespace prefix if needed for the given `leaf`."""
NO = ''
SPACE = ' '
DOUBLESPACE = ' '
NO = ""
SPACE = " "
DOUBLESPACE = " "
t = leaf.type
p = leaf.parent
v = leaf.value
@ -1185,7 +1187,7 @@ def whitespace(leaf: Leaf) -> str: # noqa C901
and prevp.parent.type == syms.shift_expr
and prevp.prev_sibling
and prevp.prev_sibling.type == token.NAME
and prevp.prev_sibling.value == 'print' # type: ignore
and prevp.prev_sibling.value == "print" # type: ignore
):
# Python 2 print chevron
return NO
@ -1342,7 +1344,7 @@ def whitespace(leaf: Leaf) -> str: # noqa C901
return NO
elif t == token.NAME:
if v == 'import':
if v == "import":
return SPACE
if prev and prev.type == token.DOT:
@ -1416,17 +1418,17 @@ def generate_comments(leaf: Leaf) -> Iterator[Leaf]:
if not p:
return
if '#' not in p:
if "#" not in p:
return
consumed = 0
nlines = 0
for index, line in enumerate(p.split('\n')):
for index, line in enumerate(p.split("\n")):
consumed += len(line) + 1 # adding the length of the split '\n'
line = line.lstrip()
if not line:
nlines += 1
if not line.startswith('#'):
if not line.startswith("#"):
continue
if index == 0 and leaf.type != token.ENDMARKER:
@ -1434,12 +1436,12 @@ def generate_comments(leaf: Leaf) -> Iterator[Leaf]:
else:
comment_type = STANDALONE_COMMENT
comment = make_comment(line)
yield Leaf(comment_type, comment, prefix='\n' * nlines)
yield Leaf(comment_type, comment, prefix="\n" * nlines)
if comment in {'# fmt: on', '# yapf: enable'}:
if comment in {"# fmt: on", "# yapf: enable"}:
raise FormatOn(consumed)
if comment in {'# fmt: off', '# yapf: disable'}:
if comment in {"# fmt: off", "# yapf: disable"}:
raise FormatOff(consumed)
nlines = 0
@ -1455,13 +1457,13 @@ def make_comment(content: str) -> str:
"""
content = content.rstrip()
if not content:
return '#'
return "#"
if content[0] == '#':
if content[0] == "#":
content = content[1:]
if content and content[0] not in ' !:#':
content = ' ' + content
return '#' + content
if content and content[0] not in " !:#":
content = " " + content
return "#" + content
def split_line(
@ -1481,10 +1483,10 @@ def split_line(
yield line
return
line_str = str(line).strip('\n')
line_str = str(line).strip("\n")
if (
len(line_str) <= line_length
and '\n' not in line_str # multiline strings
and "\n" not in line_str # multiline strings
and not line.contains_standalone_comments
):
yield line
@ -1504,7 +1506,7 @@ def split_line(
result: List[Line] = []
try:
for l in split_func(line, py36):
if str(l).strip('\n') == line_str:
if str(l).strip("\n") == line_str:
raise CannotSplit("Split function returned an unchanged result")
result.extend(
@ -1703,7 +1705,7 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]:
and current_line.leaves[-1].type != token.COMMA
and trailing_comma_safe
):
current_line.append(Leaf(token.COMMA, ','))
current_line.append(Leaf(token.COMMA, ","))
yield current_line
@ -1749,8 +1751,8 @@ def is_import(leaf: Leaf) -> bool:
return bool(
t == token.NAME
and (
(v == 'import' and p and p.type == syms.import_name)
or (v == 'from' and p and p.type == syms.import_from)
(v == "import" and p and p.type == syms.import_name)
or (v == "from" and p and p.type == syms.import_from)
)
)
@ -1762,15 +1764,52 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None:
Note: don't use backslashes for formatting or you'll lose your voting rights.
"""
if not inside_brackets:
spl = leaf.prefix.split('#')
if '\\' not in spl[0]:
nl_count = spl[-1].count('\n')
spl = leaf.prefix.split("#")
if "\\" not in spl[0]:
nl_count = spl[-1].count("\n")
if len(spl) > 1:
nl_count -= 1
leaf.prefix = '\n' * nl_count
leaf.prefix = "\n" * nl_count
return
leaf.prefix = ''
leaf.prefix = ""
def normalize_string_quotes(leaf: Leaf) -> None:
value = leaf.value.lstrip("furbFURB")
if value[:3] == '"""':
return
elif value[:3] == "'''":
orig_quote = "'''"
new_quote = '"""'
elif value[0] == '"':
orig_quote = '"'
new_quote = "'"
else:
orig_quote = "'"
new_quote = '"'
first_quote_pos = leaf.value.find(orig_quote)
if first_quote_pos == -1:
return # There's an internal error
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}"
)
if new_quote == '"""' and new_body[-1] == '"':
# edge case:
new_body = new_body[:-1] + '\\"'
orig_escape_count = body.count("\\")
new_escape_count = new_body.count("\\")
if new_escape_count > orig_escape_count:
return # Do not introduce more escaping
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}"
def is_python36(node: Node) -> bool:
@ -1783,7 +1822,7 @@ def is_python36(node: Node) -> bool:
for n in node.pre_order():
if n.type == token.STRING:
value_head = n.value[:2] # type: ignore
if value_head in {'f"', 'F"', "f'", "F'", 'rf', 'fr', 'RF', 'FR'}:
if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}:
return True
elif (
@ -1798,9 +1837,9 @@ def is_python36(node: Node) -> bool:
return False
PYTHON_EXTENSIONS = {'.py'}
PYTHON_EXTENSIONS = {".py"}
BLACKLISTED_DIRECTORIES = {
'build', 'buck-out', 'dist', '_build', '.git', '.hg', '.mypy_cache', '.tox', '.venv'
"build", "buck-out", "dist", "_build", ".git", ".hg", ".mypy_cache", ".tox", ".venv"
}
@ -1830,16 +1869,16 @@ class Report:
def done(self, src: Path, changed: bool) -> None:
"""Increment the counter for successful reformatting. Write out a message."""
if changed:
reformatted = 'would reformat' if self.check else 'reformatted'
out(f'{reformatted} {src}')
reformatted = "would reformat" if self.check else "reformatted"
out(f"{reformatted} {src}")
self.change_count += 1
else:
out(f'{src} already well formatted, good job.', bold=False)
out(f"{src} already well formatted, good job.", bold=False)
self.same_count += 1
def failed(self, src: Path, message: str) -> None:
"""Increment the counter for failed reformatting. Write out a message."""
err(f'error: cannot format {src}: {message}')
err(f"error: cannot format {src}: {message}")
self.failure_count += 1
@property
@ -1876,19 +1915,19 @@ def __str__(self) -> str:
failed = "failed to reformat"
report = []
if self.change_count:
s = 's' if self.change_count > 1 else ''
s = "s" if self.change_count > 1 else ""
report.append(
click.style(f'{self.change_count} file{s} {reformatted}', bold=True)
click.style(f"{self.change_count} file{s} {reformatted}", bold=True)
)
if self.same_count:
s = 's' if self.same_count > 1 else ''
report.append(f'{self.same_count} file{s} {unchanged}')
s = "s" if self.same_count > 1 else ""
report.append(f"{self.same_count} file{s} {unchanged}")
if self.failure_count:
s = 's' if self.failure_count > 1 else ''
s = "s" if self.failure_count > 1 else ""
report.append(
click.style(f'{self.failure_count} file{s} {failed}', fg='red')
click.style(f"{self.failure_count} file{s} {failed}", fg="red")
)
return ', '.join(report) + '.'
return ", ".join(report) + "."
def assert_equivalent(src: str, dst: str) -> None:
@ -1935,17 +1974,17 @@ def _v(node: ast.AST, depth: int = 0) -> Iterator[str]:
try:
dst_ast = ast.parse(dst)
except Exception as exc:
log = dump_to_file(''.join(traceback.format_tb(exc.__traceback__)), dst)
log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
raise AssertionError(
f"INTERNAL ERROR: Black produced invalid code: {exc}. "
f"Please report a bug on https://github.com/ambv/black/issues. "
f"This invalid output might be helpful: {log}"
) from None
src_ast_str = '\n'.join(_v(src_ast))
dst_ast_str = '\n'.join(_v(dst_ast))
src_ast_str = "\n".join(_v(src_ast))
dst_ast_str = "\n".join(_v(dst_ast))
if src_ast_str != dst_ast_str:
log = dump_to_file(diff(src_ast_str, dst_ast_str, 'src', 'dst'))
log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
raise AssertionError(
f"INTERNAL ERROR: Black produced code that is not equivalent to "
f"the source. "
@ -1959,8 +1998,8 @@ def assert_stable(src: str, dst: str, line_length: int) -> None:
newdst = format_str(dst, line_length=line_length)
if dst != newdst:
log = dump_to_file(
diff(src, dst, 'source', 'first pass'),
diff(dst, newdst, 'first pass', 'second pass'),
diff(src, dst, "source", "first pass"),
diff(dst, newdst, "first pass", "second pass"),
)
raise AssertionError(
f"INTERNAL ERROR: Black produced different code on the second pass "
@ -1975,11 +2014,11 @@ def dump_to_file(*output: str) -> str:
import tempfile
with tempfile.NamedTemporaryFile(
mode='w', prefix='blk_', suffix='.log', delete=False
mode="w", prefix="blk_", suffix=".log", delete=False
) as f:
for lines in output:
f.write(lines)
f.write('\n')
f.write("\n")
return f.name
@ -1987,9 +2026,9 @@ def diff(a: str, b: str, a_name: str, b_name: str) -> str:
"""Return a unified diff string between strings `a` and `b`."""
import difflib
a_lines = [line + '\n' for line in a.split('\n')]
b_lines = [line + '\n' for line in b.split('\n')]
return ''.join(
a_lines = [line + "\n" for line in a.split("\n")]
b_lines = [line + "\n" for line in b.split("\n")]
return "".join(
difflib.unified_diff(a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5)
)
@ -2023,5 +2062,5 @@ def shutdown(loop: BaseEventLoop) -> None:
loop.close()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -11,48 +11,48 @@
def get_long_description():
readme_md = CURRENT_DIR / 'README.md'
with open(readme_md, encoding='utf8') as ld_file:
readme_md = CURRENT_DIR / "README.md"
with open(readme_md, encoding="utf8") as ld_file:
return ld_file.read()
def get_version():
black_py = CURRENT_DIR / 'black.py'
_version_re = re.compile(r'__version__\s+=\s+(?P<version>.*)')
with open(black_py, 'r', encoding='utf8') as f:
version = _version_re.search(f.read()).group('version')
black_py = CURRENT_DIR / "black.py"
_version_re = re.compile(r"__version__\s+=\s+(?P<version>.*)")
with open(black_py, "r", encoding="utf8") as f:
version = _version_re.search(f.read()).group("version")
return str(ast.literal_eval(version))
setup(
name='black',
name="black",
version=get_version(),
description="The uncompromising code formatter.",
long_description=get_long_description(),
long_description_content_type="text/markdown",
keywords='automation formatter yapf autopep8 pyfmt gofmt rustfmt',
author='Łukasz Langa',
author_email='lukasz@langa.pl',
url='https://github.com/ambv/black',
license='MIT',
py_modules=['black'],
packages=['blib2to3', 'blib2to3.pgen2'],
package_data={'blib2to3': ['*.txt']},
keywords="automation formatter yapf autopep8 pyfmt gofmt rustfmt",
author="Łukasz Langa",
author_email="lukasz@langa.pl",
url="https://github.com/ambv/black",
license="MIT",
py_modules=["black"],
packages=["blib2to3", "blib2to3.pgen2"],
package_data={"blib2to3": ["*.txt"]},
python_requires=">=3.6",
zip_safe=False,
install_requires=['click', 'attrs>=17.4.0'],
test_suite='tests.test_black',
install_requires=["click", "attrs>=17.4.0"],
test_suite="tests.test_black",
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Quality Assurance',
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Quality Assurance",
],
entry_points={'console_scripts': ['black=black:main']},
entry_points={"console_scripts": ["black=black:main"]},
)

View File

@ -43,7 +43,7 @@ def function(default=None):
# Explains why we use global state.
GLOBAL_STATE = {'a': a(1), 'b': a(2), 'c': a(3)}
GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)}
# Another comment!
@ -76,7 +76,7 @@ async def wat():
result = await x.method1()
# Comment after ending a block.
if result:
print('A OK', file=sys.stdout)
print("A OK", file=sys.stdout)
# Comment between things.
print()

View File

@ -125,23 +125,23 @@ def inline_comments_in_brackets_ruin_everything():
__all__ = [
# Super-special typing primitives.
'Any',
'Callable',
'ClassVar',
"Any",
"Callable",
"ClassVar",
# ABCs (from collections.abc).
'AbstractSet', # collections.abc.Set.
'ByteString',
'Container',
"AbstractSet", # collections.abc.Set.
"ByteString",
"Container",
# Concrete collection types.
'Counter',
'Deque',
'Dict',
'DefaultDict',
'List',
'Set',
'FrozenSet',
'NamedTuple', # Not really a type.
'Generator',
"Counter",
"Deque",
"Dict",
"DefaultDict",
"List",
"Set",
"FrozenSet",
"NamedTuple", # Not really a type.
"Generator",
]
# Comment before function.
@ -212,7 +212,7 @@ def inline_comments_in_brackets_ruin_everything():
]
lcomp3 = [
# This one is actually too long to fit in a single line.
element.split('\n', 1)[0]
element.split("\n", 1)[0]
# yup
for element in collection.select_elements()
# right
@ -228,7 +228,7 @@ def inline_comments_in_brackets_ruin_everything():
# let's return
return Node(
syms.simple_stmt,
[Node(statement, result), Leaf(token.NEWLINE, '\n')], # FIXME: \r\n?
[Node(statement, result), Leaf(token.NEWLINE, "\n")], # FIXME: \r\n?
)

View File

@ -1,7 +1,7 @@
def func():
lcomp3 = [
# This one is actually too long to fit in a single line.
element.split('\n', 1)[0]
element.split("\n", 1)[0]
# yup
for element in collection.select_elements()
# right

View File

@ -61,7 +61,7 @@ def test_fails_invalid_post_data(
def foo(list_a, list_b):
results = (
User.query.filter(User.foo == 'bar').filter( # Because foo.
User.query.filter(User.foo == "bar").filter( # Because foo.
db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
).filter(
User.xyz.is_(None)

View File

@ -3,19 +3,19 @@ class C:
def test(self) -> None:
with patch("black.out", print):
self.assertEqual(
unstyle(str(report)), '1 file reformatted, 1 file failed to reformat.'
unstyle(str(report)), "1 file reformatted, 1 file failed to reformat."
)
self.assertEqual(
unstyle(str(report)),
'1 file reformatted, 1 file left unchanged, 1 file failed to reformat.',
"1 file reformatted, 1 file left unchanged, 1 file failed to reformat.",
)
self.assertEqual(
unstyle(str(report)),
'2 files reformatted, 1 file left unchanged, '
'1 file failed to reformat.',
"2 files reformatted, 1 file left unchanged, "
"1 file failed to reformat.",
)
self.assertEqual(
unstyle(str(report)),
'2 files reformatted, 2 files left unchanged, '
'2 files failed to reformat.',
"2 files reformatted, 2 files left unchanged, "
"2 files failed to reformat.",
)

View File

@ -64,7 +64,7 @@ def g():
return DOUBLESPACE
# Another comment because more comments
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
assert p is not None, f'INTERNAL ERROR: hand-made leaf without parent: {leaf!r}'
prev = leaf.prev_sibling
if not prev:
@ -90,9 +90,9 @@ def g():
def f():
NO = ''
SPACE = ' '
DOUBLESPACE = ' '
NO = ""
SPACE = " "
DOUBLESPACE = " "
t = leaf.type
p = leaf.parent # trailing comment
@ -139,9 +139,9 @@ def f():
def g():
NO = ''
SPACE = ' '
DOUBLESPACE = ' '
NO = ""
SPACE = " "
DOUBLESPACE = " "
t = leaf.type
p = leaf.parent

View File

@ -157,8 +157,8 @@ async def f():
...
'some_string'
b'\\xa3'
"some_string"
b"\\xa3"
Name
None
True
@ -193,18 +193,18 @@ async def f():
lambda arg: None
lambda a=True: a
lambda a, b, c=True: a
lambda a, b, c=True, *, d=(1 << v2), e='str': a
lambda a, b, c=True, *vararg, d=(v1 << 2), e='str', **kwargs: a + b
lambda a, b, c=True, *, d=(1 << v2), e="str": a
lambda a, b, c=True, *vararg, d=(v1 << 2), e="str", **kwargs: a + b
1 if True else 2
str or None if True else str or bytes or None
(str or None) if True else (str or bytes or None)
str or None if (1 if True else 2) else str or bytes or None
(str or None) if (1 if True else 2) else (str or bytes or None)
{'2.7': dead, '3.7': (long_live or die_hard)}
{'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}}
{"2.7": dead, "3.7": (long_live or die_hard)}
{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}}
{**a, **b, **c}
{'2.7', '3.6', '3.7', '3.8', '3.9', ('4.0' if gilectomy else '3.10')}
({'a': 'b'}, (True or False), (+value), 'string', b'bytes') or None
{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")}
({"a": "b"}, (True or False), (+value), "string", b"bytes") or None
()
(1,)
(1, 2)
@ -214,14 +214,14 @@ async def f():
[1, 2, 3]
{i for i in (1, 2, 3)}
{(i ** 2) for i in (1, 2, 3)}
{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))}
{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
[i for i in (1, 2, 3)]
[(i ** 2) for i in (1, 2, 3)]
[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))]
[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))]
[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
{i: 0 for i in (1, 2, 3)}
{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}
{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))}
{a: b * 2 for a, b in dictionary.items()}
{a: b * -2 for a, b in dictionary.items()}
{
@ -232,14 +232,14 @@ async def f():
Life is Life
call()
call(arg)
call(kwarg='hey')
call(arg, kwarg='hey')
call(arg, another, kwarg='hey', **kwargs)
call(kwarg="hey")
call(arg, kwarg="hey")
call(arg, another, kwarg="hey", **kwargs)
call(
this_is_a_very_long_variable_which_will_force_a_delimiter_split,
arg,
another,
kwarg='hey',
kwarg="hey",
**kwargs
) # note: no trailing comma pre-3.6
call(*gidgets[:2])
@ -283,15 +283,15 @@ async def f():
numpy[:, ::-1]
numpy[np.newaxis, :]
(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None)
{'2.7': dead, '3.7': long_live or die_hard}
{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'}
{"2.7": dead, "3.7": long_live or die_hard}
{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"}
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C]
(SomeName)
SomeName
(Good, Bad, Ugly)
(i for i in (1, 2, 3))
((i ** 2) for i in (1, 2, 3))
((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c')))
((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c")))
(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
(*starred)
a = (1,)

View File

@ -15,10 +15,10 @@ def func_no_args():
for i in range(10):
print(i)
continue
exec("new-style exec", {}, {})
exec('new-style exec', {}, {})
return None
async def coroutine(arg, exec=False):
"Single-line docstring. Multiline is harder to reformat."
'Single-line docstring. Multiline is harder to reformat.'
async with some_connection() as conn:
await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2)
await asyncio.sleep(1)
@ -27,7 +27,7 @@ async def coroutine(arg, exec=False):
with_args=True,
many_args=[1,2,3]
)
def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str:
def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str:
return text[number:-1]
# fmt: on
def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''):
@ -83,7 +83,7 @@ def long_lines():
from library import some_connection, some_decorator
f'trigger 3.6 mode'
f"trigger 3.6 mode"
# fmt: off
def func_no_args():
a; b; c
@ -92,10 +92,10 @@ def func_no_args():
for i in range(10):
print(i)
continue
exec("new-style exec", {}, {})
exec('new-style exec', {}, {})
return None
async def coroutine(arg, exec=False):
"Single-line docstring. Multiline is harder to reformat."
'Single-line docstring. Multiline is harder to reformat.'
async with some_connection() as conn:
await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2)
await asyncio.sleep(1)
@ -104,12 +104,12 @@ async def coroutine(arg, exec=False):
with_args=True,
many_args=[1,2,3]
)
def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str:
def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str:
return text[number:-1]
# fmt: on
def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''):
def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""):
offset = attr.ib(default=attr.Factory(lambda: _r.uniform(10000, 200000)))
assert task._cancel_stack[:len(old_stack)] == old_stack
@ -123,7 +123,7 @@ def spaces_types(
f: int = -1,
g: int = 1 if False else 2,
h: str = "",
i: str = r'',
i: str = r"",
):
...

View File

@ -1,5 +1,5 @@
f'f-string without formatted values is just a string'
f'{{NOT a formatted value}}'
f'some f-string with {a} {few():.2f} {formatted.values!r}'
f"f-string without formatted values is just a string"
f"{{NOT a formatted value}}"
f"some f-string with {a} {few():.2f} {formatted.values!r}"
f"{f'{nested} inner'} outer"
f'space between opening braces: { {a for a in (1, 2, 3)}}'
f"space between opening braces: { {a for a in (1, 2, 3)}}"

View File

@ -80,7 +80,7 @@ def long_lines():
from library import some_connection, some_decorator
f'trigger 3.6 mode'
f"trigger 3.6 mode"
def func_no_args():
@ -103,7 +103,7 @@ def func_no_args():
async def coroutine(arg, exec=False):
"Single-line docstring. Multiline is harder to reformat."
async with some_connection() as conn:
await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2)
await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2)
await asyncio.sleep(1)
@ -120,7 +120,7 @@ def function_signature_stress_test(
return text[number:-1]
def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''):
def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""):
offset = attr.ib(default=attr.Factory(lambda: _r.uniform(10000, 200000)))
assert task._cancel_stack[:len(old_stack)] == old_stack
@ -134,7 +134,7 @@ def spaces_types(
f: int = -1,
g: int = 1 if False else 2,
h: str = "",
i: str = r'',
i: str = r"",
):
...

37
tests/string_quotes.py Normal file
View File

@ -0,0 +1,37 @@
"Hello"
"Don't do that"
'Here is a "'
'What\'s the deal here?'
"What's the deal \"here\"?"
"And \"here\"?"
"""Strings with "" in them"""
'''Strings with "" in them'''
'''Here's a "'''
'''Here's a " '''
'''Just a normal triple
quote'''
f"just a normal {f} string"
f'''This is a triple-quoted {f}-string'''
f'MOAR {" ".join([])}'
f"MOAR {' '.join([])}"
r"raw string ftw"
# output
"Hello"
"Don't do that"
'Here is a "'
"What's the deal here?"
'What\'s the deal "here"?'
'And "here"?'
"""Strings with "" in them"""
"""Strings with "" in them"""
'''Here's a "'''
"""Here's a " """
"""Just a normal triple
quote"""
f"just a normal {f} string"
f"""This is a triple-quoted {f}-string"""
f'MOAR {" ".join([])}'
f"MOAR {' '.join([])}"
r"raw string ftw"

View File

@ -17,25 +17,25 @@
fs = partial(black.format_str, line_length=ll)
THIS_FILE = Path(__file__)
THIS_DIR = THIS_FILE.parent
EMPTY_LINE = '# EMPTY LINE WITH WHITESPACE' + ' (this comment will be removed)'
EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)"
def dump_to_stderr(*output: str) -> str:
return '\n' + '\n'.join(output) + '\n'
return "\n" + "\n".join(output) + "\n"
def read_data(name: str) -> Tuple[str, str]:
"""read_data('test_name') -> 'input', 'output'"""
if not name.endswith(('.py', '.out')):
name += '.py'
if not name.endswith((".py", ".out")):
name += ".py"
_input: List[str] = []
_output: List[str] = []
with open(THIS_DIR / name, 'r', encoding='utf8') as test:
with open(THIS_DIR / name, "r", encoding="utf8") as test:
lines = test.readlines()
result = _input
for line in lines:
line = line.replace(EMPTY_LINE, '')
if line.rstrip() == '# output':
line = line.replace(EMPTY_LINE, "")
if line.rstrip() == "# output":
result = _output
continue
@ -43,23 +43,23 @@ def read_data(name: str) -> Tuple[str, str]:
if _input and not _output:
# If there's no output marker, treat the entire file as already pre-formatted.
_output = _input[:]
return ''.join(_input).strip() + '\n', ''.join(_output).strip() + '\n'
return "".join(_input).strip() + "\n", "".join(_output).strip() + "\n"
class BlackTestCase(unittest.TestCase):
maxDiff = None
def assertFormatEqual(self, expected: str, actual: str) -> None:
if actual != expected and not os.environ.get('SKIP_AST_PRINT'):
if actual != expected and not os.environ.get("SKIP_AST_PRINT"):
bdv: black.DebugVisitor[Any]
black.out('Expected tree:', fg='green')
black.out("Expected tree:", fg="green")
try:
exp_node = black.lib2to3_parse(expected)
bdv = black.DebugVisitor()
list(bdv.visit(exp_node))
except Exception as ve:
black.err(str(ve))
black.out('Actual tree:', fg='red')
black.out("Actual tree:", fg="red")
try:
exp_node = black.lib2to3_parse(actual)
bdv = black.DebugVisitor()
@ -70,7 +70,7 @@ def assertFormatEqual(self, expected: str, actual: str) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_self(self) -> None:
source, expected = read_data('test_black')
source, expected = read_data("test_black")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -79,19 +79,19 @@ def test_self(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_black(self) -> None:
source, expected = read_data('../black')
source, expected = read_data("../black")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
black.assert_stable(source, actual, line_length=ll)
self.assertFalse(ff(THIS_DIR / '..' / 'black.py'))
self.assertFalse(ff(THIS_DIR / ".." / "black.py"))
def test_piping(self) -> None:
source, expected = read_data('../black')
source, expected = read_data("../black")
hold_stdin, hold_stdout = sys.stdin, sys.stdout
try:
sys.stdin, sys.stdout = StringIO(source), StringIO()
sys.stdin.name = '<stdin>'
sys.stdin.name = "<stdin>"
black.format_stdin_to_stdout(line_length=ll, fast=True, write_back=True)
sys.stdout.seek(0)
actual = sys.stdout.read()
@ -103,16 +103,16 @@ def test_piping(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_setup(self) -> None:
source, expected = read_data('../setup')
source, expected = read_data("../setup")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
black.assert_stable(source, actual, line_length=ll)
self.assertFalse(ff(THIS_DIR / '..' / 'setup.py'))
self.assertFalse(ff(THIS_DIR / ".." / "setup.py"))
@patch("black.dump_to_file", dump_to_stderr)
def test_function(self) -> None:
source, expected = read_data('function')
source, expected = read_data("function")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -120,7 +120,7 @@ def test_function(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_expression(self) -> None:
source, expected = read_data('expression')
source, expected = read_data("expression")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -128,7 +128,15 @@ def test_expression(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_fstring(self) -> None:
source, expected = read_data('fstring')
source, expected = read_data("fstring")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
black.assert_stable(source, actual, line_length=ll)
@patch("black.dump_to_file", dump_to_stderr)
def test_string_quotes(self) -> None:
source, expected = read_data("string_quotes")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -136,7 +144,7 @@ def test_fstring(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_comments(self) -> None:
source, expected = read_data('comments')
source, expected = read_data("comments")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -144,7 +152,7 @@ def test_comments(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_comments2(self) -> None:
source, expected = read_data('comments2')
source, expected = read_data("comments2")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -152,7 +160,7 @@ def test_comments2(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_comments3(self) -> None:
source, expected = read_data('comments3')
source, expected = read_data("comments3")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -160,7 +168,7 @@ def test_comments3(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_comments4(self) -> None:
source, expected = read_data('comments4')
source, expected = read_data("comments4")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -168,7 +176,7 @@ def test_comments4(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_cantfit(self) -> None:
source, expected = read_data('cantfit')
source, expected = read_data("cantfit")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -176,7 +184,7 @@ def test_cantfit(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_import_spacing(self) -> None:
source, expected = read_data('import_spacing')
source, expected = read_data("import_spacing")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -184,7 +192,7 @@ def test_import_spacing(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_composition(self) -> None:
source, expected = read_data('composition')
source, expected = read_data("composition")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -192,7 +200,7 @@ def test_composition(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_empty_lines(self) -> None:
source, expected = read_data('empty_lines')
source, expected = read_data("empty_lines")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -200,7 +208,7 @@ def test_empty_lines(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_python2(self) -> None:
source, expected = read_data('python2')
source, expected = read_data("python2")
actual = fs(source)
self.assertFormatEqual(expected, actual)
# black.assert_equivalent(source, actual)
@ -208,7 +216,7 @@ def test_python2(self) -> None:
@patch("black.dump_to_file", dump_to_stderr)
def test_fmtonoff(self) -> None:
source, expected = read_data('fmtonoff')
source, expected = read_data("fmtonoff")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
@ -226,68 +234,68 @@ def err(msg: str, **kwargs: Any) -> None:
err_lines.append(msg)
with patch("black.out", out), patch("black.err", err):
report.done(Path('f1'), changed=False)
report.done(Path("f1"), changed=False)
self.assertEqual(len(out_lines), 1)
self.assertEqual(len(err_lines), 0)
self.assertEqual(out_lines[-1], 'f1 already well formatted, good job.')
self.assertEqual(unstyle(str(report)), '1 file left unchanged.')
self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
self.assertEqual(report.return_code, 0)
report.done(Path('f2'), changed=True)
report.done(Path("f2"), changed=True)
self.assertEqual(len(out_lines), 2)
self.assertEqual(len(err_lines), 0)
self.assertEqual(out_lines[-1], 'reformatted f2')
self.assertEqual(out_lines[-1], "reformatted f2")
self.assertEqual(
unstyle(str(report)), '1 file reformatted, 1 file left unchanged.'
unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
)
self.assertEqual(report.return_code, 0)
report.check = True
self.assertEqual(report.return_code, 1)
report.check = False
report.failed(Path('e1'), 'boom')
report.failed(Path("e1"), "boom")
self.assertEqual(len(out_lines), 2)
self.assertEqual(len(err_lines), 1)
self.assertEqual(err_lines[-1], 'error: cannot format e1: boom')
self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
self.assertEqual(
unstyle(str(report)),
'1 file reformatted, 1 file left unchanged, '
'1 file failed to reformat.',
"1 file reformatted, 1 file left unchanged, "
"1 file failed to reformat.",
)
self.assertEqual(report.return_code, 123)
report.done(Path('f3'), changed=True)
report.done(Path("f3"), changed=True)
self.assertEqual(len(out_lines), 3)
self.assertEqual(len(err_lines), 1)
self.assertEqual(out_lines[-1], 'reformatted f3')
self.assertEqual(out_lines[-1], "reformatted f3")
self.assertEqual(
unstyle(str(report)),
'2 files reformatted, 1 file left unchanged, '
'1 file failed to reformat.',
"2 files reformatted, 1 file left unchanged, "
"1 file failed to reformat.",
)
self.assertEqual(report.return_code, 123)
report.failed(Path('e2'), 'boom')
report.failed(Path("e2"), "boom")
self.assertEqual(len(out_lines), 3)
self.assertEqual(len(err_lines), 2)
self.assertEqual(err_lines[-1], 'error: cannot format e2: boom')
self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
self.assertEqual(
unstyle(str(report)),
'2 files reformatted, 1 file left unchanged, '
'2 files failed to reformat.',
"2 files reformatted, 1 file left unchanged, "
"2 files failed to reformat.",
)
self.assertEqual(report.return_code, 123)
report.done(Path('f4'), changed=False)
report.done(Path("f4"), changed=False)
self.assertEqual(len(out_lines), 4)
self.assertEqual(len(err_lines), 2)
self.assertEqual(out_lines[-1], 'f4 already well formatted, good job.')
self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
self.assertEqual(
unstyle(str(report)),
'2 files reformatted, 2 files left unchanged, '
'2 files failed to reformat.',
"2 files reformatted, 2 files left unchanged, "
"2 files failed to reformat.",
)
self.assertEqual(report.return_code, 123)
report.check = True
self.assertEqual(
unstyle(str(report)),
'2 files would be reformatted, 2 files would be left unchanged, '
'2 files would fail to reformat.',
"2 files would be reformatted, 2 files would be left unchanged, "
"2 files would fail to reformat.",
)
def test_is_python36(self) -> None:
@ -297,20 +305,20 @@ def test_is_python36(self) -> None:
self.assertTrue(black.is_python36(node))
node = black.lib2to3_parse("def f(*, arg): f'string'\n")
self.assertTrue(black.is_python36(node))
source, expected = read_data('function')
source, expected = read_data("function")
node = black.lib2to3_parse(source)
self.assertTrue(black.is_python36(node))
node = black.lib2to3_parse(expected)
self.assertTrue(black.is_python36(node))
source, expected = read_data('expression')
source, expected = read_data("expression")
node = black.lib2to3_parse(source)
self.assertFalse(black.is_python36(node))
node = black.lib2to3_parse(expected)
self.assertFalse(black.is_python36(node))
def test_debug_visitor(self) -> None:
source, _ = read_data('debug_visitor.py')
expected, _ = read_data('debug_visitor.out')
source, _ = read_data("debug_visitor.py")
expected, _ = read_data("debug_visitor.out")
out_lines = []
err_lines = []
@ -322,8 +330,8 @@ def err(msg: str, **kwargs: Any) -> None:
with patch("black.out", out), patch("black.err", err):
black.DebugVisitor.show(source)
actual = '\n'.join(out_lines) + '\n'
log_name = ''
actual = "\n".join(out_lines) + "\n"
log_name = ""
if expected != actual:
log_name = black.dump_to_file(*out_lines)
self.assertEqual(
@ -333,5 +341,5 @@ def err(msg: str, **kwargs: Any) -> None:
)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()