import os
from pathlib import Path
import pickle
-import re
+import regex as re
import signal
import sys
import tempfile
from blib2to3.pgen2.grammar import Grammar
from blib2to3.pgen2.parse import ParseError
+from _version import version as __version__
-__version__ = "19.3b0"
DEFAULT_LINE_LENGTH = 88
DEFAULT_EXCLUDES = (
r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist)/"
ASYNC_IDENTIFIERS = 6
ASYNC_KEYWORDS = 7
ASSIGNMENT_EXPRESSIONS = 8
+ POS_ONLY_ARGUMENTS = 9
VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.ASSIGNMENT_EXPRESSIONS,
+ Feature.POS_ONLY_ARGUMENTS,
},
}
report = Report(check=check, quiet=quiet, verbose=verbose)
root = find_project_root(src)
sources: Set[Path] = set()
+ path_empty(src, quiet, verbose, ctx)
for s in src:
p = Path(s)
if p.is_dir():
err(f"invalid path: {s}")
if len(sources) == 0:
if verbose or not quiet:
- out("No paths given. Nothing to do 😴")
+ out("No Python files are present to be formatted. Nothing to do 😴")
ctx.exit(0)
if len(sources) == 1:
ctx.exit(report.return_code)
+def path_empty(src: Tuple[str], quiet: bool, verbose: bool, ctx: click.Context) -> None:
+ """
+ Exit if there is no `src` provided for formatting
+ """
+ if not src:
+ if verbose or not quiet:
+ out("No Path provided. Nothing to do 😴")
+ ctx.exit(0)
+
+
def reformat_one(
src: Path, fast: bool, write_back: WriteBack, mode: FileMode, report: "Report"
) -> None:
token.DOUBLESTAR,
}
STARS = {token.STAR, token.DOUBLESTAR}
+VARARGS_SPECIALS = STARS | {token.SLASH}
VARARGS_PARENTS = {
syms.arglist,
syms.argument, # double star in arglist
return True
return False
- def contains_inner_type_comments(self) -> bool:
+ def contains_uncollapsable_type_comments(self) -> bool:
ignored_ids = set()
try:
last_leaf = self.leaves[-1]
except IndexError:
return False
+ # A type comment is uncollapsable if it is attached to a leaf
+ # that isn't at the end of the line (since that could cause it
+ # to get associated to a different argument) or if there are
+ # comments before it (since that could cause it to get hidden
+ # behind a comment.
+ comment_seen = False
for leaf_id, comments in self.comments.items():
- if leaf_id in ignored_ids:
- continue
-
for comment in comments:
if is_type_comment(comment):
- return True
+ if leaf_id not in ignored_ids or comment_seen:
+ return True
+
+ comment_seen = True
+
+ return False
+
+ def contains_unsplittable_type_ignore(self) -> bool:
+ if not self.leaves:
+ return False
+
+ # If a 'type: ignore' is attached to the end of a line, we
+ # can't split the line, because we can't know which of the
+ # subexpressions the ignore was meant to apply to.
+ #
+ # We only want this to apply to actual physical lines from the
+ # original source, though: we don't want the presence of a
+ # 'type: ignore' at the end of a multiline expression to
+ # justify pushing it all onto one line. Thus we
+ # (unfortunately) need to check the actual source lines and
+ # only report an unsplittable 'type: ignore' if this line was
+ # one line in the original code.
+
+ # Grab the first and last line numbers, skipping generated leaves
+ first_line = next((l.lineno for l in self.leaves if l.lineno != 0), 0)
+ last_line = next((l.lineno for l in reversed(self.leaves) if l.lineno != 0), 0)
+
+ if first_line == last_line:
+ # We look at the last two leaves since a comma or an
+ # invisible paren could have been added at the end of the
+ # line.
+ for node in self.leaves[-2:]:
+ for comment in self.comments.get(id(node), []):
+ if is_type_comment(comment, " ignore"):
+ return True
return False
lines (two on module-level).
"""
before, after = self._maybe_empty_lines(current_line)
- before -= self.previous_after
+ before = (
+ # Black should not insert empty lines at the beginning
+ # of the file
+ 0
+ if self.previous_line is None
+ else before - self.previous_after
+ )
self.previous_after = after
self.previous_line = current_line
return before, after
# that, too.
return prevp.prefix
- elif prevp.type in STARS:
+ elif prevp.type in VARARGS_SPECIALS:
if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS):
return NO
if not prevp or prevp.type == token.LPAR:
return NO
- elif prev.type in {token.EQUAL} | STARS:
+ elif prev.type in {token.EQUAL} | VARARGS_SPECIALS:
return NO
elif p.type == syms.decorator:
line_str = str(line).strip("\n")
if (
- not line.contains_inner_type_comments()
+ not line.contains_uncollapsable_type_comments()
and not line.should_explode
- and is_line_short_enough(line, line_length=line_length, line_str=line_str)
+ and (
+ is_line_short_enough(line, line_length=line_length, line_str=line_str)
+ or line.contains_unsplittable_type_ignore()
+ )
):
yield line
return
)
-def is_type_comment(leaf: Leaf) -> bool:
+def is_type_comment(leaf: Leaf, suffix: str = "") -> bool:
"""Return True if the given leaf is a special comment.
Only returns true for type comments for now."""
t = leaf.type
v = leaf.value
- return t in {token.COMMENT, t == STANDALONE_COMMENT} and v.startswith("# type:")
+ return t in {token.COMMENT, t == STANDALONE_COMMENT} and v.startswith(
+ "# type:" + suffix
+ )
def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None:
if is_walrus_assignment(child):
continue
if child.type == syms.atom:
- if maybe_make_parens_invisible_in_atom(child, parent=node):
+ # Determines if the underlying atom should be surrounded with
+ # invisible params - also makes parens invisible recursively
+ # within the atom and removes repeated invisible parens within
+ # the atom
+ should_surround_with_parens = maybe_make_parens_invisible_in_atom(
+ child, parent=node
+ )
+
+ if should_surround_with_parens:
lpar = Leaf(token.LPAR, "")
rpar = Leaf(token.RPAR, "")
index = child.remove() or 0
def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool:
"""If it's safe, make the parens in the atom `node` invisible, recursively.
+ Additionally, remove repeated, adjacent invisible parens from the atom `node`
+ as they are redundant.
Returns whether the node should itself be wrapped in invisible parentheses.
first = node.children[0]
last = node.children[-1]
if first.type == token.LPAR and last.type == token.RPAR:
+ middle = node.children[1]
# make parentheses invisible
first.value = "" # type: ignore
last.value = "" # type: ignore
- if len(node.children) > 1:
- maybe_make_parens_invisible_in_atom(node.children[1], parent=parent)
+ maybe_make_parens_invisible_in_atom(middle, parent=parent)
+
+ if is_atom_with_invisible_parens(middle):
+ # Strip the invisible parens from `middle` by replacing
+ # it with the child in-between the invisible parens
+ middle.replace(middle.children[1])
+
return False
return True
+def is_atom_with_invisible_parens(node: LN) -> bool:
+ """Given a `LN`, determines whether it's an atom `node` with invisible
+ parens. Useful in dedupe-ing and normalizing parens.
+ """
+ if isinstance(node, Leaf) or node.type != syms.atom:
+ return False
+
+ first, last = node.children[0], node.children[-1]
+ return (
+ isinstance(first, Leaf)
+ and first.type == token.LPAR
+ and first.value == ""
+ and isinstance(last, Leaf)
+ and last.type == token.RPAR
+ and last.value == ""
+ )
+
+
def is_empty_tuple(node: LN) -> bool:
"""Return True if `node` holds an empty tuple."""
return (
extended iterable unpacking (PEP 3132) and additional unpacking
generalizations (PEP 448).
"""
- if leaf.type not in STARS or not leaf.parent:
+ if leaf.type not in VARARGS_SPECIALS or not leaf.parent:
return False
p = leaf.parent
Currently looking for:
- f-strings;
- - underscores in numeric literals; and
- - trailing commas after * or ** in function signatures and calls.
+ - underscores in numeric literals;
+ - trailing commas after * or ** in function signatures and calls;
+ - positional only arguments in function signatures and lambdas;
"""
features: Set[Feature] = set()
for n in node.pre_order():
if "_" in n.value: # type: ignore
features.add(Feature.NUMERIC_UNDERSCORES)
+ elif n.type == token.SLASH:
+ if n.parent and n.parent.type in {syms.typedargslist, syms.arglist}:
+ features.add(Feature.POS_ONLY_ARGUMENTS)
+
elif n.type == token.COLONEQUAL:
features.add(Feature.ASSIGNMENT_EXPRESSIONS)
"""
if "\n" in regex:
regex = "(?x)" + regex
- return re.compile(regex)
+ compiled: Pattern[str] = re.compile(regex)
+ return compiled
def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]: