Skip to content

Commit fca6eba

Browse files
committed
Extract print_location & print_source_location functions
Replicates graphql/graphql-js@a9a21f3
1 parent 7009b4b commit fca6eba

File tree

12 files changed

+206
-204
lines changed

12 files changed

+206
-204
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ a query language for APIs created by Facebook.
1313
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
1414

1515
The current version 1.0.5 of GraphQL-core-next is up-to-date with GraphQL.js version
16-
14.3.1. All parts of the API are covered by an extensive test suite of currently 1879
16+
14.3.1. All parts of the API are covered by an extensive test suite of currently 1880
1717
unit tests.
1818

1919

docs/modules/error.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ Error
1010

1111
.. autoexception:: GraphQLSyntaxError
1212

13+
.. autofunction:: format_error
1314
.. autofunction:: located_error
1415
.. autofunction:: print_error
15-
.. autofunction:: format_error
1616

1717
.. autodata:: INVALID

docs/modules/language.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Location
7777

7878
.. autofunction:: get_location
7979
.. autoclass:: SourceLocation
80-
80+
.. autofunction:: print_location
8181

8282
Parser
8383
------
@@ -90,6 +90,7 @@ Source
9090
------
9191

9292
.. autoclass:: Source
93+
.. autofunction:: print_source_location
9394

9495
Visitor
9596
-------

graphql/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@
164164
from .language import (
165165
Source,
166166
get_location,
167+
# Print source location
168+
print_location,
169+
print_source_location,
167170
# Lex
168171
Lexer,
169172
TokenKind,
@@ -500,6 +503,8 @@
500503
"GraphQLTypeResolver",
501504
"Source",
502505
"get_location",
506+
"print_location",
507+
"print_source_location",
503508
"Lexer",
504509
"TokenKind",
505510
"parse",

graphql/error/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44
errors.
55
"""
66

7-
from .graphql_error import GraphQLError
7+
from .graphql_error import GraphQLError, print_error
88

99
from .syntax_error import GraphQLSyntaxError
1010

1111
from .located_error import located_error
1212

13-
from .print_error import print_error
14-
1513
from .format_error import format_error
1614

1715
from .invalid import INVALID, InvalidType
@@ -22,6 +20,6 @@
2220
"GraphQLError",
2321
"GraphQLSyntaxError",
2422
"format_error",
25-
"print_error",
2623
"located_error",
24+
"print_error",
2725
]

graphql/error/graphql_error.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
from typing import Any, Dict, List, Optional, Sequence, Union, TYPE_CHECKING
33

44
from .format_error import format_error
5-
from .print_error import print_error
65

76
if TYPE_CHECKING: # pragma: no cover
87
from ..language.ast import Node # noqa: F401
98
from ..language.location import SourceLocation # noqa: F401
109
from ..language.source import Source # noqa: F401
1110

12-
__all__ = ["GraphQLError"]
11+
__all__ = ["GraphQLError", "print_error"]
1312

1413

1514
class GraphQLError(Exception):
@@ -176,3 +175,25 @@ def __ne__(self, other):
176175
def formatted(self):
177176
"""Get error formatted according to the specification."""
178177
return format_error(self)
178+
179+
180+
def print_error(error: GraphQLError) -> str:
181+
"""Print a GraphQLError to a string.
182+
183+
Represents useful location information about the error's position in the source.
184+
"""
185+
# Lazy import to avoid a cyclic dependency between error and language
186+
from ..language.print_location import print_location, print_source_location
187+
188+
output = [error.message]
189+
190+
if error.nodes:
191+
for node in error.nodes:
192+
if node.loc:
193+
output.append(print_location(node.loc))
194+
elif error.source and error.locations:
195+
source = error.source
196+
for location in error.locations:
197+
output.append(print_source_location(source, location))
198+
199+
return "\n\n".join(output)

graphql/error/print_error.py

Lines changed: 0 additions & 81 deletions
This file was deleted.

graphql/language/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from .location import get_location, SourceLocation
1010

11+
from .print_location import print_location, print_source_location
12+
1113
from .token_kind import TokenKind
1214

1315
from .lexer import Lexer
@@ -102,6 +104,8 @@
102104
__all__ = [
103105
"get_location",
104106
"SourceLocation",
107+
"print_location",
108+
"print_source_location",
105109
"TokenKind",
106110
"Lexer",
107111
"parse",

graphql/language/print_location.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import re
2+
from functools import reduce
3+
from typing import List, Optional, Tuple
4+
5+
from .ast import Location
6+
from .location import SourceLocation, get_location
7+
from .source import Source
8+
9+
10+
__all__ = ["print_location", "print_source_location"]
11+
12+
13+
def print_location(location: Location) -> str:
14+
"""Render a helpful description of the location in the GraphQL Source document."""
15+
return print_source_location(
16+
location.source, get_location(location.source, location.start)
17+
)
18+
19+
20+
_re_newline = re.compile(r"\r\n|[\n\r]")
21+
22+
23+
def print_source_location(source: Source, source_location: SourceLocation) -> str:
24+
"""Render a helpful description of the location in the GraphQL Source document."""
25+
first_line_column_offset = source.location_offset.column - 1
26+
body = " " * first_line_column_offset + source.body
27+
28+
line_index = source_location.line - 1
29+
line_offset = source.location_offset.line - 1
30+
line_num = source_location.line + line_offset
31+
32+
column_offset = first_line_column_offset if source_location.line == 1 else 0
33+
column_num = source_location.column + column_offset
34+
35+
lines = _re_newline.split(body) # works a bit different from splitlines()
36+
len_lines = len(lines)
37+
38+
def get_line(index: int) -> Optional[str]:
39+
return lines[index] if 0 <= index < len_lines else None
40+
41+
return f"{source.name}:{line_num}:{column_num}\n" + print_prefixed_lines(
42+
[
43+
(f"{line_num - 1}: ", get_line(line_index - 1)),
44+
(f"{line_num}: ", get_line(line_index)),
45+
("", " " * (column_num - 1) + "^"),
46+
(f"{line_num + 1}: ", get_line(line_index + 1)),
47+
]
48+
)
49+
50+
51+
def print_prefixed_lines(lines: List[Tuple[str, Optional[str]]]) -> str:
52+
"""Print lines specified like this: ["prefix", "string"]"""
53+
existing_lines = [line for line in lines if line[1] is not None]
54+
pad_len = reduce(lambda pad, line: max(pad, len(line[0])), existing_lines, 0)
55+
return "\n".join(
56+
map(
57+
lambda line: line[0].rjust(pad_len) + line[1], existing_lines # type:ignore
58+
)
59+
)

tests/error/test_graphql_error.py

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from typing import cast
22

3-
from graphql.error import GraphQLError, format_error
4-
from graphql.language import parse, OperationDefinitionNode, Source
3+
from graphql.error import GraphQLError, format_error, print_error
4+
from graphql.language import (
5+
parse,
6+
OperationDefinitionNode,
7+
ObjectTypeDefinitionNode,
8+
Source,
9+
)
510
from graphql.pyutils import dedent
611

712

@@ -134,3 +139,74 @@ def hashes_are_unique_per_instance():
134139
e1 = GraphQLError("msg")
135140
e2 = GraphQLError("msg")
136141
assert hash(e1) != hash(e2)
142+
143+
144+
def describe_print_error():
145+
def prints_an_error_without_location():
146+
error = GraphQLError("Error without location")
147+
assert print_error(error) == "Error without location"
148+
149+
def prints_an_error_using_node_without_location():
150+
error = GraphQLError(
151+
"Error attached to node without location",
152+
parse("{ foo }", no_location=True),
153+
)
154+
assert print_error(error) == "Error attached to node without location"
155+
156+
def prints_an_error_with_nodes_from_different_sources():
157+
doc_a = parse(
158+
Source(
159+
dedent(
160+
"""
161+
type Foo {
162+
field: String
163+
}
164+
"""
165+
),
166+
"SourceA",
167+
)
168+
)
169+
op_a = doc_a.definitions[0]
170+
op_a = cast(ObjectTypeDefinitionNode, op_a)
171+
assert op_a and op_a.kind == "object_type_definition" and op_a.fields
172+
field_a = op_a.fields[0]
173+
doc_b = parse(
174+
Source(
175+
dedent(
176+
"""
177+
type Foo {
178+
field: Int
179+
}
180+
"""
181+
),
182+
"SourceB",
183+
)
184+
)
185+
op_b = doc_b.definitions[0]
186+
op_b = cast(ObjectTypeDefinitionNode, op_b)
187+
assert op_b and op_b.kind == "object_type_definition" and op_b.fields
188+
field_b = op_b.fields[0]
189+
190+
error = GraphQLError(
191+
"Example error with two nodes", [field_a.type, field_b.type]
192+
)
193+
194+
printed_error = print_error(error)
195+
assert printed_error + "\n" == dedent(
196+
"""
197+
Example error with two nodes
198+
199+
SourceA:2:10
200+
1: type Foo {
201+
2: field: String
202+
^
203+
3: }
204+
205+
SourceB:2:10
206+
1: type Foo {
207+
2: field: Int
208+
^
209+
3: }
210+
"""
211+
)
212+
assert str(error) == printed_error

0 commit comments

Comments
 (0)