1+ from typing import Collection , List
2+ from sys import maxsize
3+
14__all__ = [
2- "dedent_block_string_value" ,
5+ "dedent_block_string_lines" ,
6+ "is_printable_as_block_string" ,
37 "print_block_string" ,
4- "get_block_string_indentation" ,
58]
69
710
8- def dedent_block_string_value ( raw_string : str ) -> str :
11+ def dedent_block_string_lines ( lines : Collection [ str ] ) -> List [ str ] :
912 """Produce the value of a block string from its parsed raw value.
1013
11- Similar to CoffeeScript's block string, Python's docstring trim or Ruby's
12- strip_heredoc.
14+ This function works similar to CoffeeScript's block string,
15+ Python's docstring trim or Ruby's strip_heredoc.
1316
14- This implements the GraphQL spec's BlockStringValue() static algorithm.
17+ It implements the GraphQL spec's BlockStringValue() static algorithm.
1518
1619 Note that this is very similar to Python's inspect.cleandoc() function.
17- The differences is that the latter also expands tabs to spaces and
20+ The difference is that the latter also expands tabs to spaces and
1821 removes whitespace at the beginning of the first line. Python also has
1922 textwrap.dedent() which uses a completely different algorithm.
2023
2124 For internal use only.
2225 """
23- # Expand a block string's raw value into independent lines.
24- lines = raw_string .splitlines ()
26+ common_indent = maxsize
27+ first_non_empty_line = None
28+ last_non_empty_line = - 1
29+
30+ for i , line in enumerate (lines ):
31+ indent = leading_white_space (line )
32+
33+ if indent == len (line ):
34+ continue # skip empty lines
2535
26- # Remove common indentation from all lines but first.
27- common_indent = get_block_string_indentation (raw_string )
36+ if first_non_empty_line is None :
37+ first_non_empty_line = i
38+ last_non_empty_line = i
2839
29- if common_indent :
30- lines [ 1 :] = [ line [ common_indent :] for line in lines [ 1 :]]
40+ if i and indent < common_indent :
41+ common_indent = indent
3142
32- # Remove leading and trailing blank lines.
33- start_line = 0
34- end_line = len (lines )
35- while start_line < end_line and is_blank (lines [start_line ]):
36- start_line += 1
37- while end_line > start_line and is_blank (lines [end_line - 1 ]):
38- end_line -= 1
43+ if first_non_empty_line is None :
44+ first_non_empty_line = 0
3945
40- # Return a string of the lines joined with U+000A.
41- return "\n " .join (lines [start_line :end_line ])
46+ return [ # Remove common indentation from all lines but first.
47+ line [common_indent :] if i else line for i , line in enumerate (lines )
48+ ][ # Remove leading and trailing blank lines.
49+ first_non_empty_line : last_non_empty_line + 1
50+ ]
4251
4352
44- def is_blank (s : str ) -> bool :
45- """Check whether string contains only space or tab characters."""
46- return all (c == " " or c == "\t " for c in s )
53+ def leading_white_space (s : str ) -> int :
54+ i = 0
55+ for c in s :
56+ if c not in " \t " :
57+ return i
58+ i += 1
59+ return i
4760
4861
49- def get_block_string_indentation (value : str ) -> int :
50- """Get the amount of indentation for the given block string.
62+ def is_printable_as_block_string (value : str ) -> bool :
63+ """Check whether the given string is printable as a block string.
5164
5265 For internal use only.
5366 """
54- is_first_line = is_empty_line = True
55- indent = 0
56- common_indent = None
67+ if not isinstance (value , str ):
68+ value = str (value ) # resolve lazy string proxy object
69+
70+ if not value :
71+ return True # emtpy string is printable
72+
73+ is_empty_line = True
74+ has_indent = False
75+ has_common_indent = True
76+ seen_non_empty_line = False
5777
5878 for c in value :
59- if c in "\r \n " :
60- is_first_line = False
79+ if c == "\n " :
80+ if is_empty_line and not seen_non_empty_line :
81+ return False # has leading new line
82+ seen_non_empty_line = True
6183 is_empty_line = True
62- indent = 0
63- elif c in "\t " :
64- indent += 1
84+ has_indent = False
85+ elif c in " \t " :
86+ has_indent = has_indent or is_empty_line
87+ elif c <= "\x0f " :
88+ return False
6589 else :
66- if (
67- is_empty_line
68- and not is_first_line
69- and (common_indent is None or indent < common_indent )
70- ):
71- common_indent = indent
90+ has_common_indent = has_common_indent and has_indent
7291 is_empty_line = False
7392
74- return common_indent or 0
93+ if is_empty_line :
94+ return False # has trailing empty lines
7595
96+ if has_common_indent and seen_non_empty_line :
97+ return False # has internal indent
7698
77- def print_block_string (value : str , prefer_multiple_lines : bool = False ) -> str :
99+ return True
100+
101+
102+ def print_block_string (value : str , minimize : bool = False ) -> str :
78103 """Print a block string in the indented block form.
79104
80105 Prints a block string in the indented block form by adding a leading and
@@ -86,24 +111,45 @@ def print_block_string(value: str, prefer_multiple_lines: bool = False) -> str:
86111 if not isinstance (value , str ):
87112 value = str (value ) # resolve lazy string proxy object
88113
89- is_single_line = "\n " not in value
90- has_leading_space = value .startswith (" " ) or value .startswith ("\t " )
91- has_trailing_quote = value .endswith ('"' )
114+ escaped_value = value .replace ('"""' , '\\ """' )
115+
116+ # Expand a block string's raw value into independent lines.
117+ lines = escaped_value .splitlines () or ["" ]
118+ num_lines = len (lines )
119+ is_single_line = num_lines == 1
120+
121+ # If common indentation is found,
122+ # we can fix some of those cases by adding a leading new line.
123+ force_leading_new_line = num_lines > 1 and all (
124+ not line or line [0 ] in " \t " for line in lines [1 :]
125+ )
126+
127+ # Trailing triple quotes just looks confusing but doesn't force trailing new line.
128+ has_trailing_triple_quotes = escaped_value .endswith ('\\ """' )
129+
130+ # Trailing quote (single or double) or slash forces trailing new line
131+ has_trailing_quote = value .endswith ('"' ) and not has_trailing_triple_quotes
92132 has_trailing_slash = value .endswith ("\\ " )
93- print_as_multiple_lines = (
133+ force_trailing_new_line = has_trailing_quote or has_trailing_slash
134+
135+ print_as_multiple_lines = not minimize and (
136+ # add leading and trailing new lines only if it improves readability
94137 not is_single_line
95- or has_trailing_quote
96- or has_trailing_slash
97- or prefer_multiple_lines
138+ or len (value ) > 70
139+ or force_trailing_new_line
140+ or force_leading_new_line
141+ or has_trailing_triple_quotes
98142 )
99143
100144 # Format a multi-line block quote to account for leading space.
145+ skip_leading_new_line = is_single_line and value and value [0 ] in " \t "
101146 before = (
102147 "\n "
103- if print_as_multiple_lines and not (is_single_line and has_leading_space )
148+ if print_as_multiple_lines
149+ and not skip_leading_new_line
150+ or force_leading_new_line
104151 else ""
105152 )
106- after = "\n " if print_as_multiple_lines else ""
107- value = value .replace ('"""' , '\\ """' )
153+ after = "\n " if print_as_multiple_lines or force_trailing_new_line else ""
108154
109- return f'"""{ before } { value } { after } """'
155+ return f'"""{ before } { escaped_value } { after } """'
0 commit comments