1
+ from typing import Collection , List
2
+ from sys import maxsize
3
+
1
4
__all__ = [
2
- "dedent_block_string_value" ,
5
+ "dedent_block_string_lines" ,
6
+ "is_printable_as_block_string" ,
3
7
"print_block_string" ,
4
- "get_block_string_indentation" ,
5
8
]
6
9
7
10
8
- def dedent_block_string_value ( raw_string : str ) -> str :
11
+ def dedent_block_string_lines ( lines : Collection [ str ] ) -> List [ str ] :
9
12
"""Produce the value of a block string from its parsed raw value.
10
13
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.
13
16
14
- This implements the GraphQL spec's BlockStringValue() static algorithm.
17
+ It implements the GraphQL spec's BlockStringValue() static algorithm.
15
18
16
19
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
18
21
removes whitespace at the beginning of the first line. Python also has
19
22
textwrap.dedent() which uses a completely different algorithm.
20
23
21
24
For internal use only.
22
25
"""
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
25
35
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
28
39
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
31
42
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
39
45
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
+ ]
42
51
43
52
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
47
60
48
61
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.
51
64
52
65
For internal use only.
53
66
"""
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
57
77
58
78
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
61
83
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
65
89
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
72
91
is_empty_line = False
73
92
74
- return common_indent or 0
93
+ if is_empty_line :
94
+ return False # has trailing empty lines
75
95
96
+ if has_common_indent and seen_non_empty_line :
97
+ return False # has internal indent
76
98
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 :
78
103
"""Print a block string in the indented block form.
79
104
80
105
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:
86
111
if not isinstance (value , str ):
87
112
value = str (value ) # resolve lazy string proxy object
88
113
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
92
132
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
94
137
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
98
142
)
99
143
100
144
# 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 "
101
146
before = (
102
147
"\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
104
151
else ""
105
152
)
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 ""
108
154
109
- return f'"""{ before } { value } { after } """'
155
+ return f'"""{ before } { escaped_value } { after } """'
0 commit comments