Skip to content

Commit 0d1205e

Browse files
authored
👌 IMPROVE: logger warnings (and tests) (#8)
1 parent eb6f834 commit 0d1205e

File tree

12 files changed

+155
-42
lines changed

12 files changed

+155
-42
lines changed

README.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ In normal Sphinx documentation, the documentation structure is defined *via* a b
1010

1111
This extension facilitates a **top-down** approach to defining the Table of Contents (ToC) structure, within a single file that is external to the documentation.
1212

13-
The path to the toc file can be defined with `external_toc_path` (default: `_toc.yml`).
14-
1513
## User Guide
1614

1715
### Sphinx Configuration
@@ -20,7 +18,7 @@ Add to your `conf.py`:
2018

2119
```python
2220
extensions = ["sphinx_external_toc"]
23-
external_toc_path = "_toc.yml" # optional
21+
external_toc_path = "_toc.yml" # optional, default: _toc.yml
2422
```
2523

2624
### Basic Structure
@@ -34,9 +32,9 @@ main:
3432
3533
The value of the `file` key will be a path to a file (relative to the `conf.py`) with or without the file extension.
3634

37-
```{important}
35+
:::{important}
3836
Each document file can only occur once in the ToC!
39-
```
37+
:::
4038

4139
Document files can then have a `parts` key - denoting a list of individual toctrees for that document - and in-turn each part should have a `sections` key - denoting a list of children links, that are one of: `file`, `url` or `glob`:
4240

@@ -59,6 +57,9 @@ main:
5957
- glob: other*
6058
```
6159

60+
This is equivalent to having a single `toctree` directive in `intro`, containing `doc1`,
61+
and a single `toctree` directive in `doc1`, with the `:glob:` flag and containing `doc2`, `https://example.com` and `other*`.
62+
6263
As a shorthand, the `sections` key can be at the same level as the `file`, which denotes a document with a single `part`.
6364
For example, this file is exactly equivalent to the one above:
6465

@@ -142,15 +143,19 @@ To build a template site from only a ToC file:
142143
$ sphinx-etoc create-site -p path/to/site -e rst path/to/_toc.yml
143144
```
144145

145-
Note, when using `glob` you can also add additional files in `meta`/`create_additional`, e.g.
146+
Note, you can also add additional files in `meta`/`create_files` amd append text to the end of files with `meta`/`create_append`, e.g.
146147

147148
```yaml
148149
main:
149150
file: intro
150151
sections:
151152
- glob: doc*
152153
meta:
153-
create_additional:
154+
create_append:
155+
intro: |
156+
This is some
157+
extra text
158+
create_files:
154159
- doc1
155160
- doc2
156161
- doc3
@@ -206,11 +211,10 @@ Process:
206211

207212
Questions / TODOs:
208213

209-
- What if toctree directive found in file? (raise incompatibility error/warnings)
210214
- Should `titlesonly` default to `True` (as in jupyter-book)?
211215
- nested numbered toctree not allowed (logs warning), so should be handled if `numbered: true` is in defaults
212216
- Add additional top-level keys, e.g. `appendices` and `bibliography`
213-
- testing against Windows
217+
- testing against Windows (including toc with subfolders)
214218
- option to add files not in toc to `ignore_paths` (including glob)
215219
- Add tests for "bad" toc files
216220

codecov.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ coverage:
22
status:
33
project:
44
default:
5-
target: 82%
5+
target: 85%
66
threshold: 0.2%
77
patch:
88
default:

sphinx_external_toc/events.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Sphinx event functions."""
22
from pathlib import Path
3-
from typing import Optional, Set
3+
from typing import List, Optional, Set
44

5-
from docutils.nodes import document, compound as compound_node
5+
from docutils import nodes
66
from sphinx.addnodes import toctree as toctree_node
77
from sphinx.application import Sphinx
88
from sphinx.config import Config
@@ -17,6 +17,32 @@
1717
logger = logging.getLogger(__name__)
1818

1919

20+
def create_warning(
21+
app: Sphinx,
22+
doctree: nodes.document,
23+
category: str,
24+
message: str,
25+
*,
26+
line: Optional[int] = None,
27+
append_to: Optional[nodes.Element] = None,
28+
wtype: str = "etoc",
29+
) -> Optional[nodes.system_message]:
30+
"""Generate a warning, logging it if necessary.
31+
32+
If the warning type is listed in the ``suppress_warnings`` configuration,
33+
then ``None`` will be returned and no warning logged.
34+
"""
35+
message = f"{message} [{wtype}.{category}]"
36+
kwargs = {"line": line} if line is not None else {}
37+
38+
if not logging.is_suppressed_warning(wtype, category, app.config.suppress_warnings):
39+
msg_node = doctree.reporter.warning(message, **kwargs)
40+
if append_to is not None:
41+
append_to.append(msg_node)
42+
return msg_node
43+
return None
44+
45+
2046
def parse_toc_to_env(app: Sphinx, config: Config) -> None:
2147
"""Parse the external toc file and store it in the Sphinx environment."""
2248
try:
@@ -53,11 +79,21 @@ def add_changed_toctrees(
5379
return changed_docs
5480

5581

56-
def append_toctrees(app: Sphinx, doctree: document) -> None:
82+
def append_toctrees(app: Sphinx, doctree: nodes.document) -> None:
5783
"""Create the toctree nodes and add it to the document.
5884
5985
Adapted from `sphinx/directives/other.py::TocTree`
6086
"""
87+
# check for existing toctrees and raise warning
88+
for node in doctree.traverse(toctree_node):
89+
create_warning(
90+
app,
91+
doctree,
92+
"toctree",
93+
"toctree directive not expected with external-toc",
94+
line=node.line,
95+
)
96+
6197
site_map: SiteMap = app.env.external_site_map
6298
doc_item: Optional[DocItem] = site_map.get(app.env.docname)
6399

@@ -87,10 +123,10 @@ def append_toctrees(app: Sphinx, doctree: document) -> None:
87123
subnode["includehidden"] = False
88124
subnode["numbered"] = toctree.numbered
89125
subnode["titlesonly"] = toctree.titlesonly
90-
wrappernode = compound_node(classes=["toctree-wrapper"])
126+
wrappernode = nodes.compound(classes=["toctree-wrapper"])
91127
wrappernode.append(subnode)
92128

93-
node_list = []
129+
node_list: List[nodes.Element] = []
94130

95131
for entry in toctree.sections:
96132

@@ -116,7 +152,7 @@ def append_toctrees(app: Sphinx, doctree: document) -> None:
116152
else:
117153
message = f"toctree contains reference to nonexisting document {docname!r}"
118154

119-
node_list.append(doctree.reporter.warning(message))
155+
create_warning(app, doctree, "ref", message, append_to=node_list)
120156
app.env.note_reread()
121157
else:
122158
subnode["entries"].append((title, docname))
@@ -130,11 +166,10 @@ def append_toctrees(app: Sphinx, doctree: document) -> None:
130166
subnode["entries"].append((None, docname))
131167
subnode["includefiles"].append(docname)
132168
if not docnames:
133-
node_list.append(
134-
doctree.reporter.warning(
135-
f"toctree glob pattern '{entry}' didn't match any documents"
136-
)
169+
message = (
170+
f"toctree glob pattern '{entry}' didn't match any documents"
137171
)
172+
create_warning(app, doctree, "glob", message, append_to=node_list)
138173

139174
# reversing entries can be useful when globbing
140175
if toctree.reversed:

sphinx_external_toc/tools.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from itertools import chain
22
from pathlib import Path, PurePosixPath
33
from os import linesep
4-
from typing import Union
4+
import shutil
5+
from typing import Mapping, Optional, Sequence, Union
56

6-
from .api import parse_toc_file
7+
from .api import parse_toc_file, SiteMap
78

89

910
def create_site_from_toc(
@@ -13,38 +14,65 @@ def create_site_from_toc(
1314
default_ext: str = ".rst",
1415
encoding: str = "utf8",
1516
overwrite: bool = False,
16-
) -> Path:
17+
toc_name: Optional[str] = "_toc.yml",
18+
) -> SiteMap:
1719
"""Create the files defined in the external toc file.
1820
1921
Additional files can also be created by defining them in
20-
`meta`/`create_additional` of the toc
22+
`meta`/`create_files` of the toc.
23+
Text can also be appended to files, by defining them in `meta`/`create_append`
24+
(ass a mapping of files -> text)
2125
2226
:param toc_path: Path to ToC.
2327
:param root_path: The root directory , or use ToC file directory.
2428
:param default_ext: The default file extension to use.
2529
:param encoding: Encoding for writing files
2630
:param overwrite: Overwrite existing files (otherwise raise ``IOError``).
31+
:param toc_name: Copy toc file to root with this name
2732
28-
:returns: Root path.
2933
"""
3034
assert default_ext in {".rst", ".md"}
3135
site_map = parse_toc_file(toc_path)
3236

3337
root_path = Path(toc_path).parent if root_path is None else Path(root_path)
38+
root_path.mkdir(parents=True, exist_ok=True)
3439

35-
for docname in chain(site_map, site_map.meta.get("create_additional", [])):
40+
# retrieve and validate meta variables
41+
additional_files = site_map.meta.get("create_files", [])
42+
assert isinstance(additional_files, Sequence), "'create_files' should be a list"
43+
append_text = site_map.meta.get("create_append", {})
44+
assert isinstance(append_text, Mapping), "'create_append' should be a mapping"
45+
46+
# copy toc file to root
47+
if toc_name and not root_path.joinpath(toc_name).exists():
48+
shutil.copyfile(toc_path, root_path.joinpath(toc_name))
49+
50+
# create files
51+
for docname in chain(site_map, additional_files):
52+
53+
# create document
54+
filename = docname
3655
if not any(docname.endswith(ext) for ext in {".rst", ".md"}):
37-
docname += default_ext
38-
docpath = root_path.joinpath(PurePosixPath(docname))
56+
filename += default_ext
57+
docpath = root_path.joinpath(PurePosixPath(filename))
3958
if docpath.exists() and not overwrite:
4059
raise IOError(f"Path already exists: {docpath}")
4160
docpath.parent.mkdir(parents=True, exist_ok=True)
42-
heading = f"Heading: {docname}"
61+
4362
content = []
44-
if docname.endswith(".rst"):
63+
64+
# add heading based on file type
65+
heading = f"Heading: {filename}"
66+
if filename.endswith(".rst"):
4567
content = [heading, "=" * len(heading), ""]
46-
elif docname.endswith(".md"):
68+
elif filename.endswith(".md"):
4769
content = ["# " + heading, ""]
70+
71+
# append extra text
72+
extra_lines = append_text.get(docname, "").splitlines()
73+
if extra_lines:
74+
content.extend(extra_lines + [""])
75+
4876
docpath.write_text(linesep.join(content), encoding=encoding)
4977

50-
return root_path
78+
return site_map
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
main:
2+
file: intro
3+
meta:
4+
create_files:
5+
- doc1
6+
create_append:
7+
intro: |
8+
.. toctree::
9+
10+
doc1
11+
expected_warning: toctree directive not expected
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
main:
2+
file: intro
3+
sections:
4+
- glob: doc*
5+
meta:
6+
expected_warning: toctree glob pattern

tests/_toc_files/glob.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ main:
44
sections:
55
- glob: doc*
66
meta:
7-
create_additional:
7+
create_files:
88
- doc1
99
- doc2
1010
- doc3

tests/test_api/test_file_to_sitemap_glob_.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
_meta:
2-
create_additional:
2+
create_files:
33
- doc1
44
- doc2
55
- doc3

tests/test_sphinx.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
from pathlib import Path
3-
import shutil
43

54
import pytest
65
from sphinx.testing.util import SphinxTestApp
@@ -24,7 +23,8 @@ def __init__(self, app: SphinxTestApp, src: Path):
2423

2524
def build(self, assert_pass=True):
2625
self.app.build()
27-
assert self.warnings == "", self.status
26+
if assert_pass:
27+
assert self.warnings == "", self.status
2828
return self
2929

3030
@property
@@ -52,15 +52,41 @@ def _func(src_path: Path, **kwargs) -> SphinxBuild:
5252
@pytest.mark.parametrize(
5353
"path", TOC_FILES, ids=[path.name.rsplit(".", 1)[0] for path in TOC_FILES]
5454
)
55-
def test_sphinx_build(path: Path, tmp_path: Path, sphinx_build_factory):
55+
def test_success(path: Path, tmp_path: Path, sphinx_build_factory):
56+
"""Test successful builds."""
5657
src_dir = tmp_path / "srcdir"
57-
src_dir.mkdir()
58-
# copy toc to temp
59-
shutil.copyfile(path, src_dir / "_toc.yml")
60-
# write conf.py
61-
src_dir.joinpath("conf.py").write_text(CONF_CONTENT, encoding="utf8")
6258
# write document files
6359
create_site_from_toc(path, root_path=src_dir)
60+
# write conf.py
61+
src_dir.joinpath("conf.py").write_text(CONF_CONTENT, encoding="utf8")
6462
# run sphinx
6563
builder = sphinx_build_factory(src_dir)
6664
builder.build()
65+
66+
67+
def test_contains_toctree(tmp_path: Path, sphinx_build_factory):
68+
"""Test for warning if ``toctree`` directive in file."""
69+
path = Path(__file__).parent.joinpath("_bad_toc_files", "contains_toctree.yml")
70+
src_dir = tmp_path / "srcdir"
71+
# write document files
72+
sitemap = create_site_from_toc(path, root_path=src_dir)
73+
# write conf.py
74+
src_dir.joinpath("conf.py").write_text(CONF_CONTENT, encoding="utf8")
75+
# run sphinx
76+
builder = sphinx_build_factory(src_dir)
77+
builder.build(assert_pass=False)
78+
assert sitemap.meta["expected_warning"] in builder.warnings
79+
80+
81+
def test_no_glob_match(tmp_path: Path, sphinx_build_factory):
82+
"""Test for warning if glob pattern does not match any files."""
83+
path = Path(__file__).parent.joinpath("_bad_toc_files", "no_glob_match.yml")
84+
src_dir = tmp_path / "srcdir"
85+
# write document files
86+
sitemap = create_site_from_toc(path, root_path=src_dir)
87+
# write conf.py
88+
src_dir.joinpath("conf.py").write_text(CONF_CONTENT, encoding="utf8")
89+
# run sphinx
90+
builder = sphinx_build_factory(src_dir)
91+
builder.build(assert_pass=False)
92+
assert sitemap.meta["expected_warning"] in builder.warnings

tests/test_tools/test_file_to_sitemap_basic_.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
- _toc.yml
12
- doc1.rst
23
- doc2.rst
34
- doc3.rst

0 commit comments

Comments
 (0)