kaiserpister's picture
Upload folder using huggingface_hub
6eefbd7
"""
Simple formatting on strings. Further string formatting code is in trans.py.
"""
import re
import sys
from functools import lru_cache
from typing import Final, List, Match, Pattern
from black._width_table import WIDTH_TABLE
from blib2to3.pytree import Leaf
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)")
UNICODE_ESCAPE_RE: Final = re.compile(
r"(?P<backslashes>\\+)(?P<body>"
r"(u(?P<u>[a-fA-F0-9]{4}))" # Character with 16-bit hex value xxxx
r"|(U(?P<U>[a-fA-F0-9]{8}))" # Character with 32-bit hex value xxxxxxxx
r"|(x(?P<x>[a-fA-F0-9]{2}))" # Character with hex value hh
r"|(N\{(?P<N>[a-zA-Z0-9 \-]{2,})\})" # Character named name in the Unicode database
r")",
re.VERBOSE,
)
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}"
def normalize_unicode_escape_sequences(leaf: Leaf) -> None:
"""Replace hex codes in Unicode escape sequences with lowercase representation."""
text = leaf.value
prefix = get_string_prefix(text)
if "r" in prefix.lower():
return
def replace(m: Match[str]) -> str:
groups = m.groupdict()
back_slashes = groups["backslashes"]
if len(back_slashes) % 2 == 0:
return back_slashes + groups["body"]
if groups["u"]:
# \u
return back_slashes + "u" + groups["u"].lower()
elif groups["U"]:
# \U
return back_slashes + "U" + groups["U"].lower()
elif groups["x"]:
# \x
return back_slashes + "x" + groups["x"].lower()
else:
assert groups["N"], f"Unexpected match: {m}"
# \N{}
return back_slashes + "N{" + groups["N"].upper() + "}"
leaf.value = re.sub(UNICODE_ESCAPE_RE, replace, text)
@lru_cache(maxsize=4096)
def char_width(char: str) -> int:
"""Return the width of a single character as it would be displayed in a
terminal or editor (which respects Unicode East Asian Width).
Full width characters are counted as 2, while half width characters are
counted as 1. Also control characters are counted as 0.
"""
table = WIDTH_TABLE
codepoint = ord(char)
highest = len(table) - 1
lowest = 0
idx = highest // 2
while True:
start_codepoint, end_codepoint, width = table[idx]
if codepoint < start_codepoint:
highest = idx - 1
elif codepoint > end_codepoint:
lowest = idx + 1
else:
return 0 if width < 0 else width
if highest < lowest:
break
idx = (highest + lowest) // 2
return 1
def str_width(line_str: str) -> int:
"""Return the width of `line_str` as it would be displayed in a terminal
or editor (which respects Unicode East Asian Width).
You could utilize this function to determine, for example, if a string
is too wide to display in a terminal or editor.
"""
if line_str.isascii():
# Fast path for a line consisting of only ASCII characters
return len(line_str)
return sum(map(char_width, line_str))
def count_chars_in_width(line_str: str, max_width: int) -> int:
"""Count the number of characters in `line_str` that would fit in a
terminal or editor of `max_width` (which respects Unicode East Asian
Width).
"""
total_width = 0
for i, char in enumerate(line_str):
width = char_width(char)
if width + total_width > max_width:
return i
total_width += width
return len(line_str)