Skip to content

Commit c3a21e0

Browse files
authored
Merge pull request #189 from machow/yaml-validation
[WIP] Yaml validation
2 parents 155fc72 + f34e0d9 commit c3a21e0

File tree

3 files changed

+102
-1
lines changed

3 files changed

+102
-1
lines changed

quartodoc/autosummary.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
from plum import dispatch # noqa
1515
from pathlib import Path
1616
from types import ModuleType
17+
from pydantic import ValidationError
1718

1819
from .inventory import create_inventory, convert_inventory
1920
from . import layout
2021
from .renderers import Renderer
22+
from .validation import fmt
2123

2224
from typing import Any
2325

@@ -446,7 +448,14 @@ def load_layout(self, sections: dict, package: str):
446448
# TODO: currently returning the list of sections, to make work with
447449
# previous code. We should make Layout a first-class citizen of the
448450
# process.
449-
return layout.Layout(sections=sections, package=package)
451+
try:
452+
return layout.Layout(sections=sections, package=package)
453+
except ValidationError as e:
454+
msg = 'Configuration error for YAML:\n - '
455+
errors = [fmt(err) for err in e.errors() if fmt(err)]
456+
first_error = errors[0] # we only want to show one error at a time b/c it is confusing otherwise
457+
msg += first_error
458+
raise ValueError(msg) from None
450459

451460
# building ----------------------------------------------------------------
452461

quartodoc/tests/test_validation.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import pytest, copy
2+
from quartodoc.autosummary import Builder
3+
4+
EXAMPLE_SECTIONS = [
5+
{'title': 'Preperation Functions',
6+
'desc': 'Functions that fetch objects.\nThey prepare a representation of the site.\n',
7+
'contents': ['Auto', 'blueprint', 'collect', 'get_object', 'preview']},
8+
{'title': 'Docstring Renderers',
9+
'desc': 'Renderers convert parsed docstrings into a target format, like markdown.\n',
10+
'contents': [{'name': 'MdRenderer', 'children': 'linked'},
11+
{'name': 'MdRenderer.render', 'dynamic': True},
12+
{'name': 'MdRenderer.render_annotation', 'dynamic': True},
13+
{'name': 'MdRenderer.render_header', 'dynamic': True}]
14+
},
15+
{'title': 'API Builders',
16+
'desc': 'Builders build documentation. They tie all the pieces\nof quartodoc together.\n',
17+
'contents': [{'kind': 'auto', 'name': 'Builder', 'members': []},
18+
'Builder.from_quarto_config', 'Builder.build', 'Builder.write_index']
19+
},
20+
]
21+
22+
@pytest.fixture
23+
def sections():
24+
return copy.deepcopy(EXAMPLE_SECTIONS)
25+
26+
def check_ValueError(sections):
27+
"Check that a ValueError is raised when creating a `Builder` instance. Return the error message as a string."
28+
with pytest.raises(ValueError) as e:
29+
Builder(sections=sections, package='quartodoc')
30+
return str(e.value)
31+
32+
def test_valid_yaml(sections):
33+
"Test that valid YAML passes validation"
34+
Builder(sections=sections, package='quartodoc')
35+
36+
def test_missing_title(sections):
37+
"Test that missing title raises an error"
38+
del sections[0]['title']
39+
msg = check_ValueError(sections)
40+
assert '- Missing field `title` for element 0 in the list for `sections`' in msg
41+
42+
def test_missing_desc(sections):
43+
"Test that a missing description raises an error"
44+
sections = copy.deepcopy(EXAMPLE_SECTIONS)
45+
del sections[2]['desc']
46+
msg = check_ValueError(sections)
47+
assert '- Missing field `desc` for element 2 in the list for `sections`' in msg
48+
49+
def test_missing_name_contents_1(sections):
50+
"Test that a missing name in contents raises an error"
51+
del sections[2]['contents'][0]['name']
52+
msg = check_ValueError(sections)
53+
assert '- Missing field `name` for element 0 in the list for `contents` located in element 2 in the list for `sections`' in msg
54+
55+
def test_missing_name_contents_2(sections):
56+
"Test that a missing name in contents raises an error in a different section."
57+
del sections[1]['contents'][0]['name']
58+
msg = check_ValueError(sections)
59+
assert '- Missing field `name` for element 0 in the list for `contents` located in element 1 in the list for `sections`' in msg
60+
61+
def test_misplaced_kindpage(sections):
62+
"Test that a misplaced kind: page raises an error"
63+
sections[0]['kind'] = 'page'
64+
msg = check_ValueError(sections)
65+
assert ' - Missing field `path` for element 0 in the list for `sections`, which you need when setting `kind: page`.' in msg

quartodoc/validation.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
def fmt(err:dict):
3+
"format error messages from pydantic."
4+
msg = ""
5+
if err['msg'].startswith('Discriminator'):
6+
return msg
7+
if err['type'] == 'value_error.missing':
8+
msg += 'Missing field'
9+
else:
10+
msg += err['msg'] + ':'
11+
12+
if 'loc' in err:
13+
if len(err['loc']) == 1:
14+
msg += f" from root level: `{err['loc'][0]}`"
15+
elif len(err['loc']) == 3:
16+
msg += f" `{err['loc'][2]}` for element {err['loc'][1]} in the list for `{err['loc'][0]}`"
17+
elif len(err['loc']) == 4 and err['loc'][2] == 'Page':
18+
msg += f" `{err['loc'][3]}` for element {err['loc'][1]} in the list for `{err['loc'][0]}`, which you need when setting `kind: page`."
19+
elif len(err['loc']) == 5:
20+
msg += f" `{err['loc'][4]}` for element {err['loc'][3]} in the list for `{err['loc'][2]}` located in element {err['loc'][1]} in the list for `{err['loc'][0]}`"
21+
elif len(err['loc']) == 6 and err['loc'][4] == 'Auto':
22+
msg += f" `{err['loc'][5]}` for element {err['loc'][3]} in the list for `{err['loc'][2]}` located in element {err['loc'][1]} in the list for `{err['loc'][0]}`"
23+
else:
24+
return str(err) # so we can debug and include more cases
25+
else:
26+
msg += str(err)
27+
return msg

0 commit comments

Comments
 (0)