Skip to content

Commit dd0f93b

Browse files
committed
print_schema: handle descriptions that are non-printable as block strings
Replicates graphql/graphql-js@be12613
1 parent 1b33f57 commit dd0f93b

File tree

11 files changed

+363
-207
lines changed

11 files changed

+363
-207
lines changed

docs/modules/utilities.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ Print a GraphQLSchema to GraphQL Schema language:
4848
.. autofunction:: print_introspection_schema
4949
.. autofunction:: print_schema
5050
.. autofunction:: print_type
51-
.. autofunction:: print_value
5251

5352
Create a GraphQLType from a GraphQL language AST:
5453

src/graphql/language/block_string.py

Lines changed: 100 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,105 @@
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}"""'

src/graphql/language/lexer.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from ..error import GraphQLSyntaxError
44
from .ast import Token
5-
from .block_string import dedent_block_string_value
5+
from .block_string import dedent_block_string_lines
66
from .character_classes import is_digit, is_name_start, is_name_continue
77
from .source import Source
88
from .token_kind import TokenKind
@@ -380,40 +380,49 @@ def read_block_string(self, start: int) -> Token:
380380
"""Read a block string token from the source file."""
381381
body = self.source.body
382382
body_length = len(body)
383-
start_line = self.line
384-
start_column = 1 + start - self.line_start
383+
line_start = self.line_start
385384

386385
position = start + 3
387386
chunk_start = position
388-
raw_value = []
387+
current_line = ""
389388

389+
block_lines = []
390390
while position < body_length:
391391
char = body[position]
392392

393393
if char == '"' and body[position + 1 : position + 3] == '""':
394-
raw_value.append(body[chunk_start:position])
395-
return Token(
394+
current_line += body[chunk_start:position]
395+
block_lines.append(current_line)
396+
397+
token = self.create_token(
396398
TokenKind.BLOCK_STRING,
397399
start,
398400
position + 3,
399-
start_line,
400-
start_column,
401-
dedent_block_string_value("".join(raw_value)),
401+
# return a string of the lines joined with new lines
402+
"\n".join(dedent_block_string_lines(block_lines)),
402403
)
403404

405+
self.line += len(block_lines) - 1
406+
self.line_start = line_start
407+
return token
408+
404409
if char == "\\" and body[position + 1 : position + 4] == '"""':
405-
raw_value.extend((body[chunk_start:position], '"""'))
410+
current_line += body[chunk_start:position]
411+
chunk_start = position + 1 # skip only slash
406412
position += 4
407-
chunk_start = position
408413
continue
409414

410415
if char in "\r\n":
416+
current_line += body[chunk_start:position]
417+
block_lines.append(current_line)
418+
411419
if char == "\r" and body[position + 1 : position + 2] == "\n":
412420
position += 2
413421
else:
414422
position += 1
415-
self.line += 1
416-
self.line_start = position
423+
424+
current_line = ""
425+
chunk_start = line_start = position
417426
continue
418427

419428
if is_unicode_scalar_value(char):

src/graphql/language/print_string.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33

44
def print_string(s: str) -> str:
5-
""" "Print a string as a GraphQL StringValue literal.
5+
"""Print a string as a GraphQL StringValue literal.
66
77
Replaces control characters and excluded characters (" U+0022 and \\ U+005C)
88
with escape sequences.
99
"""
10+
if not isinstance(s, str):
11+
s = str(s)
1012
return f'"{s.translate(escape_sequences)}"'
1113

1214

src/graphql/utilities/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
print_introspection_schema,
3434
print_schema,
3535
print_type,
36-
print_value,
36+
print_value, # deprecated
3737
)
3838

3939
# Create a GraphQLType from a GraphQL language AST.

src/graphql/utilities/print_schema.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, Callable, Dict, List, Optional, Union, cast
22

33
from ..language import print_ast, StringValueNode
4-
from ..language.block_string import print_block_string
4+
from ..language.block_string import is_printable_as_block_string
55
from ..pyutils import inspect
66
from ..type import (
77
DEFAULT_DEPRECATION_REASON,
@@ -282,8 +282,11 @@ def print_description(
282282
if description is None:
283283
return ""
284284

285-
prefer_multiple_lines = len(description) > 70
286-
block_string = print_block_string(description, prefer_multiple_lines)
285+
block_string = print_ast(
286+
StringValueNode(
287+
value=description, block=is_printable_as_block_string(description)
288+
)
289+
)
287290

288291
prefix = "\n" + indentation if indentation and not first_in_block else indentation
289292

src/graphql/utilities/strip_ignored_characters.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
from ..language import Lexer, TokenKind
44
from ..language.source import Source, is_source
5-
from ..language.block_string import (
6-
dedent_block_string_value,
7-
get_block_string_indentation,
8-
)
5+
from ..language.block_string import print_block_string
96
from ..language.lexer import is_punctuator_token_kind
107

8+
__all__ = ["strip_ignored_characters"]
9+
1110

1211
def strip_ignored_characters(source: Union[str, Source]) -> str:
1312
"""Strip characters that are ignored anyway.
@@ -86,25 +85,12 @@ def strip_ignored_characters(source: Union[str, Source]) -> str:
8685

8786
token_body = body[current_token.start : current_token.end]
8887
if token_kind == TokenKind.BLOCK_STRING:
89-
stripped_body += dedent_block_string(token_body)
88+
stripped_body += print_block_string(
89+
current_token.value or "", minimize=True
90+
)
9091
else:
9192
stripped_body += token_body
9293

9394
was_last_added_token_non_punctuator = is_non_punctuator
9495

9596
return stripped_body
96-
97-
98-
def dedent_block_string(block_str: str) -> str:
99-
"""Skip leading and trailing triple quotations"""
100-
raw_str = block_str[3:-3]
101-
body = dedent_block_string_value(raw_str)
102-
103-
if get_block_string_indentation(body) > 0:
104-
body = "\n" + body
105-
106-
has_trailing_quote = body.endswith('"') and not body.endswith('\\"""')
107-
if has_trailing_quote or body.endswith("\\"):
108-
body += "\n"
109-
110-
return '"""' + body + '"""'

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy