156 lines
4.8 KiB
Python
156 lines
4.8 KiB
Python
from typing import Collection, List
|
|
from sys import maxsize
|
|
|
|
__all__ = [
|
|
"dedent_block_string_lines",
|
|
"is_printable_as_block_string",
|
|
"print_block_string",
|
|
]
|
|
|
|
|
|
def dedent_block_string_lines(lines: Collection[str]) -> List[str]:
|
|
"""Produce the value of a block string from its parsed raw value.
|
|
|
|
This function works similar to CoffeeScript's block string,
|
|
Python's docstring trim or Ruby's strip_heredoc.
|
|
|
|
It implements the GraphQL spec's BlockStringValue() static algorithm.
|
|
|
|
Note that this is very similar to Python's inspect.cleandoc() function.
|
|
The difference is that the latter also expands tabs to spaces and
|
|
removes whitespace at the beginning of the first line. Python also has
|
|
textwrap.dedent() which uses a completely different algorithm.
|
|
|
|
For internal use only.
|
|
"""
|
|
common_indent = maxsize
|
|
first_non_empty_line = None
|
|
last_non_empty_line = -1
|
|
|
|
for i, line in enumerate(lines):
|
|
indent = leading_white_space(line)
|
|
|
|
if indent == len(line):
|
|
continue # skip empty lines
|
|
|
|
if first_non_empty_line is None:
|
|
first_non_empty_line = i
|
|
last_non_empty_line = i
|
|
|
|
if i and indent < common_indent:
|
|
common_indent = indent
|
|
|
|
if first_non_empty_line is None:
|
|
first_non_empty_line = 0
|
|
|
|
return [ # Remove common indentation from all lines but first.
|
|
line[common_indent:] if i else line for i, line in enumerate(lines)
|
|
][ # Remove leading and trailing blank lines.
|
|
first_non_empty_line : last_non_empty_line + 1
|
|
]
|
|
|
|
|
|
def leading_white_space(s: str) -> int:
|
|
i = 0
|
|
for c in s:
|
|
if c not in " \t":
|
|
return i
|
|
i += 1
|
|
return i
|
|
|
|
|
|
def is_printable_as_block_string(value: str) -> bool:
|
|
"""Check whether the given string is printable as a block string.
|
|
|
|
For internal use only.
|
|
"""
|
|
if not isinstance(value, str):
|
|
value = str(value) # resolve lazy string proxy object
|
|
|
|
if not value:
|
|
return True # emtpy string is printable
|
|
|
|
is_empty_line = True
|
|
has_indent = False
|
|
has_common_indent = True
|
|
seen_non_empty_line = False
|
|
|
|
for c in value:
|
|
if c == "\n":
|
|
if is_empty_line and not seen_non_empty_line:
|
|
return False # has leading new line
|
|
seen_non_empty_line = True
|
|
is_empty_line = True
|
|
has_indent = False
|
|
elif c in " \t":
|
|
has_indent = has_indent or is_empty_line
|
|
elif c <= "\x0f":
|
|
return False
|
|
else:
|
|
has_common_indent = has_common_indent and has_indent
|
|
is_empty_line = False
|
|
|
|
if is_empty_line:
|
|
return False # has trailing empty lines
|
|
|
|
if has_common_indent and seen_non_empty_line:
|
|
return False # has internal indent
|
|
|
|
return True
|
|
|
|
|
|
def print_block_string(value: str, minimize: bool = False) -> str:
|
|
"""Print a block string in the indented block form.
|
|
|
|
Prints a block string in the indented block form by adding a leading and
|
|
trailing blank line. However, if a block string starts with whitespace and
|
|
is a single-line, adding a leading blank line would strip that whitespace.
|
|
|
|
For internal use only.
|
|
"""
|
|
if not isinstance(value, str):
|
|
value = str(value) # resolve lazy string proxy object
|
|
|
|
escaped_value = value.replace('"""', '\\"""')
|
|
|
|
# Expand a block string's raw value into independent lines.
|
|
lines = escaped_value.splitlines() or [""]
|
|
num_lines = len(lines)
|
|
is_single_line = num_lines == 1
|
|
|
|
# If common indentation is found,
|
|
# we can fix some of those cases by adding a leading new line.
|
|
force_leading_new_line = num_lines > 1 and all(
|
|
not line or line[0] in " \t" for line in lines[1:]
|
|
)
|
|
|
|
# Trailing triple quotes just looks confusing but doesn't force trailing new line.
|
|
has_trailing_triple_quotes = escaped_value.endswith('\\"""')
|
|
|
|
# Trailing quote (single or double) or slash forces trailing new line
|
|
has_trailing_quote = value.endswith('"') and not has_trailing_triple_quotes
|
|
has_trailing_slash = value.endswith("\\")
|
|
force_trailing_new_line = has_trailing_quote or has_trailing_slash
|
|
|
|
print_as_multiple_lines = not minimize and (
|
|
# add leading and trailing new lines only if it improves readability
|
|
not is_single_line
|
|
or len(value) > 70
|
|
or force_trailing_new_line
|
|
or force_leading_new_line
|
|
or has_trailing_triple_quotes
|
|
)
|
|
|
|
# Format a multi-line block quote to account for leading space.
|
|
skip_leading_new_line = is_single_line and value and value[0] in " \t"
|
|
before = (
|
|
"\n"
|
|
if print_as_multiple_lines
|
|
and not skip_leading_new_line
|
|
or force_leading_new_line
|
|
else ""
|
|
)
|
|
after = "\n" if print_as_multiple_lines or force_trailing_new_line else ""
|
|
|
|
return f'"""{before}{escaped_value}{after}"""'
|