Skip to content

Commit bf60bed

Browse files
authored
👌 IMPROVE: Parsing MalformedError messages (#23)
1 parent fdfac04 commit bf60bed

File tree

10 files changed

+72
-38
lines changed

10 files changed

+72
-38
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ To see all options:
276276
$ sphinx-etoc --help
277277
Usage: sphinx-etoc [OPTIONS] COMMAND [ARGS]...
278278
279-
Command-line for ``sphinx-external-toc``.
279+
Command-line for sphinx-external-toc.
280280
281281
Options:
282282
--version Show the version and exit.
@@ -285,7 +285,7 @@ Options:
285285
Commands:
286286
from-site Create a ToC file from a site directory.
287287
migrate Migrate a ToC from a previous revision.
288-
parse-toc Parse a ToC file to a site-map YAML.
288+
parse Parse a ToC file to a site-map YAML.
289289
to-site Create a site directory from a ToC file.
290290
```
291291

sphinx_external_toc/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
1616
@click.version_option(version=__version__)
1717
def main():
18-
"""Command-line for ``sphinx-external-toc``."""
18+
"""Command-line for sphinx-external-toc."""
1919

2020

21-
@main.command("parse-toc")
21+
@main.command("parse")
2222
@click.argument("toc_file", type=click.Path(exists=True, file_okay=True))
2323
def parse_toc(toc_file):
2424
"""Parse a ToC file to a site-map YAML."""

sphinx_external_toc/parsing.py

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
DEFAULT_SUBTREES_KEY = "subtrees"
1212
DEFAULT_ITEMS_KEY = "items"
1313
FILE_FORMAT_KEY = "format"
14+
ROOT_KEY = "root"
1415
FILE_KEY = "file"
1516
GLOB_KEY = "glob"
1617
URL_KEY = "url"
@@ -97,7 +98,7 @@ def parse_toc_data(data: Dict[str, Any]) -> SiteMap:
9798
defaults: Dict[str, Any] = {**file_format.toc_defaults, **data.get("defaults", {})}
9899

99100
doc_item, docs_list = _parse_doc_item(
100-
data, defaults, "/", depth=0, file_key="root", file_format=file_format
101+
data, defaults, "/", depth=0, is_root=True, file_format=file_format
101102
)
102103

103104
site_map = SiteMap(
@@ -118,11 +119,12 @@ def _parse_doc_item(
118119
*,
119120
depth: int,
120121
file_format: FileFormat,
121-
file_key: str = FILE_KEY,
122-
) -> Tuple[Document, Sequence[Dict[str, Any]]]:
122+
is_root: bool = False,
123+
) -> Tuple[Document, Sequence[Tuple[str, Dict[str, Any]]]]:
123124
"""Parse a single doc item."""
125+
file_key = ROOT_KEY if is_root else FILE_KEY
124126
if file_key not in data:
125-
raise MalformedError(f"'{file_key}' key not found: '{path}'")
127+
raise MalformedError(f"'{file_key}' key not found @ '{path}'")
126128

127129
subtrees_key = file_format.get_subtrees_key(depth)
128130
items_key = file_format.get_items_key(depth)
@@ -142,20 +144,23 @@ def _parse_doc_item(
142144
if not allowed_keys.issuperset(data.keys()):
143145
unknown_keys = set(data.keys()).difference(allowed_keys)
144146
raise MalformedError(
145-
f"Unknown keys found: {unknown_keys!r}, allowed: {allowed_keys!r}: '{path}'"
147+
f"Unknown keys found: {unknown_keys!r}, allowed: {allowed_keys!r} @ '{path}'"
146148
)
147149

150+
shorthand_used = False
148151
if items_key in data:
149152
# this is a shorthand for defining a single subtree
150153
if subtrees_key in data:
151154
raise MalformedError(
152-
f"Both '{subtrees_key}' and '{items_key}' found: '{path}'"
155+
f"Both '{subtrees_key}' and '{items_key}' found @ '{path}'"
153156
)
154157
subtrees_data = [{items_key: data[items_key], **data.get("options", {})}]
158+
shorthand_used = True
155159
elif subtrees_key in data:
156160
subtrees_data = data[subtrees_key]
157161
if not (isinstance(subtrees_data, Sequence) and subtrees_data):
158-
raise MalformedError(f"'{subtrees_key}' not a non-empty list: '{path}'")
162+
raise MalformedError(f"'{subtrees_key}' not a non-empty list @ '{path}'")
163+
path = f"{path}{subtrees_key}/"
159164
else:
160165
subtrees_data = []
161166

@@ -164,46 +169,46 @@ def _parse_doc_item(
164169
toctrees = []
165170
for toc_idx, toc_data in enumerate(subtrees_data):
166171

172+
toc_path = path if shorthand_used else f"{path}{toc_idx}/"
173+
167174
if not (isinstance(toc_data, Mapping) and items_key in toc_data):
168175
raise MalformedError(
169-
f"subtree not a mapping containing '{items_key}' key: '{path}{toc_idx}'"
176+
f"item not a mapping containing '{items_key}' key @ '{toc_path}'"
170177
)
171178

172179
items_data = toc_data[items_key]
173180

174181
if not (isinstance(items_data, Sequence) and items_data):
175-
raise MalformedError(
176-
f"'{items_key}' not a non-empty list: '{path}{toc_idx}'"
177-
)
182+
raise MalformedError(f"'{items_key}' not a non-empty list @ '{toc_path}'")
178183

179184
# generate items list
180185
items: List[Union[GlobItem, FileItem, UrlItem]] = []
181186
for item_idx, item_data in enumerate(items_data):
182187

183188
if not isinstance(item_data, Mapping):
184189
raise MalformedError(
185-
f"'{items_key}' item not a mapping type: '{path}{toc_idx}/{item_idx}'"
190+
f"item not a mapping type @ '{toc_path}{items_key}/{item_idx}'"
186191
)
187192

188193
link_keys = _known_link_keys.intersection(item_data)
189194

190195
# validation checks
191196
if not link_keys:
192197
raise MalformedError(
193-
f"'{items_key}' item does not contain one of "
194-
f"{_known_link_keys!r}: '{path}{toc_idx}/{item_idx}'"
198+
f"item does not contain one of "
199+
f"{_known_link_keys!r} @ '{toc_path}{items_key}/{item_idx}'"
195200
)
196201
if not len(link_keys) == 1:
197202
raise MalformedError(
198-
f"'{items_key}' item contains incompatible keys "
199-
f"{link_keys!r}: {path}{toc_idx}/{item_idx}"
203+
f"item contains incompatible keys "
204+
f"{link_keys!r} @ '{toc_path}{items_key}/{item_idx}'"
200205
)
201206
for item_key in (GLOB_KEY, URL_KEY):
202207
for other_key in (subtrees_key, items_key):
203208
if link_keys == {item_key} and other_key in item_data:
204209
raise MalformedError(
205-
f"'{items_key}' item contains incompatible keys "
206-
f"'{item_key}' and '{other_key}': {path}{toc_idx}/{item_idx}"
210+
f"item contains incompatible keys "
211+
f"'{item_key}' and '{other_key}' @ '{toc_path}{items_key}/{item_idx}'"
207212
)
208213

209214
if link_keys == {FILE_KEY}:
@@ -222,7 +227,7 @@ def _parse_doc_item(
222227
try:
223228
toc_item = TocTree(items=items, **keywords)
224229
except TypeError as exc:
225-
raise MalformedError(f"toctree validation: {path}{toc_idx}") from exc
230+
raise MalformedError(f"toctree validation @ '{toc_path}'") from exc
226231
toctrees.append(toc_item)
227232

228233
try:
@@ -232,21 +237,27 @@ def _parse_doc_item(
232237
except TypeError as exc:
233238
raise MalformedError(f"doc validation: {path}") from exc
234239

235-
docs_data = [
236-
item_data
237-
for toc_data in subtrees_data
238-
for item_data in toc_data[items_key]
240+
# list of docs that need to be parsed recursively (and path)
241+
docs_to_be_parsed_list = [
242+
(
243+
f"{path}/{items_key}/{ii}/"
244+
if shorthand_used
245+
else f"{path}{ti}/{items_key}/{ii}/",
246+
item_data,
247+
)
248+
for ti, toc_data in enumerate(subtrees_data)
249+
for ii, item_data in enumerate(toc_data[items_key])
239250
if FILE_KEY in item_data
240251
]
241252

242253
return (
243254
doc_item,
244-
docs_data,
255+
docs_to_be_parsed_list,
245256
)
246257

247258

248259
def _parse_docs_list(
249-
docs_list: Sequence[Dict[str, Any]],
260+
docs_list: Sequence[Tuple[str, Dict[str, Any]]],
250261
site_map: SiteMap,
251262
defaults: Dict[str, Any],
252263
path: str,
@@ -255,11 +266,10 @@ def _parse_docs_list(
255266
file_format: FileFormat,
256267
):
257268
"""Parse a list of docs."""
258-
for doc_data in docs_list:
259-
docname = doc_data["file"]
269+
for child_path, doc_data in docs_list:
270+
docname = doc_data[FILE_KEY]
260271
if docname in site_map:
261-
raise MalformedError(f"document file used multiple times: {docname}")
262-
child_path = f"{path}{docname}/"
272+
raise MalformedError(f"document file used multiple times: '{docname}'")
263273
child_item, child_docs_list = _parse_doc_item(
264274
doc_data, defaults, child_path, depth=depth, file_format=file_format
265275
)
@@ -280,13 +290,13 @@ def create_toc_dict(site_map: SiteMap, *, skip_defaults: bool = True) -> Dict[st
280290
try:
281291
file_format = FILE_FORMATS[site_map.file_format or "default"]
282292
except KeyError:
283-
raise KeyError(f"File format not recognised: '{site_map.file_format}'")
293+
raise KeyError(f"File format not recognised @ '{site_map.file_format}'")
284294
data = _docitem_to_dict(
285295
site_map.root,
286296
site_map,
287297
depth=0,
288298
skip_defaults=skip_defaults,
289-
file_key="root",
299+
is_root=True,
290300
file_format=file_format,
291301
)
292302
if site_map.meta:
@@ -304,14 +314,15 @@ def _docitem_to_dict(
304314
depth: int,
305315
file_format: FileFormat,
306316
skip_defaults: bool = True,
307-
file_key: str = FILE_KEY,
317+
is_root: bool = False,
308318
parsed_docnames: Optional[Set[str]] = None,
309319
) -> Dict[str, Any]:
310320
"""
311321
312322
:param skip_defaults: do not add key/values for values that are already the default
313323
314324
"""
325+
file_key = ROOT_KEY if is_root else FILE_KEY
315326
subtrees_key = file_format.get_subtrees_key(depth)
316327
items_key = file_format.get_items_key(depth)
317328

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
root: main
2+
subtrees:
3+
- file: doc1
4+
items:
5+
- file: doc2
6+
- file: doc3
7+
unknown: 1

tests/test_parsing.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from sphinx_external_toc.parsing import MalformedError, create_toc_dict, parse_toc_yaml
66

77
TOC_FILES = list(Path(__file__).parent.joinpath("_toc_files").glob("*.yml"))
8-
TOC_FILES_BAD = list(Path(__file__).parent.joinpath("_bad_toc_files").glob("*.yml"))
98

109

1110
@pytest.mark.parametrize(
@@ -25,9 +24,26 @@ def test_create_toc_dict(path: Path, data_regression):
2524
data_regression.check(data)
2625

2726

27+
TOC_FILES_BAD = list(Path(__file__).parent.joinpath("_bad_toc_files").glob("*.yml"))
28+
ERROR_MESSAGES = {
29+
"empty.yml": "toc is not a mapping:",
30+
"file_and_glob_present.yml": "item contains incompatible keys .* @ '/items/0'",
31+
"list.yml": "toc is not a mapping:",
32+
"unknown_keys.yml": "Unknown keys found: .* @ '/'",
33+
"empty_items.yml": "'items' not a non-empty list @ '/'",
34+
"items_in_glob.yml": "item contains incompatible keys 'glob' and 'items' @ '/items/0'",
35+
"no_root.yml": "'root' key not found @ '/'",
36+
"unknown_keys_nested.yml": "Unknown keys found: {'unknown'}, allow.* @ '/subtrees/0/items/1/'",
37+
"empty_subtrees.yml": "'subtrees' not a non-empty list @ '/'",
38+
"items_in_url.yml": "item contains incompatible keys 'url' and 'items' @ '/items/0'",
39+
"subtree_with_no_items.yml": "item not a mapping containing 'items' key @ '/subtrees/0/'",
40+
}
41+
42+
2843
@pytest.mark.parametrize(
2944
"path", TOC_FILES_BAD, ids=[path.name.rsplit(".", 1)[0] for path in TOC_FILES_BAD]
3045
)
3146
def test_malformed_file_parse(path: Path):
32-
with pytest.raises(MalformedError):
47+
message = ERROR_MESSAGES[path.name]
48+
with pytest.raises(MalformedError, match=message):
3349
parse_toc_yaml(path)

0 commit comments

Comments
 (0)