Skip to content

Commit 58e7ea5

Browse files
committed
DOP-2637: Properly report diagnostics on YAML parsing and unmarshaling errors
1 parent d21f824 commit 58e7ea5

File tree

3 files changed

+127
-12
lines changed

3 files changed

+127
-12
lines changed

snooty/gizaparser/parse.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,13 @@ def parse(
6464
diagnostics.append(parse_diagnostic)
6565
return ([], text, diagnostics)
6666

67-
try:
68-
parsed = [check_type(ty, data) for data in parsed_yaml]
69-
return parsed, text, diagnostics
70-
except LoadError as err:
71-
mapping = err.bad_data if isinstance(err.bad_data, dict) else {}
72-
lineno = mapping._start_line if isinstance(mapping, mapping_dict) else 0
73-
return [], text, diagnostics + [UnmarshallingError(str(err), lineno)]
67+
parsed: List[_T] = []
68+
for data in parsed_yaml:
69+
try:
70+
parsed.append(check_type(ty, data))
71+
except LoadError as err:
72+
mapping = err.bad_data if isinstance(err.bad_data, dict) else {}
73+
lineno = mapping._start_line if isinstance(mapping, mapping_dict) else 0
74+
diagnostics.append(UnmarshallingError(str(err), lineno))
75+
76+
return parsed, text, diagnostics

snooty/gizaparser/test_extracts.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from pathlib import Path, PurePath
22
from typing import Dict, List, Optional, Tuple
33

4-
from ..diagnostics import Diagnostic, FailedToInheritRef
4+
from ..diagnostics import (
5+
Diagnostic,
6+
ErrorParsingYAMLFile,
7+
FailedToInheritRef,
8+
UnmarshallingError,
9+
)
510
from ..page import Page
611
from ..parser import EmbeddedRstParser
712
from ..types import FileId, ProjectConfig
@@ -140,3 +145,77 @@ def test_external_cycle() -> None:
140145
FileId("includes/extracts-test2.yaml"): [FailedToInheritRef],
141146
FileId("includes/extracts-test1.yaml"): [FailedToInheritRef],
142147
}
148+
149+
150+
def test_partial_unmarshaling_error() -> None:
151+
with make_test(
152+
{
153+
Path(
154+
"source/includes/extracts-test1.yaml"
155+
): """
156+
ref: bypassDocumentValidation-db.collection.aggregate
157+
inherit:
158+
ref: _bypassDocValidation
159+
file: extracts-bypassDocumentValidation-base.yaml
160+
replacement:
161+
role: ":method:`db.collection.aggregate()`"
162+
interface: "method"
163+
post: |
164+
Document validation only occurs if you are using the
165+
:pipeline:`$out` operator in your aggregation operation.
166+
---
167+
ref: bypassDocumentValidation-aggregate
168+
content: "Okay"
169+
...
170+
"""
171+
}
172+
) as result:
173+
assert list(result.pages.keys()) == [
174+
FileId("includes/extracts/bypassDocumentValidation-aggregate.rst")
175+
]
176+
assert {k: [type(d) for d in v] for k, v in result.diagnostics.items()} == {
177+
FileId("includes/extracts-test1.yaml"): [UnmarshallingError]
178+
}
179+
180+
181+
def test_single_unmarshaling_error() -> None:
182+
with make_test(
183+
{
184+
Path(
185+
"source/includes/extracts-test1.yaml"
186+
): """
187+
ref: bypassDocumentValidation-db.collection.aggregate
188+
inherit:
189+
ref: _bypassDocValidation
190+
file: extracts-bypassDocumentValidation-base.yaml
191+
replacement:
192+
role: ":method:`db.collection.aggregate()`"
193+
interface: "method"
194+
post: |
195+
Document validation only occurs if you are using the
196+
:pipeline:`$out` operator in your aggregation operation.
197+
...
198+
"""
199+
}
200+
) as result:
201+
assert list(result.pages.keys()) == []
202+
assert {k: [type(d) for d in v] for k, v in result.diagnostics.items()} == {
203+
FileId("includes/extracts-test1.yaml"): [UnmarshallingError]
204+
}
205+
206+
207+
def test_parse_error() -> None:
208+
with make_test(
209+
{
210+
Path(
211+
"source/includes/extracts-test1.yaml"
212+
): """
213+
ref: bypassDocumentValidation
214+
content: "not okay
215+
"""
216+
}
217+
) as result:
218+
assert list(result.pages.keys()) == []
219+
assert {k: [type(d) for d in v] for k, v in result.diagnostics.items()} == {
220+
FileId("includes/extracts-test1.yaml"): [ErrorParsingYAMLFile]
221+
}

snooty/parser.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,7 @@ def __init__(self, postprocessor_factory: Callable[[], Postprocessor]) -> None:
11051105

11061106
self.postprocessor_factory = postprocessor_factory
11071107
self._parsed: Dict[FileId, Tuple[Page, FileId, List[Diagnostic]]] = {}
1108+
self._orphan_diagnostics: Dict[FileId, List[Diagnostic]] = {}
11081109
self.__cached = PostprocessorResult({}, {}, {}, TargetDatabase())
11091110
self.__changed_pages: Set[FileId] = set()
11101111

@@ -1137,6 +1138,13 @@ def start(
11371138
Postprocessor, PostprocessorResult
11381139
] = util.WorkerLauncher("postprocessor", start)
11391140

1141+
def set_orphan_diagnostics(self, key: FileId, value: List[Diagnostic]) -> None:
1142+
"""Some diagnostics can't be associated with a parsed Page because of underlying
1143+
problems like invalid YAML syntax. These are orphan diagnostics, and we need
1144+
to track them too."""
1145+
with self._lock:
1146+
self._orphan_diagnostics[key] = value
1147+
11401148
def __setitem__(
11411149
self, key: FileId, value: Tuple[Page, FileId, List[Diagnostic]]
11421150
) -> None:
@@ -1164,7 +1172,15 @@ def __contains__(self, key: FileId) -> bool:
11641172

11651173
def __delitem__(self, key: FileId) -> None:
11661174
with self._lock:
1167-
del self._parsed[key]
1175+
try:
1176+
del self._parsed[key]
1177+
except KeyError:
1178+
pass
1179+
1180+
try:
1181+
del self._orphan_diagnostics[key]
1182+
except KeyError:
1183+
pass
11681184

11691185
def merge_diagnostics(
11701186
self, *others: Dict[FileId, List[Diagnostic]]
@@ -1174,6 +1190,12 @@ def merge_diagnostics(
11741190
v[1]: list(v[2]) for v in self._parsed.values()
11751191
}
11761192

1193+
for key, diagnostics in self._orphan_diagnostics.items():
1194+
if key in result:
1195+
result[key].extend(diagnostics)
1196+
else:
1197+
result[key] = list(diagnostics)
1198+
11771199
all_keys: Set[FileId] = set()
11781200
for other in others:
11791201
all_keys.update(other.keys())
@@ -1501,11 +1523,13 @@ def build(
15011523
for prefix, giza_category in self.yaml_mapping.items():
15021524
logger.debug("Parsing %s YAML", prefix)
15031525
for path in categorized[prefix]:
1504-
steps, text, diagnostics = giza_category.parse(path)
1505-
all_yaml_diagnostics[path] = diagnostics
1506-
giza_category.add(path, text, steps)
1526+
artifacts, text, diagnostics = giza_category.parse(path)
1527+
if diagnostics:
1528+
all_yaml_diagnostics[path] = diagnostics
1529+
giza_category.add(path, text, artifacts)
15071530

15081531
# Now that all of our YAML files are loaded, generate a page for each one
1532+
seen_paths: Set[Path] = set()
15091533
for prefix, giza_category in self.yaml_mapping.items():
15101534
logger.debug("Processing %s YAML: %d nodes", prefix, len(giza_category))
15111535
for file_id, giza_node in giza_category.reify_all_files(
@@ -1531,10 +1555,19 @@ def create_page(filename: str) -> Tuple[Page, EmbeddedRstParser]:
15311555
for page in giza_category.to_pages(
15321556
giza_node.path, create_page, giza_node.data
15331557
):
1558+
seen_paths.add(page.source_path)
15341559
self._page_updated(
15351560
page, all_yaml_diagnostics.get(page.source_path, [])
15361561
)
15371562

1563+
# Handle parsing and unmarshaling errors that lead to diagnostics not associated with
1564+
# any page.
1565+
for key in all_yaml_diagnostics:
1566+
if key not in seen_paths:
1567+
self.pages.set_orphan_diagnostics(
1568+
self.config.get_fileid(key), all_yaml_diagnostics[key]
1569+
)
1570+
15381571
if postprocess:
15391572
postprocessor_result = self.postprocess()
15401573

0 commit comments

Comments
 (0)