init
This commit is contained in:
commit
38355d2442
9083 changed files with 1225834 additions and 0 deletions
BIN
.venv/lib/python3.8/site-packages/black/__init__.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/__init__.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
1459
.venv/lib/python3.8/site-packages/black/__init__.py
Normal file
1459
.venv/lib/python3.8/site-packages/black/__init__.py
Normal file
File diff suppressed because it is too large
Load diff
3
.venv/lib/python3.8/site-packages/black/__main__.py
Normal file
3
.venv/lib/python3.8/site-packages/black/__main__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from black import patched_main
|
||||
|
||||
patched_main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.venv/lib/python3.8/site-packages/black/brackets.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/brackets.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
334
.venv/lib/python3.8/site-packages/black/brackets.py
Normal file
334
.venv/lib/python3.8/site-packages/black/brackets.py
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
"""Builds on top of nodes.py to track brackets."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import sys
|
||||
from typing import Dict, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from typing_extensions import Final
|
||||
else:
|
||||
from typing import Final
|
||||
|
||||
from blib2to3.pytree import Leaf, Node
|
||||
from blib2to3.pgen2 import token
|
||||
|
||||
from black.nodes import syms, is_vararg, VARARGS_PARENTS, UNPACKING_PARENTS
|
||||
from black.nodes import BRACKET, OPENING_BRACKETS, CLOSING_BRACKETS
|
||||
from black.nodes import MATH_OPERATORS, COMPARATORS, LOGIC_OPERATORS
|
||||
|
||||
# types
|
||||
LN = Union[Leaf, Node]
|
||||
Depth = int
|
||||
LeafID = int
|
||||
NodeType = int
|
||||
Priority = int
|
||||
|
||||
|
||||
COMPREHENSION_PRIORITY: Final = 20
|
||||
COMMA_PRIORITY: Final = 18
|
||||
TERNARY_PRIORITY: Final = 16
|
||||
LOGIC_PRIORITY: Final = 14
|
||||
STRING_PRIORITY: Final = 12
|
||||
COMPARATOR_PRIORITY: Final = 10
|
||||
MATH_PRIORITIES: Final = {
|
||||
token.VBAR: 9,
|
||||
token.CIRCUMFLEX: 8,
|
||||
token.AMPER: 7,
|
||||
token.LEFTSHIFT: 6,
|
||||
token.RIGHTSHIFT: 6,
|
||||
token.PLUS: 5,
|
||||
token.MINUS: 5,
|
||||
token.STAR: 4,
|
||||
token.SLASH: 4,
|
||||
token.DOUBLESLASH: 4,
|
||||
token.PERCENT: 4,
|
||||
token.AT: 4,
|
||||
token.TILDE: 3,
|
||||
token.DOUBLESTAR: 2,
|
||||
}
|
||||
DOT_PRIORITY: Final = 1
|
||||
|
||||
|
||||
class BracketMatchError(Exception):
|
||||
"""Raised when an opening bracket is unable to be matched to a closing bracket."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class BracketTracker:
|
||||
"""Keeps track of brackets on a line."""
|
||||
|
||||
depth: int = 0
|
||||
bracket_match: Dict[Tuple[Depth, NodeType], Leaf] = field(default_factory=dict)
|
||||
delimiters: Dict[LeafID, Priority] = field(default_factory=dict)
|
||||
previous: Optional[Leaf] = None
|
||||
_for_loop_depths: List[int] = field(default_factory=list)
|
||||
_lambda_argument_depths: List[int] = field(default_factory=list)
|
||||
invisible: List[Leaf] = field(default_factory=list)
|
||||
|
||||
def mark(self, leaf: Leaf) -> None:
|
||||
"""Mark `leaf` with bracket-related metadata. Keep track of delimiters.
|
||||
|
||||
All leaves receive an int `bracket_depth` field that stores how deep
|
||||
within brackets a given leaf is. 0 means there are no enclosing brackets
|
||||
that started on this line.
|
||||
|
||||
If a leaf is itself a closing bracket, it receives an `opening_bracket`
|
||||
field that it forms a pair with. This is a one-directional link to
|
||||
avoid reference cycles.
|
||||
|
||||
If a leaf is a delimiter (a token on which Black can split the line if
|
||||
needed) and it's on depth 0, its `id()` is stored in the tracker's
|
||||
`delimiters` field.
|
||||
"""
|
||||
if leaf.type == token.COMMENT:
|
||||
return
|
||||
|
||||
self.maybe_decrement_after_for_loop_variable(leaf)
|
||||
self.maybe_decrement_after_lambda_arguments(leaf)
|
||||
if leaf.type in CLOSING_BRACKETS:
|
||||
self.depth -= 1
|
||||
try:
|
||||
opening_bracket = self.bracket_match.pop((self.depth, leaf.type))
|
||||
except KeyError as e:
|
||||
raise BracketMatchError(
|
||||
"Unable to match a closing bracket to the following opening"
|
||||
f" bracket: {leaf}"
|
||||
) from e
|
||||
leaf.opening_bracket = opening_bracket
|
||||
if not leaf.value:
|
||||
self.invisible.append(leaf)
|
||||
leaf.bracket_depth = self.depth
|
||||
if self.depth == 0:
|
||||
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
|
||||
if not leaf.value:
|
||||
self.invisible.append(leaf)
|
||||
self.previous = leaf
|
||||
self.maybe_increment_lambda_arguments(leaf)
|
||||
self.maybe_increment_for_loop_variable(leaf)
|
||||
|
||||
def any_open_brackets(self) -> bool:
|
||||
"""Return True if there is an yet unmatched open bracket on the line."""
|
||||
return bool(self.bracket_match)
|
||||
|
||||
def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority:
|
||||
"""Return the highest priority of a delimiter found on the line.
|
||||
|
||||
Values are consistent with what `is_split_*_delimiter()` return.
|
||||
Raises ValueError on no delimiters.
|
||||
"""
|
||||
return max(v for k, v in self.delimiters.items() if k not in exclude)
|
||||
|
||||
def delimiter_count_with_priority(self, priority: Priority = 0) -> int:
|
||||
"""Return the number of delimiters with the given `priority`.
|
||||
|
||||
If no `priority` is passed, defaults to max priority on the line.
|
||||
"""
|
||||
if not self.delimiters:
|
||||
return 0
|
||||
|
||||
priority = priority or self.max_delimiter_priority()
|
||||
return sum(1 for p in self.delimiters.values() if p == priority)
|
||||
|
||||
def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool:
|
||||
"""In a for loop, or comprehension, the variables are often unpacks.
|
||||
|
||||
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":
|
||||
self.depth += 1
|
||||
self._for_loop_depths.append(self.depth)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool:
|
||||
"""See `maybe_increment_for_loop_variable` above for explanation."""
|
||||
if (
|
||||
self._for_loop_depths
|
||||
and self._for_loop_depths[-1] == self.depth
|
||||
and leaf.type == token.NAME
|
||||
and leaf.value == "in"
|
||||
):
|
||||
self.depth -= 1
|
||||
self._for_loop_depths.pop()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool:
|
||||
"""In a lambda expression, there might be more than one argument.
|
||||
|
||||
To avoid splitting on the comma in this situation, increase the depth of
|
||||
tokens between `lambda` and `:`.
|
||||
"""
|
||||
if leaf.type == token.NAME and leaf.value == "lambda":
|
||||
self.depth += 1
|
||||
self._lambda_argument_depths.append(self.depth)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def maybe_decrement_after_lambda_arguments(self, leaf: Leaf) -> bool:
|
||||
"""See `maybe_increment_lambda_arguments` above for explanation."""
|
||||
if (
|
||||
self._lambda_argument_depths
|
||||
and self._lambda_argument_depths[-1] == self.depth
|
||||
and leaf.type == token.COLON
|
||||
):
|
||||
self.depth -= 1
|
||||
self._lambda_argument_depths.pop()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_open_lsqb(self) -> Optional[Leaf]:
|
||||
"""Return the most recent opening square bracket (if any)."""
|
||||
return self.bracket_match.get((self.depth - 1, token.RSQB))
|
||||
|
||||
|
||||
def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority:
|
||||
"""Return the priority of the `leaf` delimiter, given a line break after it.
|
||||
|
||||
The delimiter priorities returned here are from those delimiters that would
|
||||
cause a line break after themselves.
|
||||
|
||||
Higher numbers are higher priority.
|
||||
"""
|
||||
if leaf.type == token.COMMA:
|
||||
return COMMA_PRIORITY
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority:
|
||||
"""Return the priority of the `leaf` delimiter, given a line break before it.
|
||||
|
||||
The delimiter priorities returned here are from those delimiters that would
|
||||
cause a line break before themselves.
|
||||
|
||||
Higher numbers are higher priority.
|
||||
"""
|
||||
if is_vararg(leaf, within=VARARGS_PARENTS | UNPACKING_PARENTS):
|
||||
# * 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 == token.DOT
|
||||
and leaf.parent
|
||||
and leaf.parent.type not in {syms.import_from, syms.dotted_name}
|
||||
and (previous is None or previous.type in CLOSING_BRACKETS)
|
||||
):
|
||||
return DOT_PRIORITY
|
||||
|
||||
if (
|
||||
leaf.type in MATH_OPERATORS
|
||||
and leaf.parent
|
||||
and leaf.parent.type not in {syms.factor, syms.star_expr}
|
||||
):
|
||||
return MATH_PRIORITIES[leaf.type]
|
||||
|
||||
if leaf.type in COMPARATORS:
|
||||
return COMPARATOR_PRIORITY
|
||||
|
||||
if (
|
||||
leaf.type == token.STRING
|
||||
and previous is not None
|
||||
and previous.type == token.STRING
|
||||
):
|
||||
return STRING_PRIORITY
|
||||
|
||||
if leaf.type not in {token.NAME, token.ASYNC}:
|
||||
return 0
|
||||
|
||||
if (
|
||||
leaf.value == "for"
|
||||
and leaf.parent
|
||||
and leaf.parent.type in {syms.comp_for, syms.old_comp_for}
|
||||
or leaf.type == token.ASYNC
|
||||
):
|
||||
if (
|
||||
not isinstance(leaf.prev_sibling, Leaf)
|
||||
or leaf.prev_sibling.value != "async"
|
||||
):
|
||||
return COMPREHENSION_PRIORITY
|
||||
|
||||
if (
|
||||
leaf.value == "if"
|
||||
and leaf.parent
|
||||
and leaf.parent.type in {syms.comp_if, syms.old_comp_if}
|
||||
):
|
||||
return COMPREHENSION_PRIORITY
|
||||
|
||||
if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test:
|
||||
return TERNARY_PRIORITY
|
||||
|
||||
if leaf.value == "is":
|
||||
return COMPARATOR_PRIORITY
|
||||
|
||||
if (
|
||||
leaf.value == "in"
|
||||
and leaf.parent
|
||||
and leaf.parent.type in {syms.comp_op, syms.comparison}
|
||||
and not (
|
||||
previous is not None
|
||||
and previous.type == token.NAME
|
||||
and previous.value == "not"
|
||||
)
|
||||
):
|
||||
return COMPARATOR_PRIORITY
|
||||
|
||||
if (
|
||||
leaf.value == "not"
|
||||
and leaf.parent
|
||||
and leaf.parent.type == syms.comp_op
|
||||
and not (
|
||||
previous is not None
|
||||
and previous.type == token.NAME
|
||||
and previous.value == "is"
|
||||
)
|
||||
):
|
||||
return COMPARATOR_PRIORITY
|
||||
|
||||
if leaf.value in LOGIC_OPERATORS and leaf.parent:
|
||||
return LOGIC_PRIORITY
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def max_delimiter_priority_in_atom(node: LN) -> Priority:
|
||||
"""Return maximum delimiter priority inside `node`.
|
||||
|
||||
This is specific to atoms with contents contained in a pair of parentheses.
|
||||
If `node` isn't an atom or there are no enclosing parentheses, returns 0.
|
||||
"""
|
||||
if node.type != syms.atom:
|
||||
return 0
|
||||
|
||||
first = node.children[0]
|
||||
last = node.children[-1]
|
||||
if not (first.type == token.LPAR and last.type == token.RPAR):
|
||||
return 0
|
||||
|
||||
bt = BracketTracker()
|
||||
for c in node.children[1:-1]:
|
||||
if isinstance(c, Leaf):
|
||||
bt.mark(c)
|
||||
else:
|
||||
for leaf in c.leaves():
|
||||
bt.mark(leaf)
|
||||
try:
|
||||
return bt.max_delimiter_priority()
|
||||
|
||||
except ValueError:
|
||||
return 0
|
||||
BIN
.venv/lib/python3.8/site-packages/black/cache.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/cache.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
99
.venv/lib/python3.8/site-packages/black/cache.py
Normal file
99
.venv/lib/python3.8/site-packages/black/cache.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"""Caching of formatted files with feature-based invalidation."""
|
||||
|
||||
import os
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Dict, Iterable, Set, Tuple
|
||||
|
||||
from platformdirs import user_cache_dir
|
||||
|
||||
from black.mode import Mode
|
||||
|
||||
from _black_version import version as __version__
|
||||
|
||||
|
||||
# types
|
||||
Timestamp = float
|
||||
FileSize = int
|
||||
CacheInfo = Tuple[Timestamp, FileSize]
|
||||
Cache = Dict[str, CacheInfo]
|
||||
|
||||
|
||||
def get_cache_dir() -> Path:
|
||||
"""Get the cache directory used by black.
|
||||
|
||||
Users can customize this directory on all systems using `BLACK_CACHE_DIR`
|
||||
environment variable. By default, the cache directory is the user cache directory
|
||||
under the black application.
|
||||
|
||||
This result is immediately set to a constant `black.cache.CACHE_DIR` as to avoid
|
||||
repeated calls.
|
||||
"""
|
||||
# NOTE: Function mostly exists as a clean way to test getting the cache directory.
|
||||
default_cache_dir = user_cache_dir("black", version=__version__)
|
||||
cache_dir = Path(os.environ.get("BLACK_CACHE_DIR", default_cache_dir))
|
||||
return cache_dir
|
||||
|
||||
|
||||
CACHE_DIR = get_cache_dir()
|
||||
|
||||
|
||||
def read_cache(mode: Mode) -> Cache:
|
||||
"""Read the cache if it exists and is well formed.
|
||||
|
||||
If it is not well formed, the call to write_cache later should resolve the issue.
|
||||
"""
|
||||
cache_file = get_cache_file(mode)
|
||||
if not cache_file.exists():
|
||||
return {}
|
||||
|
||||
with cache_file.open("rb") as fobj:
|
||||
try:
|
||||
cache: Cache = pickle.load(fobj)
|
||||
except (pickle.UnpicklingError, ValueError, IndexError):
|
||||
return {}
|
||||
|
||||
return cache
|
||||
|
||||
|
||||
def get_cache_file(mode: Mode) -> Path:
|
||||
return CACHE_DIR / f"cache.{mode.get_cache_key()}.pickle"
|
||||
|
||||
|
||||
def get_cache_info(path: Path) -> CacheInfo:
|
||||
"""Return the information used to check if a file is already formatted or not."""
|
||||
stat = path.stat()
|
||||
return stat.st_mtime, stat.st_size
|
||||
|
||||
|
||||
def filter_cached(cache: Cache, sources: Iterable[Path]) -> Tuple[Set[Path], Set[Path]]:
|
||||
"""Split an iterable of paths in `sources` into two sets.
|
||||
|
||||
The first contains paths of files that modified on disk or are not in the
|
||||
cache. The other contains paths to non-modified files.
|
||||
"""
|
||||
todo, done = set(), set()
|
||||
for src in sources:
|
||||
res_src = src.resolve()
|
||||
if cache.get(str(res_src)) != get_cache_info(res_src):
|
||||
todo.add(src)
|
||||
else:
|
||||
done.add(src)
|
||||
return todo, done
|
||||
|
||||
|
||||
def write_cache(cache: Cache, sources: Iterable[Path], mode: Mode) -> None:
|
||||
"""Update the cache file."""
|
||||
cache_file = get_cache_file(mode)
|
||||
try:
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
new_cache = {
|
||||
**cache,
|
||||
**{str(src.resolve()): get_cache_info(src) for src in sources},
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(dir=str(cache_file.parent), delete=False) as f:
|
||||
pickle.dump(new_cache, f, protocol=4)
|
||||
os.replace(f.name, cache_file)
|
||||
except OSError:
|
||||
pass
|
||||
BIN
.venv/lib/python3.8/site-packages/black/comments.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/comments.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
285
.venv/lib/python3.8/site-packages/black/comments.py
Normal file
285
.venv/lib/python3.8/site-packages/black/comments.py
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import sys
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
import re
|
||||
from typing import Iterator, List, Optional, Union
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Final
|
||||
else:
|
||||
from typing_extensions import Final
|
||||
|
||||
from blib2to3.pytree import Node, Leaf
|
||||
from blib2to3.pgen2 import token
|
||||
|
||||
from black.nodes import first_leaf_column, preceding_leaf, container_of
|
||||
from black.nodes import STANDALONE_COMMENT, WHITESPACE
|
||||
|
||||
# types
|
||||
LN = Union[Leaf, Node]
|
||||
|
||||
FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"}
|
||||
FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"}
|
||||
FMT_PASS: Final = {*FMT_OFF, *FMT_SKIP}
|
||||
FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"}
|
||||
|
||||
COMMENT_EXCEPTIONS = {True: " !:#'", False: " !:#'%"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProtoComment:
|
||||
"""Describes a piece of syntax that is a comment.
|
||||
|
||||
It's not a :class:`blib2to3.pytree.Leaf` so that:
|
||||
|
||||
* it can be cached (`Leaf` objects should not be reused more than once as
|
||||
they store their lineno, column, prefix, and parent information);
|
||||
* `newlines` and `consumed` fields are kept separate from the `value`. This
|
||||
simplifies handling of special marker comments like ``# fmt: off/on``.
|
||||
"""
|
||||
|
||||
type: int # token.COMMENT or STANDALONE_COMMENT
|
||||
value: str # content of the comment
|
||||
newlines: int # how many newlines before the comment
|
||||
consumed: int # how many characters of the original leaf's prefix did we consume
|
||||
|
||||
|
||||
def generate_comments(leaf: LN, *, preview: bool) -> Iterator[Leaf]:
|
||||
"""Clean the prefix of the `leaf` and generate comments from it, if any.
|
||||
|
||||
Comments in lib2to3 are shoved into the whitespace prefix. This happens
|
||||
in `pgen2/driver.py:Driver.parse_tokens()`. This was a brilliant implementation
|
||||
move because it does away with modifying the grammar to include all the
|
||||
possible places in which comments can be placed.
|
||||
|
||||
The sad consequence for us though is that comments don't "belong" anywhere.
|
||||
This is why this function generates simple parentless Leaf objects for
|
||||
comments. We simply don't know what the correct parent should be.
|
||||
|
||||
No matter though, we can live without this. We really only need to
|
||||
differentiate between inline and standalone comments. The latter don't
|
||||
share the line with any code.
|
||||
|
||||
Inline comments are emitted as regular token.COMMENT leaves. Standalone
|
||||
are emitted with a fake STANDALONE_COMMENT token identifier.
|
||||
"""
|
||||
for pc in list_comments(
|
||||
leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER, preview=preview
|
||||
):
|
||||
yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines)
|
||||
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def list_comments(
|
||||
prefix: str, *, is_endmarker: bool, preview: bool
|
||||
) -> List[ProtoComment]:
|
||||
"""Return a list of :class:`ProtoComment` objects parsed from the given `prefix`."""
|
||||
result: List[ProtoComment] = []
|
||||
if not prefix or "#" not in prefix:
|
||||
return result
|
||||
|
||||
consumed = 0
|
||||
nlines = 0
|
||||
ignored_lines = 0
|
||||
for index, line in enumerate(re.split("\r?\n", prefix)):
|
||||
consumed += len(line) + 1 # adding the length of the split '\n'
|
||||
line = line.lstrip()
|
||||
if not line:
|
||||
nlines += 1
|
||||
if not line.startswith("#"):
|
||||
# Escaped newlines outside of a comment are not really newlines at
|
||||
# all. We treat a single-line comment following an escaped newline
|
||||
# as a simple trailing comment.
|
||||
if line.endswith("\\"):
|
||||
ignored_lines += 1
|
||||
continue
|
||||
|
||||
if index == ignored_lines and not is_endmarker:
|
||||
comment_type = token.COMMENT # simple trailing comment
|
||||
else:
|
||||
comment_type = STANDALONE_COMMENT
|
||||
comment = make_comment(line, preview=preview)
|
||||
result.append(
|
||||
ProtoComment(
|
||||
type=comment_type, value=comment, newlines=nlines, consumed=consumed
|
||||
)
|
||||
)
|
||||
nlines = 0
|
||||
return result
|
||||
|
||||
|
||||
def make_comment(content: str, *, preview: bool) -> str:
|
||||
"""Return a consistently formatted comment from the given `content` string.
|
||||
|
||||
All comments (except for "##", "#!", "#:", '#'") should have a single
|
||||
space between the hash sign and the content.
|
||||
|
||||
If `content` didn't start with a hash sign, one is provided.
|
||||
"""
|
||||
content = content.rstrip()
|
||||
if not content:
|
||||
return "#"
|
||||
|
||||
if content[0] == "#":
|
||||
content = content[1:]
|
||||
NON_BREAKING_SPACE = " "
|
||||
if (
|
||||
content
|
||||
and content[0] == NON_BREAKING_SPACE
|
||||
and not content.lstrip().startswith("type:")
|
||||
):
|
||||
content = " " + content[1:] # Replace NBSP by a simple space
|
||||
if content and content[0] not in COMMENT_EXCEPTIONS[preview]:
|
||||
content = " " + content
|
||||
return "#" + content
|
||||
|
||||
|
||||
def normalize_fmt_off(node: Node, *, preview: bool) -> None:
|
||||
"""Convert content between `# fmt: off`/`# fmt: on` into standalone comments."""
|
||||
try_again = True
|
||||
while try_again:
|
||||
try_again = convert_one_fmt_off_pair(node, preview=preview)
|
||||
|
||||
|
||||
def convert_one_fmt_off_pair(node: Node, *, preview: bool) -> bool:
|
||||
"""Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment.
|
||||
|
||||
Returns True if a pair was converted.
|
||||
"""
|
||||
for leaf in node.leaves():
|
||||
previous_consumed = 0
|
||||
for comment in list_comments(leaf.prefix, is_endmarker=False, preview=preview):
|
||||
if comment.value not in FMT_PASS:
|
||||
previous_consumed = comment.consumed
|
||||
continue
|
||||
# We only want standalone comments. If there's no previous leaf or
|
||||
# the previous leaf is indentation, it's a standalone comment in
|
||||
# disguise.
|
||||
if comment.value in FMT_PASS and comment.type != STANDALONE_COMMENT:
|
||||
prev = preceding_leaf(leaf)
|
||||
if prev:
|
||||
if comment.value in FMT_OFF and prev.type not in WHITESPACE:
|
||||
continue
|
||||
if comment.value in FMT_SKIP and prev.type in WHITESPACE:
|
||||
continue
|
||||
|
||||
ignored_nodes = list(generate_ignored_nodes(leaf, comment, preview=preview))
|
||||
if not ignored_nodes:
|
||||
continue
|
||||
|
||||
first = ignored_nodes[0] # Can be a container node with the `leaf`.
|
||||
parent = first.parent
|
||||
prefix = first.prefix
|
||||
if comment.value in FMT_OFF:
|
||||
first.prefix = prefix[comment.consumed :]
|
||||
if comment.value in FMT_SKIP:
|
||||
first.prefix = ""
|
||||
hidden_value = "".join(str(n) for n in ignored_nodes)
|
||||
if comment.value in FMT_OFF:
|
||||
hidden_value = comment.value + "\n" + hidden_value
|
||||
if comment.value in FMT_SKIP:
|
||||
hidden_value += " " + comment.value
|
||||
if hidden_value.endswith("\n"):
|
||||
# That happens when one of the `ignored_nodes` ended with a NEWLINE
|
||||
# leaf (possibly followed by a DEDENT).
|
||||
hidden_value = hidden_value[:-1]
|
||||
first_idx: Optional[int] = None
|
||||
for ignored in ignored_nodes:
|
||||
index = ignored.remove()
|
||||
if first_idx is None:
|
||||
first_idx = index
|
||||
assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)"
|
||||
assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)"
|
||||
parent.insert_child(
|
||||
first_idx,
|
||||
Leaf(
|
||||
STANDALONE_COMMENT,
|
||||
hidden_value,
|
||||
prefix=prefix[:previous_consumed] + "\n" * comment.newlines,
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def generate_ignored_nodes(
|
||||
leaf: Leaf, comment: ProtoComment, *, preview: bool
|
||||
) -> Iterator[LN]:
|
||||
"""Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
|
||||
|
||||
If comment is skip, returns leaf only.
|
||||
Stops at the end of the block.
|
||||
"""
|
||||
container: Optional[LN] = container_of(leaf)
|
||||
if comment.value in FMT_SKIP:
|
||||
prev_sibling = leaf.prev_sibling
|
||||
if comment.value in leaf.prefix and prev_sibling is not None:
|
||||
leaf.prefix = leaf.prefix.replace(comment.value, "")
|
||||
siblings = [prev_sibling]
|
||||
while (
|
||||
"\n" not in prev_sibling.prefix
|
||||
and prev_sibling.prev_sibling is not None
|
||||
):
|
||||
prev_sibling = prev_sibling.prev_sibling
|
||||
siblings.insert(0, prev_sibling)
|
||||
for sibling in siblings:
|
||||
yield sibling
|
||||
elif leaf.parent is not None:
|
||||
yield leaf.parent
|
||||
return
|
||||
while container is not None and container.type != token.ENDMARKER:
|
||||
if is_fmt_on(container, preview=preview):
|
||||
return
|
||||
|
||||
# fix for fmt: on in children
|
||||
if contains_fmt_on_at_column(container, leaf.column, preview=preview):
|
||||
for child in container.children:
|
||||
if contains_fmt_on_at_column(child, leaf.column, preview=preview):
|
||||
return
|
||||
yield child
|
||||
else:
|
||||
yield container
|
||||
container = container.next_sibling
|
||||
|
||||
|
||||
def is_fmt_on(container: LN, preview: bool) -> bool:
|
||||
"""Determine whether formatting is switched on within a container.
|
||||
Determined by whether the last `# fmt:` comment is `on` or `off`.
|
||||
"""
|
||||
fmt_on = False
|
||||
for comment in list_comments(container.prefix, is_endmarker=False, preview=preview):
|
||||
if comment.value in FMT_ON:
|
||||
fmt_on = True
|
||||
elif comment.value in FMT_OFF:
|
||||
fmt_on = False
|
||||
return fmt_on
|
||||
|
||||
|
||||
def contains_fmt_on_at_column(container: LN, column: int, *, preview: bool) -> bool:
|
||||
"""Determine if children at a given column have formatting switched on."""
|
||||
for child in container.children:
|
||||
if (
|
||||
isinstance(child, Node)
|
||||
and first_leaf_column(child) == column
|
||||
or isinstance(child, Leaf)
|
||||
and child.column == column
|
||||
):
|
||||
if is_fmt_on(child, preview=preview):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def contains_pragma_comment(comment_list: List[Leaf]) -> bool:
|
||||
"""
|
||||
Returns:
|
||||
True iff one of the comments in @comment_list is a pragma used by one
|
||||
of the more common static analysis tools for python (e.g. mypy, flake8,
|
||||
pylint).
|
||||
"""
|
||||
for comment in comment_list:
|
||||
if comment.value.startswith(("# type:", "# noqa", "# pylint:")):
|
||||
return True
|
||||
|
||||
return False
|
||||
57
.venv/lib/python3.8/site-packages/black/concurrency.py
Normal file
57
.venv/lib/python3.8/site-packages/black/concurrency.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, Iterable
|
||||
|
||||
from black.output import err
|
||||
|
||||
|
||||
def maybe_install_uvloop() -> None:
|
||||
"""If our environment has uvloop installed we use it.
|
||||
|
||||
This is called only from command-line entry points to avoid
|
||||
interfering with the parent process if Black is used as a library.
|
||||
|
||||
"""
|
||||
try:
|
||||
import uvloop
|
||||
|
||||
uvloop.install()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None:
|
||||
"""asyncio signal handler that cancels all `tasks` and reports to stderr."""
|
||||
err("Aborted!")
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
|
||||
def shutdown(loop: asyncio.AbstractEventLoop) -> None:
|
||||
"""Cancel all pending tasks on `loop`, wait for them, and close the loop."""
|
||||
try:
|
||||
if sys.version_info[:2] >= (3, 7):
|
||||
all_tasks = asyncio.all_tasks
|
||||
else:
|
||||
all_tasks = asyncio.Task.all_tasks
|
||||
# This part is borrowed from asyncio/runners.py in Python 3.7b2.
|
||||
to_cancel = [task for task in all_tasks(loop) if not task.done()]
|
||||
if not to_cancel:
|
||||
return
|
||||
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
if sys.version_info >= (3, 7):
|
||||
loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True))
|
||||
else:
|
||||
loop.run_until_complete(
|
||||
asyncio.gather(*to_cancel, loop=loop, return_exceptions=True)
|
||||
)
|
||||
finally:
|
||||
# `concurrent.futures.Future` objects cannot be cancelled once they
|
||||
# are already running. There might be some when the `shutdown()` happened.
|
||||
# Silence their logger's spew about the event loop being closed.
|
||||
cf_logger = logging.getLogger("concurrent.futures")
|
||||
cf_logger.setLevel(logging.CRITICAL)
|
||||
loop.close()
|
||||
BIN
.venv/lib/python3.8/site-packages/black/const.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/const.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
4
.venv/lib/python3.8/site-packages/black/const.py
Normal file
4
.venv/lib/python3.8/site-packages/black/const.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
DEFAULT_LINE_LENGTH = 88
|
||||
DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.svn|_build|buck-out|build|dist|__pypackages__)/" # noqa: B950
|
||||
DEFAULT_INCLUDES = r"(\.pyi?|\.ipynb)$"
|
||||
STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__"
|
||||
48
.venv/lib/python3.8/site-packages/black/debug.py
Normal file
48
.venv/lib/python3.8/site-packages/black/debug.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Iterator, TypeVar, Union
|
||||
|
||||
from blib2to3.pytree import Node, Leaf, type_repr
|
||||
from blib2to3.pgen2 import token
|
||||
|
||||
from black.nodes import Visitor
|
||||
from black.output import out
|
||||
from black.parsing import lib2to3_parse
|
||||
|
||||
LN = Union[Leaf, Node]
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DebugVisitor(Visitor[T]):
|
||||
tree_depth: int = 0
|
||||
|
||||
def visit_default(self, node: LN) -> Iterator[T]:
|
||||
indent = " " * (2 * self.tree_depth)
|
||||
if isinstance(node, Node):
|
||||
_type = type_repr(node.type)
|
||||
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)
|
||||
else:
|
||||
_type = token.tok_name.get(node.type, str(node.type))
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def show(cls, code: Union[str, Leaf, Node]) -> None:
|
||||
"""Pretty-print the lib2to3 AST of a given string of `code`.
|
||||
|
||||
Convenience method for debugging.
|
||||
"""
|
||||
v: DebugVisitor[None] = DebugVisitor()
|
||||
if isinstance(code, str):
|
||||
code = lib2to3_parse(code)
|
||||
list(v.visit(code))
|
||||
278
.venv/lib/python3.8/site-packages/black/files.py
Normal file
278
.venv/lib/python3.8/site-packages/black/files.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
from functools import lru_cache
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Pattern,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from mypy_extensions import mypyc_attr
|
||||
from pathspec import PathSpec
|
||||
from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
import tomli as tomllib
|
||||
|
||||
from black.output import err
|
||||
from black.report import Report
|
||||
from black.handle_ipynb_magics import jupyter_dependencies_are_installed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import colorama # noqa: F401
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def find_project_root(srcs: Sequence[str]) -> Tuple[Path, str]:
|
||||
"""Return a directory containing .git, .hg, or pyproject.toml.
|
||||
|
||||
That directory will be a common parent of all files and directories
|
||||
passed in `srcs`.
|
||||
|
||||
If no directory in the tree contains a marker that would specify it's the
|
||||
project root, the root of the file system is returned.
|
||||
|
||||
Returns a two-tuple with the first element as the project root path and
|
||||
the second element as a string describing the method by which the
|
||||
project root was discovered.
|
||||
"""
|
||||
if not srcs:
|
||||
srcs = [str(Path.cwd().resolve())]
|
||||
|
||||
path_srcs = [Path(Path.cwd(), src).resolve() for src in srcs]
|
||||
|
||||
# A list of lists of parents for each 'src'. 'src' is included as a
|
||||
# "parent" of itself if it is a directory
|
||||
src_parents = [
|
||||
list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs
|
||||
]
|
||||
|
||||
common_base = max(
|
||||
set.intersection(*(set(parents) for parents in src_parents)),
|
||||
key=lambda path: path.parts,
|
||||
)
|
||||
|
||||
for directory in (common_base, *common_base.parents):
|
||||
if (directory / ".git").exists():
|
||||
return directory, ".git directory"
|
||||
|
||||
if (directory / ".hg").is_dir():
|
||||
return directory, ".hg directory"
|
||||
|
||||
if (directory / "pyproject.toml").is_file():
|
||||
return directory, "pyproject.toml"
|
||||
|
||||
return directory, "file system root"
|
||||
|
||||
|
||||
def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
|
||||
"""Find the absolute filepath to a pyproject.toml if it exists"""
|
||||
path_project_root, _ = find_project_root(path_search_start)
|
||||
path_pyproject_toml = path_project_root / "pyproject.toml"
|
||||
if path_pyproject_toml.is_file():
|
||||
return str(path_pyproject_toml)
|
||||
|
||||
try:
|
||||
path_user_pyproject_toml = find_user_pyproject_toml()
|
||||
return (
|
||||
str(path_user_pyproject_toml)
|
||||
if path_user_pyproject_toml.is_file()
|
||||
else None
|
||||
)
|
||||
except (PermissionError, RuntimeError) as e:
|
||||
# We do not have access to the user-level config directory, so ignore it.
|
||||
err(f"Ignoring user configuration directory due to {e!r}")
|
||||
return None
|
||||
|
||||
|
||||
@mypyc_attr(patchable=True)
|
||||
def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
|
||||
"""Parse a pyproject toml file, pulling out relevant parts for Black
|
||||
|
||||
If parsing fails, will raise a tomllib.TOMLDecodeError
|
||||
"""
|
||||
with open(path_config, "rb") as f:
|
||||
pyproject_toml = tomllib.load(f)
|
||||
config = pyproject_toml.get("tool", {}).get("black", {})
|
||||
return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def find_user_pyproject_toml() -> Path:
|
||||
r"""Return the path to the top-level user configuration for black.
|
||||
|
||||
This looks for ~\.black on Windows and ~/.config/black on Linux and other
|
||||
Unix systems.
|
||||
|
||||
May raise:
|
||||
- RuntimeError: if the current user has no homedir
|
||||
- PermissionError: if the current process cannot access the user's homedir
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
# Windows
|
||||
user_config_path = Path.home() / ".black"
|
||||
else:
|
||||
config_root = os.environ.get("XDG_CONFIG_HOME", "~/.config")
|
||||
user_config_path = Path(config_root).expanduser() / "black"
|
||||
return user_config_path.resolve()
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_gitignore(root: Path) -> PathSpec:
|
||||
"""Return a PathSpec matching gitignore content if present."""
|
||||
gitignore = root / ".gitignore"
|
||||
lines: List[str] = []
|
||||
if gitignore.is_file():
|
||||
with gitignore.open(encoding="utf-8") as gf:
|
||||
lines = gf.readlines()
|
||||
try:
|
||||
return PathSpec.from_lines("gitwildmatch", lines)
|
||||
except GitWildMatchPatternError as e:
|
||||
err(f"Could not parse {gitignore}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def normalize_path_maybe_ignore(
|
||||
path: Path,
|
||||
root: Path,
|
||||
report: Optional[Report] = None,
|
||||
) -> Optional[str]:
|
||||
"""Normalize `path`. May return `None` if `path` was ignored.
|
||||
|
||||
`report` is where "path ignored" output goes.
|
||||
"""
|
||||
try:
|
||||
abspath = path if path.is_absolute() else Path.cwd() / path
|
||||
normalized_path = abspath.resolve()
|
||||
try:
|
||||
root_relative_path = normalized_path.relative_to(root).as_posix()
|
||||
except ValueError:
|
||||
if report:
|
||||
report.path_ignored(
|
||||
path, f"is a symbolic link that points outside {root}"
|
||||
)
|
||||
return None
|
||||
|
||||
except OSError as e:
|
||||
if report:
|
||||
report.path_ignored(path, f"cannot be read because {e}")
|
||||
return None
|
||||
|
||||
return root_relative_path
|
||||
|
||||
|
||||
def path_is_excluded(
|
||||
normalized_path: str,
|
||||
pattern: Optional[Pattern[str]],
|
||||
) -> bool:
|
||||
match = pattern.search(normalized_path) if pattern else None
|
||||
return bool(match and match.group(0))
|
||||
|
||||
|
||||
def gen_python_files(
|
||||
paths: Iterable[Path],
|
||||
root: Path,
|
||||
include: Pattern[str],
|
||||
exclude: Pattern[str],
|
||||
extend_exclude: Optional[Pattern[str]],
|
||||
force_exclude: Optional[Pattern[str]],
|
||||
report: Report,
|
||||
gitignore: Optional[PathSpec],
|
||||
*,
|
||||
verbose: bool,
|
||||
quiet: bool,
|
||||
) -> Iterator[Path]:
|
||||
"""Generate all files under `path` whose paths are not excluded by the
|
||||
`exclude_regex`, `extend_exclude`, or `force_exclude` regexes,
|
||||
but are included by the `include` regex.
|
||||
|
||||
Symbolic links pointing outside of the `root` directory are ignored.
|
||||
|
||||
`report` is where output about exclusions goes.
|
||||
"""
|
||||
assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
|
||||
for child in paths:
|
||||
normalized_path = normalize_path_maybe_ignore(child, root, report)
|
||||
if normalized_path is None:
|
||||
continue
|
||||
|
||||
# First ignore files matching .gitignore, if passed
|
||||
if gitignore is not None and gitignore.match_file(normalized_path):
|
||||
report.path_ignored(child, "matches the .gitignore file content")
|
||||
continue
|
||||
|
||||
# Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options.
|
||||
normalized_path = "/" + normalized_path
|
||||
if child.is_dir():
|
||||
normalized_path += "/"
|
||||
|
||||
if path_is_excluded(normalized_path, exclude):
|
||||
report.path_ignored(child, "matches the --exclude regular expression")
|
||||
continue
|
||||
|
||||
if path_is_excluded(normalized_path, extend_exclude):
|
||||
report.path_ignored(
|
||||
child, "matches the --extend-exclude regular expression"
|
||||
)
|
||||
continue
|
||||
|
||||
if path_is_excluded(normalized_path, force_exclude):
|
||||
report.path_ignored(child, "matches the --force-exclude regular expression")
|
||||
continue
|
||||
|
||||
if child.is_dir():
|
||||
# If gitignore is None, gitignore usage is disabled, while a Falsey
|
||||
# gitignore is when the directory doesn't have a .gitignore file.
|
||||
yield from gen_python_files(
|
||||
child.iterdir(),
|
||||
root,
|
||||
include,
|
||||
exclude,
|
||||
extend_exclude,
|
||||
force_exclude,
|
||||
report,
|
||||
gitignore + get_gitignore(child) if gitignore is not None else None,
|
||||
verbose=verbose,
|
||||
quiet=quiet,
|
||||
)
|
||||
|
||||
elif child.is_file():
|
||||
if child.suffix == ".ipynb" and not jupyter_dependencies_are_installed(
|
||||
verbose=verbose, quiet=quiet
|
||||
):
|
||||
continue
|
||||
include_match = include.search(normalized_path) if include else True
|
||||
if include_match:
|
||||
yield child
|
||||
|
||||
|
||||
def wrap_stream_for_windows(
|
||||
f: io.TextIOWrapper,
|
||||
) -> Union[io.TextIOWrapper, "colorama.AnsiToWin32"]:
|
||||
"""
|
||||
Wrap stream with colorama's wrap_stream so colors are shown on Windows.
|
||||
|
||||
If `colorama` is unavailable, the original stream is returned unmodified.
|
||||
Otherwise, the `wrap_stream()` function determines whether the stream needs
|
||||
to be wrapped for a Windows environment and will accordingly either return
|
||||
an `AnsiToWin32` wrapper or the original stream.
|
||||
"""
|
||||
try:
|
||||
from colorama.initialise import wrap_stream
|
||||
except ImportError:
|
||||
return f
|
||||
else:
|
||||
# Set `strip=False` to avoid needing to modify test_express_diff_with_color.
|
||||
return wrap_stream(f, convert=None, strip=False, autoreset=False, wrap=True)
|
||||
Binary file not shown.
465
.venv/lib/python3.8/site-packages/black/handle_ipynb_magics.py
Normal file
465
.venv/lib/python3.8/site-packages/black/handle_ipynb_magics.py
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
"""Functions to process IPython magics with."""
|
||||
|
||||
from functools import lru_cache
|
||||
import dataclasses
|
||||
import ast
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
import secrets
|
||||
import sys
|
||||
import collections
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeGuard
|
||||
else:
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from black.report import NothingChanged
|
||||
from black.output import out
|
||||
|
||||
|
||||
TRANSFORMED_MAGICS = frozenset(
|
||||
(
|
||||
"get_ipython().run_cell_magic",
|
||||
"get_ipython().system",
|
||||
"get_ipython().getoutput",
|
||||
"get_ipython().run_line_magic",
|
||||
)
|
||||
)
|
||||
TOKENS_TO_IGNORE = frozenset(
|
||||
(
|
||||
"ENDMARKER",
|
||||
"NL",
|
||||
"NEWLINE",
|
||||
"COMMENT",
|
||||
"DEDENT",
|
||||
"UNIMPORTANT_WS",
|
||||
"ESCAPED_NL",
|
||||
)
|
||||
)
|
||||
PYTHON_CELL_MAGICS = frozenset(
|
||||
(
|
||||
"capture",
|
||||
"prun",
|
||||
"pypy",
|
||||
"python",
|
||||
"python3",
|
||||
"time",
|
||||
"timeit",
|
||||
)
|
||||
)
|
||||
TOKEN_HEX = secrets.token_hex
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Replacement:
|
||||
mask: str
|
||||
src: str
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool:
|
||||
try:
|
||||
import IPython # noqa:F401
|
||||
import tokenize_rt # noqa:F401
|
||||
except ModuleNotFoundError:
|
||||
if verbose or not quiet:
|
||||
msg = (
|
||||
"Skipping .ipynb files as Jupyter dependencies are not installed.\n"
|
||||
"You can fix this by running ``pip install black[jupyter]``"
|
||||
)
|
||||
out(msg)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def remove_trailing_semicolon(src: str) -> Tuple[str, bool]:
|
||||
"""Remove trailing semicolon from Jupyter notebook cell.
|
||||
|
||||
For example,
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
ax.plot(x_data, y_data); # plot data
|
||||
|
||||
would become
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
ax.plot(x_data, y_data) # plot data
|
||||
|
||||
Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses
|
||||
``tokenize_rt`` so that round-tripping works fine.
|
||||
"""
|
||||
from tokenize_rt import (
|
||||
src_to_tokens,
|
||||
tokens_to_src,
|
||||
reversed_enumerate,
|
||||
)
|
||||
|
||||
tokens = src_to_tokens(src)
|
||||
trailing_semicolon = False
|
||||
for idx, token in reversed_enumerate(tokens):
|
||||
if token.name in TOKENS_TO_IGNORE:
|
||||
continue
|
||||
if token.name == "OP" and token.src == ";":
|
||||
del tokens[idx]
|
||||
trailing_semicolon = True
|
||||
break
|
||||
if not trailing_semicolon:
|
||||
return src, False
|
||||
return tokens_to_src(tokens), True
|
||||
|
||||
|
||||
def put_trailing_semicolon_back(src: str, has_trailing_semicolon: bool) -> str:
|
||||
"""Put trailing semicolon back if cell originally had it.
|
||||
|
||||
Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses
|
||||
``tokenize_rt`` so that round-tripping works fine.
|
||||
"""
|
||||
if not has_trailing_semicolon:
|
||||
return src
|
||||
from tokenize_rt import src_to_tokens, tokens_to_src, reversed_enumerate
|
||||
|
||||
tokens = src_to_tokens(src)
|
||||
for idx, token in reversed_enumerate(tokens):
|
||||
if token.name in TOKENS_TO_IGNORE:
|
||||
continue
|
||||
tokens[idx] = token._replace(src=token.src + ";")
|
||||
break
|
||||
else: # pragma: nocover
|
||||
raise AssertionError(
|
||||
"INTERNAL ERROR: Was not able to reinstate trailing semicolon. "
|
||||
"Please report a bug on https://github.com/psf/black/issues. "
|
||||
) from None
|
||||
return str(tokens_to_src(tokens))
|
||||
|
||||
|
||||
def mask_cell(src: str) -> Tuple[str, List[Replacement]]:
|
||||
"""Mask IPython magics so content becomes parseable Python code.
|
||||
|
||||
For example,
|
||||
|
||||
%matplotlib inline
|
||||
'foo'
|
||||
|
||||
becomes
|
||||
|
||||
"25716f358c32750e"
|
||||
'foo'
|
||||
|
||||
The replacements are returned, along with the transformed code.
|
||||
"""
|
||||
replacements: List[Replacement] = []
|
||||
try:
|
||||
ast.parse(src)
|
||||
except SyntaxError:
|
||||
# Might have IPython magics, will process below.
|
||||
pass
|
||||
else:
|
||||
# Syntax is fine, nothing to mask, early return.
|
||||
return src, replacements
|
||||
|
||||
from IPython.core.inputtransformer2 import TransformerManager
|
||||
|
||||
transformer_manager = TransformerManager()
|
||||
transformed = transformer_manager.transform_cell(src)
|
||||
transformed, cell_magic_replacements = replace_cell_magics(transformed)
|
||||
replacements += cell_magic_replacements
|
||||
transformed = transformer_manager.transform_cell(transformed)
|
||||
transformed, magic_replacements = replace_magics(transformed)
|
||||
if len(transformed.splitlines()) != len(src.splitlines()):
|
||||
# Multi-line magic, not supported.
|
||||
raise NothingChanged
|
||||
replacements += magic_replacements
|
||||
return transformed, replacements
|
||||
|
||||
|
||||
def get_token(src: str, magic: str) -> str:
|
||||
"""Return randomly generated token to mask IPython magic with.
|
||||
|
||||
For example, if 'magic' was `%matplotlib inline`, then a possible
|
||||
token to mask it with would be `"43fdd17f7e5ddc83"`. The token
|
||||
will be the same length as the magic, and we make sure that it was
|
||||
not already present anywhere else in the cell.
|
||||
"""
|
||||
assert magic
|
||||
nbytes = max(len(magic) // 2 - 1, 1)
|
||||
token = TOKEN_HEX(nbytes)
|
||||
counter = 0
|
||||
while token in src:
|
||||
token = TOKEN_HEX(nbytes)
|
||||
counter += 1
|
||||
if counter > 100:
|
||||
raise AssertionError(
|
||||
"INTERNAL ERROR: Black was not able to replace IPython magic. "
|
||||
"Please report a bug on https://github.com/psf/black/issues. "
|
||||
f"The magic might be helpful: {magic}"
|
||||
) from None
|
||||
if len(token) + 2 < len(magic):
|
||||
token = f"{token}."
|
||||
return f'"{token}"'
|
||||
|
||||
|
||||
def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]:
|
||||
"""Replace cell magic with token.
|
||||
|
||||
Note that 'src' will already have been processed by IPython's
|
||||
TransformerManager().transform_cell.
|
||||
|
||||
Example,
|
||||
|
||||
get_ipython().run_cell_magic('t', '-n1', 'ls =!ls\\n')
|
||||
|
||||
becomes
|
||||
|
||||
"a794."
|
||||
ls =!ls
|
||||
|
||||
The replacement, along with the transformed code, is returned.
|
||||
"""
|
||||
replacements: List[Replacement] = []
|
||||
|
||||
tree = ast.parse(src)
|
||||
|
||||
cell_magic_finder = CellMagicFinder()
|
||||
cell_magic_finder.visit(tree)
|
||||
if cell_magic_finder.cell_magic is None:
|
||||
return src, replacements
|
||||
header = cell_magic_finder.cell_magic.header
|
||||
mask = get_token(src, header)
|
||||
replacements.append(Replacement(mask=mask, src=header))
|
||||
return f"{mask}\n{cell_magic_finder.cell_magic.body}", replacements
|
||||
|
||||
|
||||
def replace_magics(src: str) -> Tuple[str, List[Replacement]]:
|
||||
"""Replace magics within body of cell.
|
||||
|
||||
Note that 'src' will already have been processed by IPython's
|
||||
TransformerManager().transform_cell.
|
||||
|
||||
Example, this
|
||||
|
||||
get_ipython().run_line_magic('matplotlib', 'inline')
|
||||
'foo'
|
||||
|
||||
becomes
|
||||
|
||||
"5e67db56d490fd39"
|
||||
'foo'
|
||||
|
||||
The replacement, along with the transformed code, are returned.
|
||||
"""
|
||||
replacements = []
|
||||
magic_finder = MagicFinder()
|
||||
magic_finder.visit(ast.parse(src))
|
||||
new_srcs = []
|
||||
for i, line in enumerate(src.splitlines(), start=1):
|
||||
if i in magic_finder.magics:
|
||||
offsets_and_magics = magic_finder.magics[i]
|
||||
if len(offsets_and_magics) != 1: # pragma: nocover
|
||||
raise AssertionError(
|
||||
f"Expecting one magic per line, got: {offsets_and_magics}\n"
|
||||
"Please report a bug on https://github.com/psf/black/issues."
|
||||
)
|
||||
col_offset, magic = (
|
||||
offsets_and_magics[0].col_offset,
|
||||
offsets_and_magics[0].magic,
|
||||
)
|
||||
mask = get_token(src, magic)
|
||||
replacements.append(Replacement(mask=mask, src=magic))
|
||||
line = line[:col_offset] + mask
|
||||
new_srcs.append(line)
|
||||
return "\n".join(new_srcs), replacements
|
||||
|
||||
|
||||
def unmask_cell(src: str, replacements: List[Replacement]) -> str:
|
||||
"""Remove replacements from cell.
|
||||
|
||||
For example
|
||||
|
||||
"9b20"
|
||||
foo = bar
|
||||
|
||||
becomes
|
||||
|
||||
%%time
|
||||
foo = bar
|
||||
"""
|
||||
for replacement in replacements:
|
||||
src = src.replace(replacement.mask, replacement.src)
|
||||
return src
|
||||
|
||||
|
||||
def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]:
|
||||
"""Check if attribute is IPython magic.
|
||||
|
||||
Note that the source of the abstract syntax tree
|
||||
will already have been processed by IPython's
|
||||
TransformerManager().transform_cell.
|
||||
"""
|
||||
return (
|
||||
isinstance(node, ast.Attribute)
|
||||
and isinstance(node.value, ast.Call)
|
||||
and isinstance(node.value.func, ast.Name)
|
||||
and node.value.func.id == "get_ipython"
|
||||
)
|
||||
|
||||
|
||||
def _get_str_args(args: List[ast.expr]) -> List[str]:
|
||||
str_args = []
|
||||
for arg in args:
|
||||
assert isinstance(arg, ast.Str)
|
||||
str_args.append(arg.s)
|
||||
return str_args
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CellMagic:
|
||||
name: str
|
||||
params: Optional[str]
|
||||
body: str
|
||||
|
||||
@property
|
||||
def header(self) -> str:
|
||||
if self.params:
|
||||
return f"%%{self.name} {self.params}"
|
||||
return f"%%{self.name}"
|
||||
|
||||
|
||||
# ast.NodeVisitor + dataclass = breakage under mypyc.
|
||||
class CellMagicFinder(ast.NodeVisitor):
|
||||
"""Find cell magics.
|
||||
|
||||
Note that the source of the abstract syntax tree
|
||||
will already have been processed by IPython's
|
||||
TransformerManager().transform_cell.
|
||||
|
||||
For example,
|
||||
|
||||
%%time\nfoo()
|
||||
|
||||
would have been transformed to
|
||||
|
||||
get_ipython().run_cell_magic('time', '', 'foo()\\n')
|
||||
|
||||
and we look for instances of the latter.
|
||||
"""
|
||||
|
||||
def __init__(self, cell_magic: Optional[CellMagic] = None) -> None:
|
||||
self.cell_magic = cell_magic
|
||||
|
||||
def visit_Expr(self, node: ast.Expr) -> None:
|
||||
"""Find cell magic, extract header and body."""
|
||||
if (
|
||||
isinstance(node.value, ast.Call)
|
||||
and _is_ipython_magic(node.value.func)
|
||||
and node.value.func.attr == "run_cell_magic"
|
||||
):
|
||||
args = _get_str_args(node.value.args)
|
||||
self.cell_magic = CellMagic(name=args[0], params=args[1], body=args[2])
|
||||
self.generic_visit(node)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class OffsetAndMagic:
|
||||
col_offset: int
|
||||
magic: str
|
||||
|
||||
|
||||
# Unsurprisingly, subclassing ast.NodeVisitor means we can't use dataclasses here
|
||||
# as mypyc will generate broken code.
|
||||
class MagicFinder(ast.NodeVisitor):
|
||||
"""Visit cell to look for get_ipython calls.
|
||||
|
||||
Note that the source of the abstract syntax tree
|
||||
will already have been processed by IPython's
|
||||
TransformerManager().transform_cell.
|
||||
|
||||
For example,
|
||||
|
||||
%matplotlib inline
|
||||
|
||||
would have been transformed to
|
||||
|
||||
get_ipython().run_line_magic('matplotlib', 'inline')
|
||||
|
||||
and we look for instances of the latter (and likewise for other
|
||||
types of magics).
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.magics: Dict[int, List[OffsetAndMagic]] = collections.defaultdict(list)
|
||||
|
||||
def visit_Assign(self, node: ast.Assign) -> None:
|
||||
"""Look for system assign magics.
|
||||
|
||||
For example,
|
||||
|
||||
black_version = !black --version
|
||||
env = %env var
|
||||
|
||||
would have been (respectively) transformed to
|
||||
|
||||
black_version = get_ipython().getoutput('black --version')
|
||||
env = get_ipython().run_line_magic('env', 'var')
|
||||
|
||||
and we look for instances of any of the latter.
|
||||
"""
|
||||
if isinstance(node.value, ast.Call) and _is_ipython_magic(node.value.func):
|
||||
args = _get_str_args(node.value.args)
|
||||
if node.value.func.attr == "getoutput":
|
||||
src = f"!{args[0]}"
|
||||
elif node.value.func.attr == "run_line_magic":
|
||||
src = f"%{args[0]}"
|
||||
if args[1]:
|
||||
src += f" {args[1]}"
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"Unexpected IPython magic {node.value.func.attr!r} found. "
|
||||
"Please report a bug on https://github.com/psf/black/issues."
|
||||
) from None
|
||||
self.magics[node.value.lineno].append(
|
||||
OffsetAndMagic(node.value.col_offset, src)
|
||||
)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Expr(self, node: ast.Expr) -> None:
|
||||
"""Look for magics in body of cell.
|
||||
|
||||
For examples,
|
||||
|
||||
!ls
|
||||
!!ls
|
||||
?ls
|
||||
??ls
|
||||
|
||||
would (respectively) get transformed to
|
||||
|
||||
get_ipython().system('ls')
|
||||
get_ipython().getoutput('ls')
|
||||
get_ipython().run_line_magic('pinfo', 'ls')
|
||||
get_ipython().run_line_magic('pinfo2', 'ls')
|
||||
|
||||
and we look for instances of any of the latter.
|
||||
"""
|
||||
if isinstance(node.value, ast.Call) and _is_ipython_magic(node.value.func):
|
||||
args = _get_str_args(node.value.args)
|
||||
if node.value.func.attr == "run_line_magic":
|
||||
if args[0] == "pinfo":
|
||||
src = f"?{args[1]}"
|
||||
elif args[0] == "pinfo2":
|
||||
src = f"??{args[1]}"
|
||||
else:
|
||||
src = f"%{args[0]}"
|
||||
if args[1]:
|
||||
src += f" {args[1]}"
|
||||
elif node.value.func.attr == "system":
|
||||
src = f"!{args[0]}"
|
||||
elif node.value.func.attr == "getoutput":
|
||||
src = f"!!{args[0]}"
|
||||
else:
|
||||
raise NothingChanged # unsupported magic.
|
||||
self.magics[node.value.lineno].append(
|
||||
OffsetAndMagic(node.value.col_offset, src)
|
||||
)
|
||||
self.generic_visit(node)
|
||||
BIN
.venv/lib/python3.8/site-packages/black/linegen.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/linegen.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
1086
.venv/lib/python3.8/site-packages/black/linegen.py
Normal file
1086
.venv/lib/python3.8/site-packages/black/linegen.py
Normal file
File diff suppressed because it is too large
Load diff
BIN
.venv/lib/python3.8/site-packages/black/lines.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/lines.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
773
.venv/lib/python3.8/site-packages/black/lines.py
Normal file
773
.venv/lib/python3.8/site-packages/black/lines.py
Normal file
|
|
@ -0,0 +1,773 @@
|
|||
from dataclasses import dataclass, field
|
||||
import itertools
|
||||
import sys
|
||||
from typing import (
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
from blib2to3.pytree import Node, Leaf
|
||||
from blib2to3.pgen2 import token
|
||||
|
||||
from black.brackets import BracketTracker, DOT_PRIORITY
|
||||
from black.mode import Mode, Preview
|
||||
from black.nodes import STANDALONE_COMMENT, TEST_DESCENDANTS
|
||||
from black.nodes import BRACKETS, OPENING_BRACKETS, CLOSING_BRACKETS
|
||||
from black.nodes import syms, whitespace, replace_child, child_towards
|
||||
from black.nodes import is_multiline_string, is_import, is_type_comment
|
||||
from black.nodes import is_one_sequence_between
|
||||
|
||||
# types
|
||||
T = TypeVar("T")
|
||||
Index = int
|
||||
LeafID = int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Line:
|
||||
"""Holds leaves and comments. Can be printed with `str(line)`."""
|
||||
|
||||
mode: Mode
|
||||
depth: int = 0
|
||||
leaves: List[Leaf] = field(default_factory=list)
|
||||
# keys ordered like `leaves`
|
||||
comments: Dict[LeafID, List[Leaf]] = field(default_factory=dict)
|
||||
bracket_tracker: BracketTracker = field(default_factory=BracketTracker)
|
||||
inside_brackets: bool = False
|
||||
should_split_rhs: bool = False
|
||||
magic_trailing_comma: Optional[Leaf] = None
|
||||
|
||||
def append(self, leaf: Leaf, preformatted: bool = False) -> None:
|
||||
"""Add a new `leaf` to the end of the line.
|
||||
|
||||
Unless `preformatted` is True, the `leaf` will receive a new consistent
|
||||
whitespace prefix and metadata applied by :class:`BracketTracker`.
|
||||
Trailing commas are maybe removed, unpacked for loop variables are
|
||||
demoted from being delimiters.
|
||||
|
||||
Inline comments are put aside.
|
||||
"""
|
||||
has_value = leaf.type in BRACKETS or bool(leaf.value.strip())
|
||||
if not has_value:
|
||||
return
|
||||
|
||||
if token.COLON == leaf.type and self.is_class_paren_empty:
|
||||
del self.leaves[-2:]
|
||||
if self.leaves and not preformatted:
|
||||
# Note: at this point leaf.prefix should be empty except for
|
||||
# imports, for which we only preserve newlines.
|
||||
leaf.prefix += whitespace(
|
||||
leaf, complex_subscript=self.is_complex_subscript(leaf)
|
||||
)
|
||||
if self.inside_brackets or not preformatted:
|
||||
self.bracket_tracker.mark(leaf)
|
||||
if self.mode.magic_trailing_comma:
|
||||
if self.has_magic_trailing_comma(leaf):
|
||||
self.magic_trailing_comma = leaf
|
||||
elif self.has_magic_trailing_comma(leaf, ensure_removable=True):
|
||||
self.remove_trailing_comma()
|
||||
if not self.append_comment(leaf):
|
||||
self.leaves.append(leaf)
|
||||
|
||||
def append_safe(self, leaf: Leaf, preformatted: bool = False) -> None:
|
||||
"""Like :func:`append()` but disallow invalid standalone comment structure.
|
||||
|
||||
Raises ValueError when any `leaf` is appended after a standalone comment
|
||||
or when a standalone comment is not the first leaf on the line.
|
||||
"""
|
||||
if self.bracket_tracker.depth == 0:
|
||||
if self.is_comment:
|
||||
raise ValueError("cannot append to standalone comments")
|
||||
|
||||
if self.leaves and leaf.type == STANDALONE_COMMENT:
|
||||
raise ValueError(
|
||||
"cannot append standalone comments to a populated line"
|
||||
)
|
||||
|
||||
self.append(leaf, preformatted=preformatted)
|
||||
|
||||
@property
|
||||
def is_comment(self) -> bool:
|
||||
"""Is this line a standalone comment?"""
|
||||
return len(self.leaves) == 1 and self.leaves[0].type == STANDALONE_COMMENT
|
||||
|
||||
@property
|
||||
def is_decorator(self) -> bool:
|
||||
"""Is this line a decorator?"""
|
||||
return bool(self) and self.leaves[0].type == token.AT
|
||||
|
||||
@property
|
||||
def is_import(self) -> bool:
|
||||
"""Is this an import line?"""
|
||||
return bool(self) and is_import(self.leaves[0])
|
||||
|
||||
@property
|
||||
def is_class(self) -> bool:
|
||||
"""Is this line a class definition?"""
|
||||
return (
|
||||
bool(self)
|
||||
and self.leaves[0].type == token.NAME
|
||||
and self.leaves[0].value == "class"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_stub_class(self) -> bool:
|
||||
"""Is this line a class definition with a body consisting only of "..."?"""
|
||||
return self.is_class and self.leaves[-3:] == [
|
||||
Leaf(token.DOT, ".") for _ in range(3)
|
||||
]
|
||||
|
||||
@property
|
||||
def is_def(self) -> bool:
|
||||
"""Is this a function definition? (Also returns True for async defs.)"""
|
||||
try:
|
||||
first_leaf = self.leaves[0]
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
try:
|
||||
second_leaf: Optional[Leaf] = self.leaves[1]
|
||||
except IndexError:
|
||||
second_leaf = None
|
||||
return (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"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_class_paren_empty(self) -> bool:
|
||||
"""Is this a class with no base classes but using parentheses?
|
||||
|
||||
Those are unnecessary and should be removed.
|
||||
"""
|
||||
return (
|
||||
bool(self)
|
||||
and len(self.leaves) == 4
|
||||
and self.is_class
|
||||
and self.leaves[2].type == token.LPAR
|
||||
and self.leaves[2].value == "("
|
||||
and self.leaves[3].type == token.RPAR
|
||||
and self.leaves[3].value == ")"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_triple_quoted_string(self) -> bool:
|
||||
"""Is the line a triple quoted string?"""
|
||||
return (
|
||||
bool(self)
|
||||
and self.leaves[0].type == token.STRING
|
||||
and self.leaves[0].value.startswith(('"""', "'''"))
|
||||
)
|
||||
|
||||
def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool:
|
||||
"""If so, needs to be split before emitting."""
|
||||
for leaf in self.leaves:
|
||||
if leaf.type == STANDALONE_COMMENT and leaf.bracket_depth <= depth_limit:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def contains_uncollapsable_type_comments(self) -> bool:
|
||||
ignored_ids = set()
|
||||
try:
|
||||
last_leaf = self.leaves[-1]
|
||||
ignored_ids.add(id(last_leaf))
|
||||
if last_leaf.type == token.COMMA or (
|
||||
last_leaf.type == token.RPAR and not last_leaf.value
|
||||
):
|
||||
# When trailing commas or optional parens are inserted by Black for
|
||||
# consistency, comments after the previous last element are not moved
|
||||
# (they don't have to, rendering will still be correct). So we ignore
|
||||
# trailing commas and invisible.
|
||||
last_leaf = self.leaves[-2]
|
||||
ignored_ids.add(id(last_leaf))
|
||||
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():
|
||||
for comment in comments:
|
||||
if is_type_comment(comment):
|
||||
if comment_seen or (
|
||||
not is_type_comment(comment, " ignore")
|
||||
and leaf_id not in ignored_ids
|
||||
):
|
||||
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((leaf.lineno for leaf in self.leaves if leaf.lineno != 0), 0)
|
||||
last_line = next(
|
||||
(leaf.lineno for leaf in reversed(self.leaves) if leaf.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
|
||||
|
||||
def contains_multiline_strings(self) -> bool:
|
||||
return any(is_multiline_string(leaf) for leaf in self.leaves)
|
||||
|
||||
def has_magic_trailing_comma(
|
||||
self, closing: Leaf, ensure_removable: bool = False
|
||||
) -> bool:
|
||||
"""Return True if we have a magic trailing comma, that is when:
|
||||
- there's a trailing comma here
|
||||
- it's not a one-tuple
|
||||
- it's not a single-element subscript
|
||||
Additionally, if ensure_removable:
|
||||
- it's not from square bracket indexing
|
||||
"""
|
||||
if not (
|
||||
closing.type in CLOSING_BRACKETS
|
||||
and self.leaves
|
||||
and self.leaves[-1].type == token.COMMA
|
||||
):
|
||||
return False
|
||||
|
||||
if closing.type == token.RBRACE:
|
||||
return True
|
||||
|
||||
if closing.type == token.RSQB:
|
||||
if (
|
||||
Preview.one_element_subscript in self.mode
|
||||
and closing.parent
|
||||
and closing.parent.type == syms.trailer
|
||||
and closing.opening_bracket
|
||||
and is_one_sequence_between(
|
||||
closing.opening_bracket,
|
||||
closing,
|
||||
self.leaves,
|
||||
brackets=(token.LSQB, token.RSQB),
|
||||
)
|
||||
):
|
||||
return False
|
||||
|
||||
if not ensure_removable:
|
||||
return True
|
||||
comma = self.leaves[-1]
|
||||
return bool(comma.parent and comma.parent.type == syms.listmaker)
|
||||
|
||||
if self.is_import:
|
||||
return True
|
||||
|
||||
if closing.opening_bracket is not None and not is_one_sequence_between(
|
||||
closing.opening_bracket, closing, self.leaves
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def append_comment(self, comment: Leaf) -> bool:
|
||||
"""Add an inline or standalone comment to the line."""
|
||||
if (
|
||||
comment.type == STANDALONE_COMMENT
|
||||
and self.bracket_tracker.any_open_brackets()
|
||||
):
|
||||
comment.prefix = ""
|
||||
return False
|
||||
|
||||
if comment.type != token.COMMENT:
|
||||
return False
|
||||
|
||||
if not self.leaves:
|
||||
comment.type = STANDALONE_COMMENT
|
||||
comment.prefix = ""
|
||||
return False
|
||||
|
||||
last_leaf = self.leaves[-1]
|
||||
if (
|
||||
last_leaf.type == token.RPAR
|
||||
and not last_leaf.value
|
||||
and last_leaf.parent
|
||||
and len(list(last_leaf.parent.leaves())) <= 3
|
||||
and not is_type_comment(comment)
|
||||
):
|
||||
# Comments on an optional parens wrapping a single leaf should belong to
|
||||
# the wrapped node except if it's a type comment. Pinning the comment like
|
||||
# this avoids unstable formatting caused by comment migration.
|
||||
if len(self.leaves) < 2:
|
||||
comment.type = STANDALONE_COMMENT
|
||||
comment.prefix = ""
|
||||
return False
|
||||
|
||||
last_leaf = self.leaves[-2]
|
||||
self.comments.setdefault(id(last_leaf), []).append(comment)
|
||||
return True
|
||||
|
||||
def comments_after(self, leaf: Leaf) -> List[Leaf]:
|
||||
"""Generate comments that should appear directly after `leaf`."""
|
||||
return self.comments.get(id(leaf), [])
|
||||
|
||||
def remove_trailing_comma(self) -> None:
|
||||
"""Remove the trailing comma and moves the comments attached to it."""
|
||||
trailing_comma = self.leaves.pop()
|
||||
trailing_comma_comments = self.comments.pop(id(trailing_comma), [])
|
||||
self.comments.setdefault(id(self.leaves[-1]), []).extend(
|
||||
trailing_comma_comments
|
||||
)
|
||||
|
||||
def is_complex_subscript(self, leaf: Leaf) -> bool:
|
||||
"""Return True iff `leaf` is part of a slice with non-trivial exprs."""
|
||||
open_lsqb = self.bracket_tracker.get_open_lsqb()
|
||||
if open_lsqb is None:
|
||||
return False
|
||||
|
||||
subscript_start = open_lsqb.next_sibling
|
||||
|
||||
if isinstance(subscript_start, Node):
|
||||
if subscript_start.type == syms.listmaker:
|
||||
return False
|
||||
|
||||
if subscript_start.type == syms.subscriptlist:
|
||||
subscript_start = child_towards(subscript_start, leaf)
|
||||
return subscript_start is not None and any(
|
||||
n.type in TEST_DESCENDANTS for n in subscript_start.pre_order()
|
||||
)
|
||||
|
||||
def enumerate_with_length(
|
||||
self, reversed: bool = False
|
||||
) -> Iterator[Tuple[Index, Leaf, int]]:
|
||||
"""Return an enumeration of leaves with their length.
|
||||
|
||||
Stops prematurely on multiline strings and standalone comments.
|
||||
"""
|
||||
op = cast(
|
||||
Callable[[Sequence[Leaf]], Iterator[Tuple[Index, Leaf]]],
|
||||
enumerate_reversed if reversed else enumerate,
|
||||
)
|
||||
for index, leaf in op(self.leaves):
|
||||
length = len(leaf.prefix) + len(leaf.value)
|
||||
if "\n" in leaf.value:
|
||||
return # Multiline strings, we can't continue.
|
||||
|
||||
for comment in self.comments_after(leaf):
|
||||
length += len(comment.value)
|
||||
|
||||
yield index, leaf, length
|
||||
|
||||
def clone(self) -> "Line":
|
||||
return Line(
|
||||
mode=self.mode,
|
||||
depth=self.depth,
|
||||
inside_brackets=self.inside_brackets,
|
||||
should_split_rhs=self.should_split_rhs,
|
||||
magic_trailing_comma=self.magic_trailing_comma,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Render the line."""
|
||||
if not self:
|
||||
return "\n"
|
||||
|
||||
indent = " " * self.depth
|
||||
leaves = iter(self.leaves)
|
||||
first = next(leaves)
|
||||
res = f"{first.prefix}{indent}{first.value}"
|
||||
for leaf in leaves:
|
||||
res += str(leaf)
|
||||
for comment in itertools.chain.from_iterable(self.comments.values()):
|
||||
res += str(comment)
|
||||
|
||||
return res + "\n"
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""Return True if the line has leaves or comments."""
|
||||
return bool(self.leaves or self.comments)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmptyLineTracker:
|
||||
"""Provides a stateful method that returns the number of potential extra
|
||||
empty lines needed before and after the currently processed line.
|
||||
|
||||
Note: this tracker works on lines that haven't been split yet. It assumes
|
||||
the prefix of the first leaf consists of optional newlines. Those newlines
|
||||
are consumed by `maybe_empty_lines()` and included in the computation.
|
||||
"""
|
||||
|
||||
is_pyi: bool = False
|
||||
previous_line: Optional[Line] = None
|
||||
previous_after: int = 0
|
||||
previous_defs: List[int] = field(default_factory=list)
|
||||
|
||||
def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
|
||||
"""Return the number of extra empty lines before and after the `current_line`.
|
||||
|
||||
This is for separating `def`, `async def` and `class` with extra empty
|
||||
lines (two on module-level).
|
||||
"""
|
||||
before, after = self._maybe_empty_lines(current_line)
|
||||
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
|
||||
|
||||
def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
|
||||
max_allowed = 1
|
||||
if current_line.depth == 0:
|
||||
max_allowed = 1 if self.is_pyi else 2
|
||||
if current_line.leaves:
|
||||
# Consume the first leaf's extra newlines.
|
||||
first_leaf = current_line.leaves[0]
|
||||
before = first_leaf.prefix.count("\n")
|
||||
before = min(before, max_allowed)
|
||||
first_leaf.prefix = ""
|
||||
else:
|
||||
before = 0
|
||||
depth = current_line.depth
|
||||
while self.previous_defs and self.previous_defs[-1] >= depth:
|
||||
if self.is_pyi:
|
||||
assert self.previous_line is not None
|
||||
if depth and not current_line.is_def and self.previous_line.is_def:
|
||||
# Empty lines between attributes and methods should be preserved.
|
||||
before = min(1, before)
|
||||
elif depth:
|
||||
before = 0
|
||||
else:
|
||||
before = 1
|
||||
else:
|
||||
if depth:
|
||||
before = 1
|
||||
elif (
|
||||
not depth
|
||||
and self.previous_defs[-1]
|
||||
and current_line.leaves[-1].type == token.COLON
|
||||
and (
|
||||
current_line.leaves[0].value
|
||||
not in ("with", "try", "for", "while", "if", "match")
|
||||
)
|
||||
):
|
||||
# We shouldn't add two newlines between an indented function and
|
||||
# a dependent non-indented clause. This is to avoid issues with
|
||||
# conditional function definitions that are technically top-level
|
||||
# and therefore get two trailing newlines, but look weird and
|
||||
# inconsistent when they're followed by elif, else, etc. This is
|
||||
# worse because these functions only get *one* preceding newline
|
||||
# already.
|
||||
before = 1
|
||||
else:
|
||||
before = 2
|
||||
self.previous_defs.pop()
|
||||
if current_line.is_decorator or current_line.is_def or current_line.is_class:
|
||||
return self._maybe_empty_lines_for_class_or_def(current_line, before)
|
||||
|
||||
if (
|
||||
self.previous_line
|
||||
and self.previous_line.is_import
|
||||
and not current_line.is_import
|
||||
and depth == self.previous_line.depth
|
||||
):
|
||||
return (before or 1), 0
|
||||
|
||||
if (
|
||||
self.previous_line
|
||||
and self.previous_line.is_class
|
||||
and current_line.is_triple_quoted_string
|
||||
):
|
||||
return before, 1
|
||||
|
||||
return before, 0
|
||||
|
||||
def _maybe_empty_lines_for_class_or_def(
|
||||
self, current_line: Line, before: int
|
||||
) -> Tuple[int, int]:
|
||||
if not current_line.is_decorator:
|
||||
self.previous_defs.append(current_line.depth)
|
||||
if self.previous_line is None:
|
||||
# Don't insert empty lines before the first line in the file.
|
||||
return 0, 0
|
||||
|
||||
if self.previous_line.is_decorator:
|
||||
if self.is_pyi and current_line.is_stub_class:
|
||||
# Insert an empty line after a decorated stub class
|
||||
return 0, 1
|
||||
|
||||
return 0, 0
|
||||
|
||||
if self.previous_line.depth < current_line.depth and (
|
||||
self.previous_line.is_class or self.previous_line.is_def
|
||||
):
|
||||
return 0, 0
|
||||
|
||||
if (
|
||||
self.previous_line.is_comment
|
||||
and self.previous_line.depth == current_line.depth
|
||||
and before == 0
|
||||
):
|
||||
return 0, 0
|
||||
|
||||
if self.is_pyi:
|
||||
if current_line.is_class or self.previous_line.is_class:
|
||||
if self.previous_line.depth < current_line.depth:
|
||||
newlines = 0
|
||||
elif self.previous_line.depth > current_line.depth:
|
||||
newlines = 1
|
||||
elif current_line.is_stub_class and self.previous_line.is_stub_class:
|
||||
# No blank line between classes with an empty body
|
||||
newlines = 0
|
||||
else:
|
||||
newlines = 1
|
||||
elif (
|
||||
current_line.is_def or current_line.is_decorator
|
||||
) and not self.previous_line.is_def:
|
||||
if current_line.depth:
|
||||
# In classes empty lines between attributes and methods should
|
||||
# be preserved.
|
||||
newlines = min(1, before)
|
||||
else:
|
||||
# Blank line between a block of functions (maybe with preceding
|
||||
# decorators) and a block of non-functions
|
||||
newlines = 1
|
||||
elif self.previous_line.depth > current_line.depth:
|
||||
newlines = 1
|
||||
else:
|
||||
newlines = 0
|
||||
else:
|
||||
newlines = 1 if current_line.depth else 2
|
||||
return newlines, 0
|
||||
|
||||
|
||||
def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]:
|
||||
"""Like `reversed(enumerate(sequence))` if that were possible."""
|
||||
index = len(sequence) - 1
|
||||
for element in reversed(sequence):
|
||||
yield (index, element)
|
||||
index -= 1
|
||||
|
||||
|
||||
def append_leaves(
|
||||
new_line: Line, old_line: Line, leaves: List[Leaf], preformatted: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Append leaves (taken from @old_line) to @new_line, making sure to fix the
|
||||
underlying Node structure where appropriate.
|
||||
|
||||
All of the leaves in @leaves are duplicated. The duplicates are then
|
||||
appended to @new_line and used to replace their originals in the underlying
|
||||
Node structure. Any comments attached to the old leaves are reattached to
|
||||
the new leaves.
|
||||
|
||||
Pre-conditions:
|
||||
set(@leaves) is a subset of set(@old_line.leaves).
|
||||
"""
|
||||
for old_leaf in leaves:
|
||||
new_leaf = Leaf(old_leaf.type, old_leaf.value)
|
||||
replace_child(old_leaf, new_leaf)
|
||||
new_line.append(new_leaf, preformatted=preformatted)
|
||||
|
||||
for comment_leaf in old_line.comments_after(old_leaf):
|
||||
new_line.append(comment_leaf, preformatted=True)
|
||||
|
||||
|
||||
def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") -> bool:
|
||||
"""Return True if `line` is no longer than `line_length`.
|
||||
|
||||
Uses the provided `line_str` rendering, if any, otherwise computes a new one.
|
||||
"""
|
||||
if not line_str:
|
||||
line_str = line_to_string(line)
|
||||
return (
|
||||
len(line_str) <= line_length
|
||||
and "\n" not in line_str # multiline strings
|
||||
and not line.contains_standalone_comments()
|
||||
)
|
||||
|
||||
|
||||
def can_be_split(line: Line) -> bool:
|
||||
"""Return False if the line cannot be split *for sure*.
|
||||
|
||||
This is not an exhaustive search but a cheap heuristic that we can use to
|
||||
avoid some unfortunate formattings (mostly around wrapping unsplittable code
|
||||
in unnecessary parentheses).
|
||||
"""
|
||||
leaves = line.leaves
|
||||
if len(leaves) < 2:
|
||||
return False
|
||||
|
||||
if leaves[0].type == token.STRING and leaves[1].type == token.DOT:
|
||||
call_count = 0
|
||||
dot_count = 0
|
||||
next = leaves[-1]
|
||||
for leaf in leaves[-2::-1]:
|
||||
if leaf.type in OPENING_BRACKETS:
|
||||
if next.type not in CLOSING_BRACKETS:
|
||||
return False
|
||||
|
||||
call_count += 1
|
||||
elif leaf.type == token.DOT:
|
||||
dot_count += 1
|
||||
elif leaf.type == token.NAME:
|
||||
if not (next.type == token.DOT or next.type in OPENING_BRACKETS):
|
||||
return False
|
||||
|
||||
elif leaf.type not in CLOSING_BRACKETS:
|
||||
return False
|
||||
|
||||
if dot_count > 1 and call_count > 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def can_omit_invisible_parens(
|
||||
line: Line,
|
||||
line_length: int,
|
||||
) -> bool:
|
||||
"""Does `line` have a shape safe to reformat without optional parens around it?
|
||||
|
||||
Returns True for only a subset of potentially nice looking formattings but
|
||||
the point is to not return false positives that end up producing lines that
|
||||
are too long.
|
||||
"""
|
||||
bt = line.bracket_tracker
|
||||
if not bt.delimiters:
|
||||
# Without delimiters the optional parentheses are useless.
|
||||
return True
|
||||
|
||||
max_priority = bt.max_delimiter_priority()
|
||||
if bt.delimiter_count_with_priority(max_priority) > 1:
|
||||
# With more than one delimiter of a kind the optional parentheses read better.
|
||||
return False
|
||||
|
||||
if max_priority == DOT_PRIORITY:
|
||||
# A single stranded method call doesn't require optional parentheses.
|
||||
return True
|
||||
|
||||
assert len(line.leaves) >= 2, "Stranded delimiter"
|
||||
|
||||
# With a single delimiter, omit if the expression starts or ends with
|
||||
# a bracket.
|
||||
first = line.leaves[0]
|
||||
second = line.leaves[1]
|
||||
if first.type in OPENING_BRACKETS and second.type not in CLOSING_BRACKETS:
|
||||
if _can_omit_opening_paren(line, first=first, line_length=line_length):
|
||||
return True
|
||||
|
||||
# Note: we are not returning False here because a line might have *both*
|
||||
# a leading opening bracket and a trailing closing bracket. If the
|
||||
# opening bracket doesn't match our rule, maybe the closing will.
|
||||
|
||||
penultimate = line.leaves[-2]
|
||||
last = line.leaves[-1]
|
||||
|
||||
if (
|
||||
last.type == token.RPAR
|
||||
or last.type == token.RBRACE
|
||||
or (
|
||||
# don't use indexing for omitting optional parentheses;
|
||||
# it looks weird
|
||||
last.type == token.RSQB
|
||||
and last.parent
|
||||
and last.parent.type != syms.trailer
|
||||
)
|
||||
):
|
||||
if penultimate.type in OPENING_BRACKETS:
|
||||
# Empty brackets don't help.
|
||||
return False
|
||||
|
||||
if is_multiline_string(first):
|
||||
# Additional wrapping of a multiline string in this situation is
|
||||
# unnecessary.
|
||||
return True
|
||||
|
||||
if _can_omit_closing_paren(line, last=last, line_length=line_length):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _can_omit_opening_paren(line: Line, *, first: Leaf, line_length: int) -> bool:
|
||||
"""See `can_omit_invisible_parens`."""
|
||||
remainder = False
|
||||
length = 4 * line.depth
|
||||
_index = -1
|
||||
for _index, leaf, leaf_length in line.enumerate_with_length():
|
||||
if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first:
|
||||
remainder = True
|
||||
if remainder:
|
||||
length += leaf_length
|
||||
if length > line_length:
|
||||
break
|
||||
|
||||
if leaf.type in OPENING_BRACKETS:
|
||||
# There are brackets we can further split on.
|
||||
remainder = False
|
||||
|
||||
else:
|
||||
# checked the entire string and line length wasn't exceeded
|
||||
if len(line.leaves) == _index + 1:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool:
|
||||
"""See `can_omit_invisible_parens`."""
|
||||
length = 4 * line.depth
|
||||
seen_other_brackets = False
|
||||
for _index, leaf, leaf_length in line.enumerate_with_length():
|
||||
length += leaf_length
|
||||
if leaf is last.opening_bracket:
|
||||
if seen_other_brackets or length <= line_length:
|
||||
return True
|
||||
|
||||
elif leaf.type in OPENING_BRACKETS:
|
||||
# There are brackets we can further split on.
|
||||
seen_other_brackets = True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def line_to_string(line: Line) -> str:
|
||||
"""Returns the string representation of @line.
|
||||
|
||||
WARNING: This is known to be computationally expensive.
|
||||
"""
|
||||
return str(line).strip("\n")
|
||||
BIN
.venv/lib/python3.8/site-packages/black/mode.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/mode.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
188
.venv/lib/python3.8/site-packages/black/mode.py
Normal file
188
.venv/lib/python3.8/site-packages/black/mode.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
"""Data structures configuring Black behavior.
|
||||
|
||||
Mostly around Python language feature support per version and Black configuration
|
||||
chosen by the user.
|
||||
"""
|
||||
|
||||
from hashlib import sha256
|
||||
import sys
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from operator import attrgetter
|
||||
from typing import Dict, Set
|
||||
from warnings import warn
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from typing_extensions import Final
|
||||
else:
|
||||
from typing import Final
|
||||
|
||||
from black.const import DEFAULT_LINE_LENGTH
|
||||
|
||||
|
||||
class TargetVersion(Enum):
|
||||
PY33 = 3
|
||||
PY34 = 4
|
||||
PY35 = 5
|
||||
PY36 = 6
|
||||
PY37 = 7
|
||||
PY38 = 8
|
||||
PY39 = 9
|
||||
PY310 = 10
|
||||
|
||||
|
||||
class Feature(Enum):
|
||||
F_STRINGS = 2
|
||||
NUMERIC_UNDERSCORES = 3
|
||||
TRAILING_COMMA_IN_CALL = 4
|
||||
TRAILING_COMMA_IN_DEF = 5
|
||||
# The following two feature-flags are mutually exclusive, and exactly one should be
|
||||
# set for every version of python.
|
||||
ASYNC_IDENTIFIERS = 6
|
||||
ASYNC_KEYWORDS = 7
|
||||
ASSIGNMENT_EXPRESSIONS = 8
|
||||
POS_ONLY_ARGUMENTS = 9
|
||||
RELAXED_DECORATORS = 10
|
||||
PATTERN_MATCHING = 11
|
||||
UNPACKING_ON_FLOW = 12
|
||||
ANN_ASSIGN_EXTENDED_RHS = 13
|
||||
FORCE_OPTIONAL_PARENTHESES = 50
|
||||
|
||||
# __future__ flags
|
||||
FUTURE_ANNOTATIONS = 51
|
||||
|
||||
|
||||
FUTURE_FLAG_TO_FEATURE: Final = {
|
||||
"annotations": Feature.FUTURE_ANNOTATIONS,
|
||||
}
|
||||
|
||||
|
||||
VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
|
||||
TargetVersion.PY33: {Feature.ASYNC_IDENTIFIERS},
|
||||
TargetVersion.PY34: {Feature.ASYNC_IDENTIFIERS},
|
||||
TargetVersion.PY35: {Feature.TRAILING_COMMA_IN_CALL, Feature.ASYNC_IDENTIFIERS},
|
||||
TargetVersion.PY36: {
|
||||
Feature.F_STRINGS,
|
||||
Feature.NUMERIC_UNDERSCORES,
|
||||
Feature.TRAILING_COMMA_IN_CALL,
|
||||
Feature.TRAILING_COMMA_IN_DEF,
|
||||
Feature.ASYNC_IDENTIFIERS,
|
||||
},
|
||||
TargetVersion.PY37: {
|
||||
Feature.F_STRINGS,
|
||||
Feature.NUMERIC_UNDERSCORES,
|
||||
Feature.TRAILING_COMMA_IN_CALL,
|
||||
Feature.TRAILING_COMMA_IN_DEF,
|
||||
Feature.ASYNC_KEYWORDS,
|
||||
Feature.FUTURE_ANNOTATIONS,
|
||||
},
|
||||
TargetVersion.PY38: {
|
||||
Feature.F_STRINGS,
|
||||
Feature.NUMERIC_UNDERSCORES,
|
||||
Feature.TRAILING_COMMA_IN_CALL,
|
||||
Feature.TRAILING_COMMA_IN_DEF,
|
||||
Feature.ASYNC_KEYWORDS,
|
||||
Feature.FUTURE_ANNOTATIONS,
|
||||
Feature.ASSIGNMENT_EXPRESSIONS,
|
||||
Feature.POS_ONLY_ARGUMENTS,
|
||||
Feature.UNPACKING_ON_FLOW,
|
||||
Feature.ANN_ASSIGN_EXTENDED_RHS,
|
||||
},
|
||||
TargetVersion.PY39: {
|
||||
Feature.F_STRINGS,
|
||||
Feature.NUMERIC_UNDERSCORES,
|
||||
Feature.TRAILING_COMMA_IN_CALL,
|
||||
Feature.TRAILING_COMMA_IN_DEF,
|
||||
Feature.ASYNC_KEYWORDS,
|
||||
Feature.FUTURE_ANNOTATIONS,
|
||||
Feature.ASSIGNMENT_EXPRESSIONS,
|
||||
Feature.RELAXED_DECORATORS,
|
||||
Feature.POS_ONLY_ARGUMENTS,
|
||||
Feature.UNPACKING_ON_FLOW,
|
||||
Feature.ANN_ASSIGN_EXTENDED_RHS,
|
||||
},
|
||||
TargetVersion.PY310: {
|
||||
Feature.F_STRINGS,
|
||||
Feature.NUMERIC_UNDERSCORES,
|
||||
Feature.TRAILING_COMMA_IN_CALL,
|
||||
Feature.TRAILING_COMMA_IN_DEF,
|
||||
Feature.ASYNC_KEYWORDS,
|
||||
Feature.FUTURE_ANNOTATIONS,
|
||||
Feature.ASSIGNMENT_EXPRESSIONS,
|
||||
Feature.RELAXED_DECORATORS,
|
||||
Feature.POS_ONLY_ARGUMENTS,
|
||||
Feature.UNPACKING_ON_FLOW,
|
||||
Feature.ANN_ASSIGN_EXTENDED_RHS,
|
||||
Feature.PATTERN_MATCHING,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> bool:
|
||||
return all(feature in VERSION_TO_FEATURES[version] for version in target_versions)
|
||||
|
||||
|
||||
class Preview(Enum):
|
||||
"""Individual preview style features."""
|
||||
|
||||
string_processing = auto()
|
||||
remove_redundant_parens = auto()
|
||||
one_element_subscript = auto()
|
||||
|
||||
|
||||
class Deprecated(UserWarning):
|
||||
"""Visible deprecation warning."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mode:
|
||||
target_versions: Set[TargetVersion] = field(default_factory=set)
|
||||
line_length: int = DEFAULT_LINE_LENGTH
|
||||
string_normalization: bool = True
|
||||
is_pyi: bool = False
|
||||
is_ipynb: bool = False
|
||||
magic_trailing_comma: bool = True
|
||||
experimental_string_processing: bool = False
|
||||
python_cell_magics: Set[str] = field(default_factory=set)
|
||||
preview: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.experimental_string_processing:
|
||||
warn(
|
||||
"`experimental string processing` has been included in `preview`"
|
||||
" and deprecated. Use `preview` instead.",
|
||||
Deprecated,
|
||||
)
|
||||
|
||||
def __contains__(self, feature: Preview) -> bool:
|
||||
"""
|
||||
Provide `Preview.FEATURE in Mode` syntax that mirrors the ``preview`` flag.
|
||||
|
||||
The argument is not checked and features are not differentiated.
|
||||
They only exist to make development easier by clarifying intent.
|
||||
"""
|
||||
if feature is Preview.string_processing:
|
||||
return self.preview or self.experimental_string_processing
|
||||
return self.preview
|
||||
|
||||
def get_cache_key(self) -> str:
|
||||
if self.target_versions:
|
||||
version_str = ",".join(
|
||||
str(version.value)
|
||||
for version in sorted(self.target_versions, key=attrgetter("value"))
|
||||
)
|
||||
else:
|
||||
version_str = "-"
|
||||
parts = [
|
||||
version_str,
|
||||
str(self.line_length),
|
||||
str(int(self.string_normalization)),
|
||||
str(int(self.is_pyi)),
|
||||
str(int(self.is_ipynb)),
|
||||
str(int(self.magic_trailing_comma)),
|
||||
str(int(self.experimental_string_processing)),
|
||||
str(int(self.preview)),
|
||||
sha256((",".join(sorted(self.python_cell_magics))).encode()).hexdigest(),
|
||||
]
|
||||
return ".".join(parts)
|
||||
BIN
.venv/lib/python3.8/site-packages/black/nodes.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/nodes.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
843
.venv/lib/python3.8/site-packages/black/nodes.py
Normal file
843
.venv/lib/python3.8/site-packages/black/nodes.py
Normal file
|
|
@ -0,0 +1,843 @@
|
|||
"""
|
||||
blib2to3 Node/Leaf transformation-related utility functions.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import (
|
||||
Generic,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Final
|
||||
else:
|
||||
from typing_extensions import Final
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeGuard
|
||||
else:
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from mypy_extensions import mypyc_attr
|
||||
|
||||
# lib2to3 fork
|
||||
from blib2to3.pytree import Node, Leaf, type_repr, NL
|
||||
from blib2to3 import pygram
|
||||
from blib2to3.pgen2 import token
|
||||
|
||||
from black.cache import CACHE_DIR
|
||||
from black.strings import has_triple_quotes
|
||||
|
||||
|
||||
pygram.initialize(CACHE_DIR)
|
||||
syms: Final = pygram.python_symbols
|
||||
|
||||
|
||||
# types
|
||||
T = TypeVar("T")
|
||||
LN = Union[Leaf, Node]
|
||||
LeafID = int
|
||||
NodeType = int
|
||||
|
||||
|
||||
WHITESPACE: Final = {token.DEDENT, token.INDENT, token.NEWLINE}
|
||||
STATEMENT: Final = {
|
||||
syms.if_stmt,
|
||||
syms.while_stmt,
|
||||
syms.for_stmt,
|
||||
syms.try_stmt,
|
||||
syms.except_clause,
|
||||
syms.with_stmt,
|
||||
syms.funcdef,
|
||||
syms.classdef,
|
||||
syms.match_stmt,
|
||||
syms.case_block,
|
||||
}
|
||||
STANDALONE_COMMENT: Final = 153
|
||||
token.tok_name[STANDALONE_COMMENT] = "STANDALONE_COMMENT"
|
||||
LOGIC_OPERATORS: Final = {"and", "or"}
|
||||
COMPARATORS: Final = {
|
||||
token.LESS,
|
||||
token.GREATER,
|
||||
token.EQEQUAL,
|
||||
token.NOTEQUAL,
|
||||
token.LESSEQUAL,
|
||||
token.GREATEREQUAL,
|
||||
}
|
||||
MATH_OPERATORS: Final = {
|
||||
token.VBAR,
|
||||
token.CIRCUMFLEX,
|
||||
token.AMPER,
|
||||
token.LEFTSHIFT,
|
||||
token.RIGHTSHIFT,
|
||||
token.PLUS,
|
||||
token.MINUS,
|
||||
token.STAR,
|
||||
token.SLASH,
|
||||
token.DOUBLESLASH,
|
||||
token.PERCENT,
|
||||
token.AT,
|
||||
token.TILDE,
|
||||
token.DOUBLESTAR,
|
||||
}
|
||||
STARS: Final = {token.STAR, token.DOUBLESTAR}
|
||||
VARARGS_SPECIALS: Final = STARS | {token.SLASH}
|
||||
VARARGS_PARENTS: Final = {
|
||||
syms.arglist,
|
||||
syms.argument, # double star in arglist
|
||||
syms.trailer, # single argument to call
|
||||
syms.typedargslist,
|
||||
syms.varargslist, # lambdas
|
||||
}
|
||||
UNPACKING_PARENTS: Final = {
|
||||
syms.atom, # single element of a list or set literal
|
||||
syms.dictsetmaker,
|
||||
syms.listmaker,
|
||||
syms.testlist_gexp,
|
||||
syms.testlist_star_expr,
|
||||
syms.subject_expr,
|
||||
syms.pattern,
|
||||
}
|
||||
TEST_DESCENDANTS: Final = {
|
||||
syms.test,
|
||||
syms.lambdef,
|
||||
syms.or_test,
|
||||
syms.and_test,
|
||||
syms.not_test,
|
||||
syms.comparison,
|
||||
syms.star_expr,
|
||||
syms.expr,
|
||||
syms.xor_expr,
|
||||
syms.and_expr,
|
||||
syms.shift_expr,
|
||||
syms.arith_expr,
|
||||
syms.trailer,
|
||||
syms.term,
|
||||
syms.power,
|
||||
}
|
||||
ASSIGNMENTS: Final = {
|
||||
"=",
|
||||
"+=",
|
||||
"-=",
|
||||
"*=",
|
||||
"@=",
|
||||
"/=",
|
||||
"%=",
|
||||
"&=",
|
||||
"|=",
|
||||
"^=",
|
||||
"<<=",
|
||||
">>=",
|
||||
"**=",
|
||||
"//=",
|
||||
}
|
||||
|
||||
IMPLICIT_TUPLE: Final = {syms.testlist, syms.testlist_star_expr, syms.exprlist}
|
||||
BRACKET: Final = {
|
||||
token.LPAR: token.RPAR,
|
||||
token.LSQB: token.RSQB,
|
||||
token.LBRACE: token.RBRACE,
|
||||
}
|
||||
OPENING_BRACKETS: Final = set(BRACKET.keys())
|
||||
CLOSING_BRACKETS: Final = set(BRACKET.values())
|
||||
BRACKETS: Final = OPENING_BRACKETS | CLOSING_BRACKETS
|
||||
ALWAYS_NO_SPACE: Final = CLOSING_BRACKETS | {token.COMMA, STANDALONE_COMMENT}
|
||||
|
||||
RARROW = 55
|
||||
|
||||
|
||||
@mypyc_attr(allow_interpreted_subclasses=True)
|
||||
class Visitor(Generic[T]):
|
||||
"""Basic lib2to3 visitor that yields things of type `T` on `visit()`."""
|
||||
|
||||
def visit(self, node: LN) -> Iterator[T]:
|
||||
"""Main method to visit `node` and its children.
|
||||
|
||||
It tries to find a `visit_*()` method for the given `node.type`, like
|
||||
`visit_simple_stmt` for Node objects or `visit_INDENT` for Leaf objects.
|
||||
If no dedicated `visit_*()` method is found, chooses `visit_default()`
|
||||
instead.
|
||||
|
||||
Then yields objects of type `T` from the selected visitor.
|
||||
"""
|
||||
if node.type < 256:
|
||||
name = token.tok_name[node.type]
|
||||
else:
|
||||
name = str(type_repr(node.type))
|
||||
# We explicitly branch on whether a visitor exists (instead of
|
||||
# using self.visit_default as the default arg to getattr) in order
|
||||
# to save needing to create a bound method object and so mypyc can
|
||||
# generate a native call to visit_default.
|
||||
visitf = getattr(self, f"visit_{name}", None)
|
||||
if visitf:
|
||||
yield from visitf(node)
|
||||
else:
|
||||
yield from self.visit_default(node)
|
||||
|
||||
def visit_default(self, node: LN) -> Iterator[T]:
|
||||
"""Default `visit_*()` implementation. Recurses to children of `node`."""
|
||||
if isinstance(node, Node):
|
||||
for child in node.children:
|
||||
yield from self.visit(child)
|
||||
|
||||
|
||||
def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901
|
||||
"""Return whitespace prefix if needed for the given `leaf`.
|
||||
|
||||
`complex_subscript` signals whether the given leaf is part of a subscription
|
||||
which has non-trivial arguments, like arithmetic expressions or function calls.
|
||||
"""
|
||||
NO: Final = ""
|
||||
SPACE: Final = " "
|
||||
DOUBLESPACE: Final = " "
|
||||
t = leaf.type
|
||||
p = leaf.parent
|
||||
v = leaf.value
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
return NO
|
||||
|
||||
if t == token.COMMENT:
|
||||
return DOUBLESPACE
|
||||
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
if t == token.COLON and p.type not in {
|
||||
syms.subscript,
|
||||
syms.subscriptlist,
|
||||
syms.sliceop,
|
||||
}:
|
||||
return NO
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
return NO
|
||||
|
||||
if t == token.COLON:
|
||||
if prevp.type == token.COLON:
|
||||
return NO
|
||||
|
||||
elif prevp.type != token.COMMA and not complex_subscript:
|
||||
return NO
|
||||
|
||||
return SPACE
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent:
|
||||
if prevp.parent.type in {
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
syms.parameters,
|
||||
syms.varargslist,
|
||||
}:
|
||||
return NO
|
||||
|
||||
elif prevp.parent.type == syms.typedargslist:
|
||||
# A bit hacky: if the equal sign has whitespace, it means we
|
||||
# previously found it's a typed argument. So, we're using
|
||||
# that, too.
|
||||
return prevp.prefix
|
||||
|
||||
elif prevp.type in VARARGS_SPECIALS:
|
||||
if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS):
|
||||
return NO
|
||||
|
||||
elif prevp.type == token.COLON:
|
||||
if prevp.parent and prevp.parent.type in {syms.subscript, syms.sliceop}:
|
||||
return SPACE if complex_subscript else NO
|
||||
|
||||
elif (
|
||||
prevp.parent
|
||||
and prevp.parent.type == syms.factor
|
||||
and prevp.type in MATH_OPERATORS
|
||||
):
|
||||
return NO
|
||||
|
||||
elif prevp.type == token.AT and p.parent and p.parent.type == syms.decorator:
|
||||
# no space in decorators
|
||||
return NO
|
||||
|
||||
elif prev.type in OPENING_BRACKETS:
|
||||
return NO
|
||||
|
||||
if p.type in {syms.parameters, syms.arglist}:
|
||||
# untyped function signatures or calls
|
||||
if not prev or prev.type != token.COMMA:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.varargslist:
|
||||
# lambdas
|
||||
if prev and prev.type != token.COMMA:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.typedargslist:
|
||||
# typed function signatures
|
||||
if not prev:
|
||||
return NO
|
||||
|
||||
if t == token.EQUAL:
|
||||
if prev.type != syms.tname:
|
||||
return NO
|
||||
|
||||
elif prev.type == token.EQUAL:
|
||||
# A bit hacky: if the equal sign has whitespace, it means we
|
||||
# previously found it's a typed argument. So, we're using that, too.
|
||||
return prev.prefix
|
||||
|
||||
elif prev.type != token.COMMA:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.tname:
|
||||
# type names
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type != token.COMMA:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.trailer:
|
||||
# attributes and calls
|
||||
if t == token.LPAR or t == token.RPAR:
|
||||
return NO
|
||||
|
||||
if not prev:
|
||||
if t == token.DOT or t == token.LSQB:
|
||||
return NO
|
||||
|
||||
elif prev.type != token.COMMA:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.argument:
|
||||
# single argument
|
||||
if t == token.EQUAL:
|
||||
return NO
|
||||
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type == token.LPAR:
|
||||
return NO
|
||||
|
||||
elif prev.type in {token.EQUAL} | VARARGS_SPECIALS:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.decorator:
|
||||
# decorators
|
||||
return NO
|
||||
|
||||
elif p.type == syms.dotted_name:
|
||||
if prev:
|
||||
return NO
|
||||
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type == token.AT or prevp.type == token.DOT:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.classdef:
|
||||
if t == token.LPAR:
|
||||
return NO
|
||||
|
||||
if prev and prev.type == token.LPAR:
|
||||
return NO
|
||||
|
||||
elif p.type in {syms.subscript, syms.sliceop}:
|
||||
# indexing
|
||||
if not prev:
|
||||
assert p.parent is not None, "subscripts are always parented"
|
||||
if p.parent.type == syms.subscriptlist:
|
||||
return SPACE
|
||||
|
||||
return NO
|
||||
|
||||
elif not complex_subscript:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.atom:
|
||||
if prev and t == token.DOT:
|
||||
# dots, but not the first one.
|
||||
return NO
|
||||
|
||||
elif p.type == syms.dictsetmaker:
|
||||
# dict unpacking
|
||||
if prev and prev.type == token.DOUBLESTAR:
|
||||
return NO
|
||||
|
||||
elif p.type in {syms.factor, syms.star_expr}:
|
||||
# unary ops
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
return NO
|
||||
|
||||
prevp_parent = prevp.parent
|
||||
assert prevp_parent is not None
|
||||
if prevp.type == token.COLON and prevp_parent.type in {
|
||||
syms.subscript,
|
||||
syms.sliceop,
|
||||
}:
|
||||
return NO
|
||||
|
||||
elif prevp.type == token.EQUAL and prevp_parent.type == syms.argument:
|
||||
return NO
|
||||
|
||||
elif t in {token.NAME, token.NUMBER, token.STRING}:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.import_from:
|
||||
if t == token.DOT:
|
||||
if prev and prev.type == token.DOT:
|
||||
return NO
|
||||
|
||||
elif t == token.NAME:
|
||||
if v == "import":
|
||||
return SPACE
|
||||
|
||||
if prev and prev.type == token.DOT:
|
||||
return NO
|
||||
|
||||
elif p.type == syms.sliceop:
|
||||
return NO
|
||||
|
||||
return SPACE
|
||||
|
||||
|
||||
def preceding_leaf(node: Optional[LN]) -> Optional[Leaf]:
|
||||
"""Return the first leaf that precedes `node`, if any."""
|
||||
while node:
|
||||
res = node.prev_sibling
|
||||
if res:
|
||||
if isinstance(res, Leaf):
|
||||
return res
|
||||
|
||||
try:
|
||||
return list(res.leaves())[-1]
|
||||
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
node = node.parent
|
||||
return None
|
||||
|
||||
|
||||
def prev_siblings_are(node: Optional[LN], tokens: List[Optional[NodeType]]) -> bool:
|
||||
"""Return if the `node` and its previous siblings match types against the provided
|
||||
list of tokens; the provided `node`has its type matched against the last element in
|
||||
the list. `None` can be used as the first element to declare that the start of the
|
||||
list is anchored at the start of its parent's children."""
|
||||
if not tokens:
|
||||
return True
|
||||
if tokens[-1] is None:
|
||||
return node is None
|
||||
if not node:
|
||||
return False
|
||||
if node.type != tokens[-1]:
|
||||
return False
|
||||
return prev_siblings_are(node.prev_sibling, tokens[:-1])
|
||||
|
||||
|
||||
def parent_type(node: Optional[LN]) -> Optional[NodeType]:
|
||||
"""
|
||||
Returns:
|
||||
@node.parent.type, if @node is not None and has a parent.
|
||||
OR
|
||||
None, otherwise.
|
||||
"""
|
||||
if node is None or node.parent is None:
|
||||
return None
|
||||
|
||||
return node.parent.type
|
||||
|
||||
|
||||
def child_towards(ancestor: Node, descendant: LN) -> Optional[LN]:
|
||||
"""Return the child of `ancestor` that contains `descendant`."""
|
||||
node: Optional[LN] = descendant
|
||||
while node and node.parent != ancestor:
|
||||
node = node.parent
|
||||
return node
|
||||
|
||||
|
||||
def replace_child(old_child: LN, new_child: LN) -> None:
|
||||
"""
|
||||
Side Effects:
|
||||
* If @old_child.parent is set, replace @old_child with @new_child in
|
||||
@old_child's underlying Node structure.
|
||||
OR
|
||||
* Otherwise, this function does nothing.
|
||||
"""
|
||||
parent = old_child.parent
|
||||
if not parent:
|
||||
return
|
||||
|
||||
child_idx = old_child.remove()
|
||||
if child_idx is not None:
|
||||
parent.insert_child(child_idx, new_child)
|
||||
|
||||
|
||||
def container_of(leaf: Leaf) -> LN:
|
||||
"""Return `leaf` or one of its ancestors that is the topmost container of it.
|
||||
|
||||
By "container" we mean a node where `leaf` is the very first child.
|
||||
"""
|
||||
same_prefix = leaf.prefix
|
||||
container: LN = leaf
|
||||
while container:
|
||||
parent = container.parent
|
||||
if parent is None:
|
||||
break
|
||||
|
||||
if parent.children[0].prefix != same_prefix:
|
||||
break
|
||||
|
||||
if parent.type == syms.file_input:
|
||||
break
|
||||
|
||||
if parent.prev_sibling is not None and parent.prev_sibling.type in BRACKETS:
|
||||
break
|
||||
|
||||
container = parent
|
||||
return container
|
||||
|
||||
|
||||
def first_leaf_column(node: Node) -> Optional[int]:
|
||||
"""Returns the column of the first leaf child of a node."""
|
||||
for child in node.children:
|
||||
if isinstance(child, Leaf):
|
||||
return child.column
|
||||
return None
|
||||
|
||||
|
||||
def is_arith_like(node: LN) -> bool:
|
||||
"""Whether node is an arithmetic or a binary arithmetic expression"""
|
||||
return node.type in {
|
||||
syms.arith_expr,
|
||||
syms.shift_expr,
|
||||
syms.xor_expr,
|
||||
syms.and_expr,
|
||||
}
|
||||
|
||||
|
||||
def is_docstring(leaf: Leaf) -> bool:
|
||||
if prev_siblings_are(
|
||||
leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt]
|
||||
):
|
||||
return True
|
||||
|
||||
# Multiline docstring on the same line as the `def`.
|
||||
if prev_siblings_are(leaf.parent, [syms.parameters, token.COLON, syms.simple_stmt]):
|
||||
# `syms.parameters` is only used in funcdefs and async_funcdefs in the Python
|
||||
# grammar. We're safe to return True without further checks.
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_empty_tuple(node: LN) -> bool:
|
||||
"""Return True if `node` holds an empty tuple."""
|
||||
return (
|
||||
node.type == syms.atom
|
||||
and len(node.children) == 2
|
||||
and node.children[0].type == token.LPAR
|
||||
and node.children[1].type == token.RPAR
|
||||
)
|
||||
|
||||
|
||||
def is_one_tuple(node: LN) -> bool:
|
||||
"""Return True if `node` holds a tuple with one element, with or without parens."""
|
||||
if node.type == syms.atom:
|
||||
gexp = unwrap_singleton_parenthesis(node)
|
||||
if gexp is None or gexp.type != syms.testlist_gexp:
|
||||
return False
|
||||
|
||||
return len(gexp.children) == 2 and gexp.children[1].type == token.COMMA
|
||||
|
||||
return (
|
||||
node.type in IMPLICIT_TUPLE
|
||||
and len(node.children) == 2
|
||||
and node.children[1].type == token.COMMA
|
||||
)
|
||||
|
||||
|
||||
def is_one_sequence_between(
|
||||
opening: Leaf,
|
||||
closing: Leaf,
|
||||
leaves: List[Leaf],
|
||||
brackets: Tuple[int, int] = (token.LPAR, token.RPAR),
|
||||
) -> bool:
|
||||
"""Return True if content between `opening` and `closing` is a one-sequence."""
|
||||
if (opening.type, closing.type) != brackets:
|
||||
return False
|
||||
|
||||
depth = closing.bracket_depth + 1
|
||||
for _opening_index, leaf in enumerate(leaves):
|
||||
if leaf is opening:
|
||||
break
|
||||
|
||||
else:
|
||||
raise LookupError("Opening paren not found in `leaves`")
|
||||
|
||||
commas = 0
|
||||
_opening_index += 1
|
||||
for leaf in leaves[_opening_index:]:
|
||||
if leaf is closing:
|
||||
break
|
||||
|
||||
bracket_depth = leaf.bracket_depth
|
||||
if bracket_depth == depth and leaf.type == token.COMMA:
|
||||
commas += 1
|
||||
if leaf.parent and leaf.parent.type in {
|
||||
syms.arglist,
|
||||
syms.typedargslist,
|
||||
}:
|
||||
commas += 1
|
||||
break
|
||||
|
||||
return commas < 2
|
||||
|
||||
|
||||
def is_walrus_assignment(node: LN) -> bool:
|
||||
"""Return True iff `node` is of the shape ( test := test )"""
|
||||
inner = unwrap_singleton_parenthesis(node)
|
||||
return inner is not None and inner.type == syms.namedexpr_test
|
||||
|
||||
|
||||
def is_simple_decorator_trailer(node: LN, last: bool = False) -> bool:
|
||||
"""Return True iff `node` is a trailer valid in a simple decorator"""
|
||||
return node.type == syms.trailer and (
|
||||
(
|
||||
len(node.children) == 2
|
||||
and node.children[0].type == token.DOT
|
||||
and node.children[1].type == token.NAME
|
||||
)
|
||||
# last trailer can be an argument-less parentheses pair
|
||||
or (
|
||||
last
|
||||
and len(node.children) == 2
|
||||
and node.children[0].type == token.LPAR
|
||||
and node.children[1].type == token.RPAR
|
||||
)
|
||||
# last trailer can be arguments
|
||||
or (
|
||||
last
|
||||
and len(node.children) == 3
|
||||
and node.children[0].type == token.LPAR
|
||||
# and node.children[1].type == syms.argument
|
||||
and node.children[2].type == token.RPAR
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_simple_decorator_expression(node: LN) -> bool:
|
||||
"""Return True iff `node` could be a 'dotted name' decorator
|
||||
|
||||
This function takes the node of the 'namedexpr_test' of the new decorator
|
||||
grammar and test if it would be valid under the old decorator grammar.
|
||||
|
||||
The old grammar was: decorator: @ dotted_name [arguments] NEWLINE
|
||||
The new grammar is : decorator: @ namedexpr_test NEWLINE
|
||||
"""
|
||||
if node.type == token.NAME:
|
||||
return True
|
||||
if node.type == syms.power:
|
||||
if node.children:
|
||||
return (
|
||||
node.children[0].type == token.NAME
|
||||
and all(map(is_simple_decorator_trailer, node.children[1:-1]))
|
||||
and (
|
||||
len(node.children) < 2
|
||||
or is_simple_decorator_trailer(node.children[-1], last=True)
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def is_yield(node: LN) -> bool:
|
||||
"""Return True if `node` holds a `yield` or `yield from` expression."""
|
||||
if node.type == syms.yield_expr:
|
||||
return True
|
||||
|
||||
if is_name_token(node) and node.value == "yield":
|
||||
return True
|
||||
|
||||
if node.type != syms.atom:
|
||||
return False
|
||||
|
||||
if len(node.children) != 3:
|
||||
return False
|
||||
|
||||
lpar, expr, rpar = node.children
|
||||
if lpar.type == token.LPAR and rpar.type == token.RPAR:
|
||||
return is_yield(expr)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_vararg(leaf: Leaf, within: Set[NodeType]) -> bool:
|
||||
"""Return True if `leaf` is a star or double star in a vararg or kwarg.
|
||||
|
||||
If `within` includes VARARGS_PARENTS, this applies to function signatures.
|
||||
If `within` includes UNPACKING_PARENTS, it applies to right hand-side
|
||||
extended iterable unpacking (PEP 3132) and additional unpacking
|
||||
generalizations (PEP 448).
|
||||
"""
|
||||
if leaf.type not in VARARGS_SPECIALS or not leaf.parent:
|
||||
return False
|
||||
|
||||
p = leaf.parent
|
||||
if p.type == syms.star_expr:
|
||||
# Star expressions are also used as assignment targets in extended
|
||||
# iterable unpacking (PEP 3132). See what its parent is instead.
|
||||
if not p.parent:
|
||||
return False
|
||||
|
||||
p = p.parent
|
||||
|
||||
return p.type in within
|
||||
|
||||
|
||||
def is_multiline_string(leaf: Leaf) -> bool:
|
||||
"""Return True if `leaf` is a multiline string that actually spans many lines."""
|
||||
return has_triple_quotes(leaf.value) and "\n" in leaf.value
|
||||
|
||||
|
||||
def is_stub_suite(node: Node) -> bool:
|
||||
"""Return True if `node` is a suite with a stub body."""
|
||||
if (
|
||||
len(node.children) != 4
|
||||
or node.children[0].type != token.NEWLINE
|
||||
or node.children[1].type != token.INDENT
|
||||
or node.children[3].type != token.DEDENT
|
||||
):
|
||||
return False
|
||||
|
||||
return is_stub_body(node.children[2])
|
||||
|
||||
|
||||
def is_stub_body(node: LN) -> bool:
|
||||
"""Return True if `node` is a simple statement containing an ellipsis."""
|
||||
if not isinstance(node, Node) or node.type != syms.simple_stmt:
|
||||
return False
|
||||
|
||||
if len(node.children) != 2:
|
||||
return False
|
||||
|
||||
child = node.children[0]
|
||||
return (
|
||||
child.type == syms.atom
|
||||
and len(child.children) == 3
|
||||
and all(leaf == Leaf(token.DOT, ".") for leaf in child.children)
|
||||
)
|
||||
|
||||
|
||||
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_par(leaf: Leaf) -> bool:
|
||||
return is_empty_lpar(leaf) or is_empty_rpar(leaf)
|
||||
|
||||
|
||||
def is_empty_lpar(leaf: Leaf) -> bool:
|
||||
return leaf.type == token.LPAR and leaf.value == ""
|
||||
|
||||
|
||||
def is_empty_rpar(leaf: Leaf) -> bool:
|
||||
return leaf.type == token.RPAR and leaf.value == ""
|
||||
|
||||
|
||||
def is_import(leaf: Leaf) -> bool:
|
||||
"""Return True if the given leaf starts an import statement."""
|
||||
p = leaf.parent
|
||||
t = leaf.type
|
||||
v = leaf.value
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
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, STANDALONE_COMMENT} and v.startswith("# type:" + suffix)
|
||||
|
||||
|
||||
def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None:
|
||||
"""Wrap `child` in parentheses.
|
||||
|
||||
This replaces `child` with an atom holding the parentheses and the old
|
||||
child. That requires moving the prefix.
|
||||
|
||||
If `visible` is False, the leaves will be valueless (and thus invisible).
|
||||
"""
|
||||
lpar = Leaf(token.LPAR, "(" if visible else "")
|
||||
rpar = Leaf(token.RPAR, ")" if visible else "")
|
||||
prefix = child.prefix
|
||||
child.prefix = ""
|
||||
index = child.remove() or 0
|
||||
new_child = Node(syms.atom, [lpar, child, rpar])
|
||||
new_child.prefix = prefix
|
||||
parent.insert_child(index, new_child)
|
||||
|
||||
|
||||
def unwrap_singleton_parenthesis(node: LN) -> Optional[LN]:
|
||||
"""Returns `wrapped` if `node` is of the shape ( wrapped ).
|
||||
|
||||
Parenthesis can be optional. Returns None otherwise"""
|
||||
if len(node.children) != 3:
|
||||
return None
|
||||
|
||||
lpar, wrapped, rpar = node.children
|
||||
if not (lpar.type == token.LPAR and rpar.type == token.RPAR):
|
||||
return None
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def ensure_visible(leaf: Leaf) -> None:
|
||||
"""Make sure parentheses are visible.
|
||||
|
||||
They could be invisible as part of some statements (see
|
||||
:func:`normalize_invisible_parens` and :func:`visit_import_from`).
|
||||
"""
|
||||
if leaf.type == token.LPAR:
|
||||
leaf.value = "("
|
||||
elif leaf.type == token.RPAR:
|
||||
leaf.value = ")"
|
||||
|
||||
|
||||
def is_name_token(nl: NL) -> TypeGuard[Leaf]:
|
||||
return nl.type == token.NAME
|
||||
|
||||
|
||||
def is_lpar_token(nl: NL) -> TypeGuard[Leaf]:
|
||||
return nl.type == token.LPAR
|
||||
|
||||
|
||||
def is_rpar_token(nl: NL) -> TypeGuard[Leaf]:
|
||||
return nl.type == token.RPAR
|
||||
|
||||
|
||||
def is_string_token(nl: NL) -> TypeGuard[Leaf]:
|
||||
return nl.type == token.STRING
|
||||
BIN
.venv/lib/python3.8/site-packages/black/numerics.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/numerics.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
60
.venv/lib/python3.8/site-packages/black/numerics.py
Normal file
60
.venv/lib/python3.8/site-packages/black/numerics.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
Formatting numeric literals.
|
||||
"""
|
||||
from blib2to3.pytree import Leaf
|
||||
|
||||
|
||||
def format_hex(text: str) -> str:
|
||||
"""
|
||||
Formats a hexadecimal string like "0x12B3"
|
||||
"""
|
||||
before, after = text[:2], text[2:]
|
||||
return f"{before}{after.upper()}"
|
||||
|
||||
|
||||
def format_scientific_notation(text: str) -> str:
|
||||
"""Formats a numeric string utilizing scentific notation"""
|
||||
before, after = text.split("e")
|
||||
sign = ""
|
||||
if after.startswith("-"):
|
||||
after = after[1:]
|
||||
sign = "-"
|
||||
elif after.startswith("+"):
|
||||
after = after[1:]
|
||||
before = format_float_or_int_string(before)
|
||||
return f"{before}e{sign}{after}"
|
||||
|
||||
|
||||
def format_complex_number(text: str) -> str:
|
||||
"""Formats a complex string like `10j`"""
|
||||
number = text[:-1]
|
||||
suffix = text[-1]
|
||||
return f"{format_float_or_int_string(number)}{suffix}"
|
||||
|
||||
|
||||
def format_float_or_int_string(text: str) -> str:
|
||||
"""Formats a float string like "1.0"."""
|
||||
if "." not in text:
|
||||
return text
|
||||
|
||||
before, after = text.split(".")
|
||||
return f"{before or 0}.{after or 0}"
|
||||
|
||||
|
||||
def normalize_numeric_literal(leaf: Leaf) -> None:
|
||||
"""Normalizes numeric (float, int, and complex) literals.
|
||||
|
||||
All letters used in the representation are normalized to lowercase."""
|
||||
text = leaf.value.lower()
|
||||
if text.startswith(("0o", "0b")):
|
||||
# Leave octal and binary literals alone.
|
||||
pass
|
||||
elif text.startswith("0x"):
|
||||
text = format_hex(text)
|
||||
elif "e" in text:
|
||||
text = format_scientific_notation(text)
|
||||
elif text.endswith("j"):
|
||||
text = format_complex_number(text)
|
||||
else:
|
||||
text = format_float_or_int_string(text)
|
||||
leaf.value = text
|
||||
105
.venv/lib/python3.8/site-packages/black/output.py
Normal file
105
.venv/lib/python3.8/site-packages/black/output.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""Nice output for Black.
|
||||
|
||||
The double calls are for patching purposes in tests.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
from mypy_extensions import mypyc_attr
|
||||
import tempfile
|
||||
|
||||
from click import echo, style
|
||||
|
||||
|
||||
@mypyc_attr(patchable=True)
|
||||
def _out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None:
|
||||
if message is not None:
|
||||
if "bold" not in styles:
|
||||
styles["bold"] = True
|
||||
message = style(message, **styles)
|
||||
echo(message, nl=nl, err=True)
|
||||
|
||||
|
||||
@mypyc_attr(patchable=True)
|
||||
def _err(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None:
|
||||
if message is not None:
|
||||
if "fg" not in styles:
|
||||
styles["fg"] = "red"
|
||||
message = style(message, **styles)
|
||||
echo(message, nl=nl, err=True)
|
||||
|
||||
|
||||
@mypyc_attr(patchable=True)
|
||||
def out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None:
|
||||
_out(message, nl=nl, **styles)
|
||||
|
||||
|
||||
def err(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None:
|
||||
_err(message, nl=nl, **styles)
|
||||
|
||||
|
||||
def ipynb_diff(a: str, b: str, a_name: str, b_name: str) -> str:
|
||||
"""Return a unified diff string between each cell in notebooks `a` and `b`."""
|
||||
a_nb = json.loads(a)
|
||||
b_nb = json.loads(b)
|
||||
diff_lines = [
|
||||
diff(
|
||||
"".join(a_nb["cells"][cell_number]["source"]) + "\n",
|
||||
"".join(b_nb["cells"][cell_number]["source"]) + "\n",
|
||||
f"{a_name}:cell_{cell_number}",
|
||||
f"{b_name}:cell_{cell_number}",
|
||||
)
|
||||
for cell_number, cell in enumerate(a_nb["cells"])
|
||||
if cell["cell_type"] == "code"
|
||||
]
|
||||
return "".join(diff_lines)
|
||||
|
||||
|
||||
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 = a.splitlines(keepends=True)
|
||||
b_lines = b.splitlines(keepends=True)
|
||||
diff_lines = []
|
||||
for line in difflib.unified_diff(
|
||||
a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5
|
||||
):
|
||||
# Work around https://bugs.python.org/issue2142
|
||||
# See:
|
||||
# https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html
|
||||
if line[-1] == "\n":
|
||||
diff_lines.append(line)
|
||||
else:
|
||||
diff_lines.append(line + "\n")
|
||||
diff_lines.append("\\ No newline at end of file\n")
|
||||
return "".join(diff_lines)
|
||||
|
||||
|
||||
def color_diff(contents: str) -> str:
|
||||
"""Inject the ANSI color codes to the diff."""
|
||||
lines = contents.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("+++") or line.startswith("---"):
|
||||
line = "\033[1m" + line + "\033[0m" # bold, reset
|
||||
elif line.startswith("@@"):
|
||||
line = "\033[36m" + line + "\033[0m" # cyan, reset
|
||||
elif line.startswith("+"):
|
||||
line = "\033[32m" + line + "\033[0m" # green, reset
|
||||
elif line.startswith("-"):
|
||||
line = "\033[31m" + line + "\033[0m" # red, reset
|
||||
lines[i] = line
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@mypyc_attr(patchable=True)
|
||||
def dump_to_file(*output: str, ensure_final_newline: bool = True) -> str:
|
||||
"""Dump `output` to a temporary file. Return path to the file."""
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", prefix="blk_", suffix=".log", delete=False, encoding="utf8"
|
||||
) as f:
|
||||
for lines in output:
|
||||
f.write(lines)
|
||||
if ensure_final_newline and lines and lines[-1] != "\n":
|
||||
f.write("\n")
|
||||
return f.name
|
||||
BIN
.venv/lib/python3.8/site-packages/black/parsing.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/parsing.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
272
.venv/lib/python3.8/site-packages/black/parsing.py
Normal file
272
.venv/lib/python3.8/site-packages/black/parsing.py
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
"""
|
||||
Parse Python code and perform AST validation.
|
||||
"""
|
||||
import ast
|
||||
import platform
|
||||
import sys
|
||||
from typing import Any, Iterable, Iterator, List, Set, Tuple, Type, Union
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from typing_extensions import Final
|
||||
else:
|
||||
from typing import Final
|
||||
|
||||
# lib2to3 fork
|
||||
from blib2to3.pytree import Node, Leaf
|
||||
from blib2to3 import pygram
|
||||
from blib2to3.pgen2 import driver
|
||||
from blib2to3.pgen2.grammar import Grammar
|
||||
from blib2to3.pgen2.parse import ParseError
|
||||
from blib2to3.pgen2.tokenize import TokenError
|
||||
|
||||
from black.mode import TargetVersion, Feature, supports_feature
|
||||
from black.nodes import syms
|
||||
|
||||
ast3: Any
|
||||
|
||||
_IS_PYPY = platform.python_implementation() == "PyPy"
|
||||
|
||||
try:
|
||||
from typed_ast import ast3
|
||||
except ImportError:
|
||||
# Either our python version is too low, or we're on pypy
|
||||
if sys.version_info < (3, 7) or (sys.version_info < (3, 8) and not _IS_PYPY):
|
||||
print(
|
||||
"The typed_ast package is required but not installed.\n"
|
||||
"You can upgrade to Python 3.8+ or install typed_ast with\n"
|
||||
"`python3 -m pip install typed-ast`.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
ast3 = ast
|
||||
|
||||
|
||||
PY2_HINT: Final = "Python 2 support was removed in version 22.0."
|
||||
|
||||
|
||||
class InvalidInput(ValueError):
|
||||
"""Raised when input source code fails all parse attempts."""
|
||||
|
||||
|
||||
def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:
|
||||
if not target_versions:
|
||||
# No target_version specified, so try all grammars.
|
||||
return [
|
||||
# Python 3.7+
|
||||
pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords,
|
||||
# Python 3.0-3.6
|
||||
pygram.python_grammar_no_print_statement_no_exec_statement,
|
||||
# Python 3.10+
|
||||
pygram.python_grammar_soft_keywords,
|
||||
]
|
||||
|
||||
grammars = []
|
||||
# If we have to parse both, try to parse async as a keyword first
|
||||
if not supports_feature(
|
||||
target_versions, Feature.ASYNC_IDENTIFIERS
|
||||
) and not supports_feature(target_versions, Feature.PATTERN_MATCHING):
|
||||
# Python 3.7-3.9
|
||||
grammars.append(
|
||||
pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords
|
||||
)
|
||||
if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):
|
||||
# Python 3.0-3.6
|
||||
grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement)
|
||||
if supports_feature(target_versions, Feature.PATTERN_MATCHING):
|
||||
# Python 3.10+
|
||||
grammars.append(pygram.python_grammar_soft_keywords)
|
||||
|
||||
# At least one of the above branches must have been taken, because every Python
|
||||
# version has exactly one of the two 'ASYNC_*' flags
|
||||
return grammars
|
||||
|
||||
|
||||
def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node:
|
||||
"""Given a string with source, return the lib2to3 Node."""
|
||||
if not src_txt.endswith("\n"):
|
||||
src_txt += "\n"
|
||||
|
||||
grammars = get_grammars(set(target_versions))
|
||||
errors = {}
|
||||
for grammar in grammars:
|
||||
drv = driver.Driver(grammar)
|
||||
try:
|
||||
result = drv.parse_string(src_txt, True)
|
||||
break
|
||||
|
||||
except ParseError as pe:
|
||||
lineno, column = pe.context[1]
|
||||
lines = src_txt.splitlines()
|
||||
try:
|
||||
faulty_line = lines[lineno - 1]
|
||||
except IndexError:
|
||||
faulty_line = "<line number missing in source>"
|
||||
errors[grammar.version] = InvalidInput(
|
||||
f"Cannot parse: {lineno}:{column}: {faulty_line}"
|
||||
)
|
||||
|
||||
except TokenError as te:
|
||||
# In edge cases these are raised; and typically don't have a "faulty_line".
|
||||
lineno, column = te.args[1]
|
||||
errors[grammar.version] = InvalidInput(
|
||||
f"Cannot parse: {lineno}:{column}: {te.args[0]}"
|
||||
)
|
||||
|
||||
else:
|
||||
# Choose the latest version when raising the actual parsing error.
|
||||
assert len(errors) >= 1
|
||||
exc = errors[max(errors)]
|
||||
|
||||
if matches_grammar(src_txt, pygram.python_grammar) or matches_grammar(
|
||||
src_txt, pygram.python_grammar_no_print_statement
|
||||
):
|
||||
original_msg = exc.args[0]
|
||||
msg = f"{original_msg}\n{PY2_HINT}"
|
||||
raise InvalidInput(msg) from None
|
||||
|
||||
raise exc from None
|
||||
|
||||
if isinstance(result, Leaf):
|
||||
result = Node(syms.file_input, [result])
|
||||
return result
|
||||
|
||||
|
||||
def matches_grammar(src_txt: str, grammar: Grammar) -> bool:
|
||||
drv = driver.Driver(grammar)
|
||||
try:
|
||||
drv.parse_string(src_txt, True)
|
||||
except (ParseError, TokenError, IndentationError):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def lib2to3_unparse(node: Node) -> str:
|
||||
"""Given a lib2to3 node, return its string representation."""
|
||||
code = str(node)
|
||||
return code
|
||||
|
||||
|
||||
def parse_single_version(
|
||||
src: str, version: Tuple[int, int]
|
||||
) -> Union[ast.AST, ast3.AST]:
|
||||
filename = "<unknown>"
|
||||
# typed_ast is needed because of feature version limitations in the builtin ast
|
||||
if sys.version_info >= (3, 8) and version >= (3,):
|
||||
return ast.parse(src, filename, feature_version=version)
|
||||
elif version >= (3,):
|
||||
if _IS_PYPY:
|
||||
return ast3.parse(src, filename)
|
||||
else:
|
||||
return ast3.parse(src, filename, feature_version=version[1])
|
||||
raise AssertionError("INTERNAL ERROR: Tried parsing unsupported Python version!")
|
||||
|
||||
|
||||
def parse_ast(src: str) -> Union[ast.AST, ast3.AST]:
|
||||
# TODO: support Python 4+ ;)
|
||||
versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]
|
||||
|
||||
first_error = ""
|
||||
for version in sorted(versions, reverse=True):
|
||||
try:
|
||||
return parse_single_version(src, version)
|
||||
except SyntaxError as e:
|
||||
if not first_error:
|
||||
first_error = str(e)
|
||||
|
||||
raise SyntaxError(first_error)
|
||||
|
||||
|
||||
ast3_AST: Final[Type[ast3.AST]] = ast3.AST
|
||||
|
||||
|
||||
def _normalize(lineend: str, value: str) -> str:
|
||||
# To normalize, we strip any leading and trailing space from
|
||||
# each line...
|
||||
stripped: List[str] = [i.strip() for i in value.splitlines()]
|
||||
normalized = lineend.join(stripped)
|
||||
# ...and remove any blank lines at the beginning and end of
|
||||
# the whole string
|
||||
return normalized.strip()
|
||||
|
||||
|
||||
def stringify_ast(node: Union[ast.AST, ast3.AST], depth: int = 0) -> Iterator[str]:
|
||||
"""Simple visitor generating strings to compare ASTs by content."""
|
||||
|
||||
node = fixup_ast_constants(node)
|
||||
|
||||
yield f"{' ' * depth}{node.__class__.__name__}("
|
||||
|
||||
type_ignore_classes: Tuple[Type[Any], ...]
|
||||
for field in sorted(node._fields): # noqa: F402
|
||||
# TypeIgnore will not be present using pypy < 3.8, so need for this
|
||||
if not (_IS_PYPY and sys.version_info < (3, 8)):
|
||||
# TypeIgnore has only one field 'lineno' which breaks this comparison
|
||||
type_ignore_classes = (ast3.TypeIgnore,)
|
||||
if sys.version_info >= (3, 8):
|
||||
type_ignore_classes += (ast.TypeIgnore,)
|
||||
if isinstance(node, type_ignore_classes):
|
||||
break
|
||||
|
||||
try:
|
||||
value: object = getattr(node, field)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
yield f"{' ' * (depth+1)}{field}="
|
||||
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
# Ignore nested tuples within del statements, because we may insert
|
||||
# parentheses and they change the AST.
|
||||
if (
|
||||
field == "targets"
|
||||
and isinstance(node, (ast.Delete, ast3.Delete))
|
||||
and isinstance(item, (ast.Tuple, ast3.Tuple))
|
||||
):
|
||||
for elt in item.elts:
|
||||
yield from stringify_ast(elt, depth + 2)
|
||||
|
||||
elif isinstance(item, (ast.AST, ast3.AST)):
|
||||
yield from stringify_ast(item, depth + 2)
|
||||
|
||||
# Note that we are referencing the typed-ast ASTs via global variables and not
|
||||
# direct module attribute accesses because that breaks mypyc. It's probably
|
||||
# something to do with the ast3 variables being marked as Any leading
|
||||
# mypy to think this branch is always taken, leaving the rest of the code
|
||||
# unanalyzed. Tighting up the types for the typed-ast AST types avoids the
|
||||
# mypyc crash.
|
||||
elif isinstance(value, (ast.AST, ast3_AST)):
|
||||
yield from stringify_ast(value, depth + 2)
|
||||
|
||||
else:
|
||||
normalized: object
|
||||
# Constant strings may be indented across newlines, if they are
|
||||
# docstrings; fold spaces after newlines when comparing. Similarly,
|
||||
# trailing and leading space may be removed.
|
||||
if (
|
||||
isinstance(node, ast.Constant)
|
||||
and field == "value"
|
||||
and isinstance(value, str)
|
||||
):
|
||||
normalized = _normalize("\n", value)
|
||||
else:
|
||||
normalized = value
|
||||
yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}"
|
||||
|
||||
yield f"{' ' * depth}) # /{node.__class__.__name__}"
|
||||
|
||||
|
||||
def fixup_ast_constants(node: Union[ast.AST, ast3.AST]) -> Union[ast.AST, ast3.AST]:
|
||||
"""Map ast nodes deprecated in 3.8 to Constant."""
|
||||
if isinstance(node, (ast.Str, ast3.Str, ast.Bytes, ast3.Bytes)):
|
||||
return ast.Constant(value=node.s)
|
||||
|
||||
if isinstance(node, (ast.Num, ast3.Num)):
|
||||
return ast.Constant(value=node.n)
|
||||
|
||||
if isinstance(node, (ast.NameConstant, ast3.NameConstant)):
|
||||
return ast.Constant(value=node.value)
|
||||
|
||||
return node
|
||||
0
.venv/lib/python3.8/site-packages/black/py.typed
Normal file
0
.venv/lib/python3.8/site-packages/black/py.typed
Normal file
106
.venv/lib/python3.8/site-packages/black/report.py
Normal file
106
.venv/lib/python3.8/site-packages/black/report.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"""
|
||||
Summarize Black runs to users.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from click import style
|
||||
|
||||
from black.output import out, err
|
||||
|
||||
|
||||
class Changed(Enum):
|
||||
NO = 0
|
||||
CACHED = 1
|
||||
YES = 2
|
||||
|
||||
|
||||
class NothingChanged(UserWarning):
|
||||
"""Raised when reformatted code is the same as source."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Report:
|
||||
"""Provides a reformatting counter. Can be rendered with `str(report)`."""
|
||||
|
||||
check: bool = False
|
||||
diff: bool = False
|
||||
quiet: bool = False
|
||||
verbose: bool = False
|
||||
change_count: int = 0
|
||||
same_count: int = 0
|
||||
failure_count: int = 0
|
||||
|
||||
def done(self, src: Path, changed: Changed) -> None:
|
||||
"""Increment the counter for successful reformatting. Write out a message."""
|
||||
if changed is Changed.YES:
|
||||
reformatted = "would reformat" if self.check or self.diff else "reformatted"
|
||||
if self.verbose or not self.quiet:
|
||||
out(f"{reformatted} {src}")
|
||||
self.change_count += 1
|
||||
else:
|
||||
if self.verbose:
|
||||
if changed is Changed.NO:
|
||||
msg = f"{src} already well formatted, good job."
|
||||
else:
|
||||
msg = f"{src} wasn't modified on disk since last run."
|
||||
out(msg, 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}")
|
||||
self.failure_count += 1
|
||||
|
||||
def path_ignored(self, path: Path, message: str) -> None:
|
||||
if self.verbose:
|
||||
out(f"{path} ignored: {message}", bold=False)
|
||||
|
||||
@property
|
||||
def return_code(self) -> int:
|
||||
"""Return the exit code that the app should use.
|
||||
|
||||
This considers the current state of changed files and failures:
|
||||
- if there were any failures, return 123;
|
||||
- if any files were changed and --check is being used, return 1;
|
||||
- otherwise return 0.
|
||||
"""
|
||||
# According to http://tldp.org/LDP/abs/html/exitcodes.html starting with
|
||||
# 126 we have special return codes reserved by the shell.
|
||||
if self.failure_count:
|
||||
return 123
|
||||
|
||||
elif self.change_count and self.check:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Render a color report of the current state.
|
||||
|
||||
Use `click.unstyle` to remove colors.
|
||||
"""
|
||||
if self.check or self.diff:
|
||||
reformatted = "would be reformatted"
|
||||
unchanged = "would be left unchanged"
|
||||
failed = "would fail to reformat"
|
||||
else:
|
||||
reformatted = "reformatted"
|
||||
unchanged = "left unchanged"
|
||||
failed = "failed to reformat"
|
||||
report = []
|
||||
if self.change_count:
|
||||
s = "s" if self.change_count > 1 else ""
|
||||
report.append(
|
||||
style(f"{self.change_count} file{s} ", bold=True, fg="blue")
|
||||
+ style(f"{reformatted}", bold=True)
|
||||
)
|
||||
|
||||
if self.same_count:
|
||||
s = "s" if self.same_count > 1 else ""
|
||||
report.append(style(f"{self.same_count} file{s} ", fg="blue") + unchanged)
|
||||
if self.failure_count:
|
||||
s = "s" if self.failure_count > 1 else ""
|
||||
report.append(style(f"{self.failure_count} file{s} {failed}", fg="red"))
|
||||
return ", ".join(report) + "."
|
||||
BIN
.venv/lib/python3.8/site-packages/black/rusty.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/rusty.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
28
.venv/lib/python3.8/site-packages/black/rusty.py
Normal file
28
.venv/lib/python3.8/site-packages/black/rusty.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"""An error-handling model influenced by that used by the Rust programming language
|
||||
|
||||
See https://doc.rust-lang.org/book/ch09-00-error-handling.html.
|
||||
"""
|
||||
from typing import Generic, TypeVar, Union
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
E = TypeVar("E", bound=Exception)
|
||||
|
||||
|
||||
class Ok(Generic[T]):
|
||||
def __init__(self, value: T) -> None:
|
||||
self._value = value
|
||||
|
||||
def ok(self) -> T:
|
||||
return self._value
|
||||
|
||||
|
||||
class Err(Generic[E]):
|
||||
def __init__(self, e: E) -> None:
|
||||
self._e = e
|
||||
|
||||
def err(self) -> E:
|
||||
return self._e
|
||||
|
||||
|
||||
Result = Union[Ok[T], Err[E]]
|
||||
BIN
.venv/lib/python3.8/site-packages/black/strings.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/strings.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
238
.venv/lib/python3.8/site-packages/black/strings.py
Normal file
238
.venv/lib/python3.8/site-packages/black/strings.py
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
"""
|
||||
Simple formatting on strings. Further string formatting code is in trans.py.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from typing import List, Pattern
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from typing_extensions import Final
|
||||
else:
|
||||
from typing import Final
|
||||
|
||||
|
||||
STRING_PREFIX_CHARS: Final = "furbFURB" # All possible string prefix characters.
|
||||
STRING_PREFIX_RE: Final = re.compile(
|
||||
r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", re.DOTALL
|
||||
)
|
||||
FIRST_NON_WHITESPACE_RE: Final = re.compile(r"\s*\t+\s*(\S)")
|
||||
|
||||
|
||||
def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str:
|
||||
"""Replace `regex` with `replacement` twice on `original`.
|
||||
|
||||
This is used by string normalization to perform replaces on
|
||||
overlapping matches.
|
||||
"""
|
||||
return regex.sub(replacement, regex.sub(replacement, original))
|
||||
|
||||
|
||||
def has_triple_quotes(string: str) -> bool:
|
||||
"""
|
||||
Returns:
|
||||
True iff @string starts with three quotation characters.
|
||||
"""
|
||||
raw_string = string.lstrip(STRING_PREFIX_CHARS)
|
||||
return raw_string[:3] in {'"""', "'''"}
|
||||
|
||||
|
||||
def lines_with_leading_tabs_expanded(s: str) -> List[str]:
|
||||
"""
|
||||
Splits string into lines and expands only leading tabs (following the normal
|
||||
Python rules)
|
||||
"""
|
||||
lines = []
|
||||
for line in s.splitlines():
|
||||
# Find the index of the first non-whitespace character after a string of
|
||||
# whitespace that includes at least one tab
|
||||
match = FIRST_NON_WHITESPACE_RE.match(line)
|
||||
if match:
|
||||
first_non_whitespace_idx = match.start(1)
|
||||
|
||||
lines.append(
|
||||
line[:first_non_whitespace_idx].expandtabs()
|
||||
+ line[first_non_whitespace_idx:]
|
||||
)
|
||||
else:
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def fix_docstring(docstring: str, prefix: str) -> str:
|
||||
# https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
|
||||
if not docstring:
|
||||
return ""
|
||||
lines = lines_with_leading_tabs_expanded(docstring)
|
||||
# Determine minimum indentation (first line doesn't count):
|
||||
indent = sys.maxsize
|
||||
for line in lines[1:]:
|
||||
stripped = line.lstrip()
|
||||
if stripped:
|
||||
indent = min(indent, len(line) - len(stripped))
|
||||
# Remove indentation (first line is special):
|
||||
trimmed = [lines[0].strip()]
|
||||
if indent < sys.maxsize:
|
||||
last_line_idx = len(lines) - 2
|
||||
for i, line in enumerate(lines[1:]):
|
||||
stripped_line = line[indent:].rstrip()
|
||||
if stripped_line or i == last_line_idx:
|
||||
trimmed.append(prefix + stripped_line)
|
||||
else:
|
||||
trimmed.append("")
|
||||
return "\n".join(trimmed)
|
||||
|
||||
|
||||
def get_string_prefix(string: str) -> str:
|
||||
"""
|
||||
Pre-conditions:
|
||||
* assert_is_leaf_string(@string)
|
||||
|
||||
Returns:
|
||||
@string's prefix (e.g. '', 'r', 'f', or 'rf').
|
||||
"""
|
||||
assert_is_leaf_string(string)
|
||||
|
||||
prefix = ""
|
||||
prefix_idx = 0
|
||||
while string[prefix_idx] in STRING_PREFIX_CHARS:
|
||||
prefix += string[prefix_idx]
|
||||
prefix_idx += 1
|
||||
|
||||
return prefix
|
||||
|
||||
|
||||
def assert_is_leaf_string(string: str) -> None:
|
||||
"""
|
||||
Checks the pre-condition that @string has the format that you would expect
|
||||
of `leaf.value` where `leaf` is some Leaf such that `leaf.type ==
|
||||
token.STRING`. A more precise description of the pre-conditions that are
|
||||
checked are listed below.
|
||||
|
||||
Pre-conditions:
|
||||
* @string starts with either ', ", <prefix>', or <prefix>" where
|
||||
`set(<prefix>)` is some subset of `set(STRING_PREFIX_CHARS)`.
|
||||
* @string ends with a quote character (' or ").
|
||||
|
||||
Raises:
|
||||
AssertionError(...) if the pre-conditions listed above are not
|
||||
satisfied.
|
||||
"""
|
||||
dquote_idx = string.find('"')
|
||||
squote_idx = string.find("'")
|
||||
if -1 in [dquote_idx, squote_idx]:
|
||||
quote_idx = max(dquote_idx, squote_idx)
|
||||
else:
|
||||
quote_idx = min(squote_idx, dquote_idx)
|
||||
|
||||
assert (
|
||||
0 <= quote_idx < len(string) - 1
|
||||
), f"{string!r} is missing a starting quote character (' or \")."
|
||||
assert string[-1] in (
|
||||
"'",
|
||||
'"',
|
||||
), f"{string!r} is missing an ending quote character (' or \")."
|
||||
assert set(string[:quote_idx]).issubset(
|
||||
set(STRING_PREFIX_CHARS)
|
||||
), f"{set(string[:quote_idx])} is NOT a subset of {set(STRING_PREFIX_CHARS)}."
|
||||
|
||||
|
||||
def normalize_string_prefix(s: str) -> str:
|
||||
"""Make all string prefixes lowercase."""
|
||||
match = STRING_PREFIX_RE.match(s)
|
||||
assert match is not None, f"failed to match string {s!r}"
|
||||
orig_prefix = match.group(1)
|
||||
new_prefix = (
|
||||
orig_prefix.replace("F", "f")
|
||||
.replace("B", "b")
|
||||
.replace("U", "")
|
||||
.replace("u", "")
|
||||
)
|
||||
|
||||
# Python syntax guarantees max 2 prefixes and that one of them is "r"
|
||||
if len(new_prefix) == 2 and "r" != new_prefix[0].lower():
|
||||
new_prefix = new_prefix[::-1]
|
||||
return f"{new_prefix}{match.group(2)}"
|
||||
|
||||
|
||||
# Re(gex) does actually cache patterns internally but this still improves
|
||||
# performance on a long list literal of strings by 5-9% since lru_cache's
|
||||
# caching overhead is much lower.
|
||||
@lru_cache(maxsize=64)
|
||||
def _cached_compile(pattern: str) -> Pattern[str]:
|
||||
return re.compile(pattern)
|
||||
|
||||
|
||||
def normalize_string_quotes(s: str) -> str:
|
||||
"""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.
|
||||
"""
|
||||
value = s.lstrip(STRING_PREFIX_CHARS)
|
||||
if value[:3] == '"""':
|
||||
return s
|
||||
|
||||
elif value[:3] == "'''":
|
||||
orig_quote = "'''"
|
||||
new_quote = '"""'
|
||||
elif value[0] == '"':
|
||||
orig_quote = '"'
|
||||
new_quote = "'"
|
||||
else:
|
||||
orig_quote = "'"
|
||||
new_quote = '"'
|
||||
first_quote_pos = s.find(orig_quote)
|
||||
if first_quote_pos == -1:
|
||||
return s # There's an internal error
|
||||
|
||||
prefix = s[:first_quote_pos]
|
||||
unescaped_new_quote = _cached_compile(rf"(([^\\]|^)(\\\\)*){new_quote}")
|
||||
escaped_new_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){new_quote}")
|
||||
escaped_orig_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){orig_quote}")
|
||||
body = s[first_quote_pos + len(orig_quote) : -len(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 s
|
||||
|
||||
# Do not introduce or remove backslashes in raw strings
|
||||
new_body = body
|
||||
else:
|
||||
# remove unnecessary escapes
|
||||
new_body = sub_twice(escaped_new_quote, rf"\1\2{new_quote}", body)
|
||||
if body != new_body:
|
||||
# Consider the string without unnecessary escapes as the original
|
||||
body = new_body
|
||||
s = f"{prefix}{orig_quote}{body}{orig_quote}"
|
||||
new_body = sub_twice(escaped_orig_quote, rf"\1\2{orig_quote}", new_body)
|
||||
new_body = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_body)
|
||||
if "f" in prefix.casefold():
|
||||
matches = re.findall(
|
||||
r"""
|
||||
(?:(?<!\{)|^)\{ # start of the string or a non-{ followed by a single {
|
||||
([^{].*?) # contents of the brackets except if begins with {{
|
||||
\}(?:(?!\})|$) # A } followed by end of the string or a non-}
|
||||
""",
|
||||
new_body,
|
||||
re.VERBOSE,
|
||||
)
|
||||
for m in matches:
|
||||
if "\\" in str(m):
|
||||
# Do not introduce backslashes in interpolated expressions
|
||||
return s
|
||||
|
||||
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 s # Do not introduce more escaping
|
||||
|
||||
if new_escape_count == orig_escape_count and orig_quote == '"':
|
||||
return s # Prefer double quotes
|
||||
|
||||
return f"{prefix}{new_quote}{new_body}{new_quote}"
|
||||
BIN
.venv/lib/python3.8/site-packages/black/trans.cpython-38-x86_64-linux-gnu.so
Executable file
BIN
.venv/lib/python3.8/site-packages/black/trans.cpython-38-x86_64-linux-gnu.so
Executable file
Binary file not shown.
2146
.venv/lib/python3.8/site-packages/black/trans.py
Normal file
2146
.venv/lib/python3.8/site-packages/black/trans.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue