Skip to content

Commit 29ff697

Browse files
authored
Advanced section numbering customization (#112)
This merge commit introduces section numbering styles and improved numbering control to the sphinx-external-toc extension, making it possible to customize section numbers (numerical, roman, alphabetic, etc.) and restart numbering per subtree. It also integrates the sphinx-multitoc-numbering extension directly, disables the built-in Sphinx toctree collector, and adds support for new configuration options. The documentation and codebase have been updated to reflect these enhancements.
1 parent 3893340 commit 29ff697

40 files changed

+1995
-56
lines changed

.github/workflows/tests.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
os: [ubuntu-latest]
21-
python-version: ["3.9", "3.10", "3.11", "3.12"]
21+
python-version: ["3.9","3.10", "3.11", "3.12", "3.13","3.14"]
2222
include:
2323
- os: windows-latest
24-
python-version: 3.9
24+
python-version: "3.9"
25+
- os: windows-latest
26+
python-version: "3.14"
2527

2628
runs-on: ${{ matrix.os }}
2729

@@ -39,13 +41,13 @@ jobs:
3941
run: |
4042
pytest --cov=sphinx_external_toc --cov-report=xml --cov-report=term-missing
4143
- name: Upload to Codecov
42-
if: matrix.python-version == 3.11
43-
uses: codecov/codecov-action@v3
44+
if: matrix.python-version == '3.14'
45+
uses: codecov/codecov-action@v4
4446
with:
45-
name: pytests-py3.11
47+
name: pytests-py3.14
4648
flags: pytests
4749
file: ./coverage.xml
48-
fail_ci_if_error: true
50+
fail_ci_if_error: false # uploading coverage should not fail the tests
4951

5052
publish:
5153

.readthedocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ python:
1515
sphinx:
1616
builder: html
1717
fail_on_warning: true
18+
configuration: docs/conf.py

README.md

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![PyPI][pypi-badge]][pypi-link]
77

88
A sphinx extension that allows the documentation site-map (a.k.a Table of Contents) to be defined external to the documentation files.
9-
As used by [Jupyter Book](https://jupyterbook.org)!
9+
As used by default by [Jupyter Book](https://jupyterbook.org) (no need to manually add this extension to the extensions in `_config.yml` in a JupyterBook)!
1010

1111
In normal Sphinx documentation, the documentation site-map is defined *via* a bottom-up approach - adding [`toctree` directives](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#table-of-contents) within pages of the documentation.
1212

@@ -24,12 +24,25 @@ Add to your `conf.py`:
2424

2525
```python
2626
extensions = ["sphinx_external_toc"]
27+
use_multitoc_numbering = True # optional, default: True
2728
external_toc_path = "_toc.yml" # optional, default: _toc.yml
2829
external_toc_exclude_missing = False # optional, default: False
2930
```
3031

3132
Note the `external_toc_path` is always read as a Unix path, and can either be specified relative to the source directory (recommended) or as an absolute path.
3233

34+
### Jupyterbook configuration
35+
36+
This extension is included in your jupyterbook configuration by default, so there's need to add it to the list of extensions. The other options can still be added:
37+
38+
```yaml
39+
use_multitoc_numbering: true # optional, default: true
40+
external_toc_path: "_toc.yml" # optional, default: _toc.yml
41+
external_toc_exclude_missing: False # optional, default: False
42+
```
43+
44+
Note the `external_toc_path` is always read as a Unix path, and can either be specified relative to the source directory (recommended) or as an absolute path.
45+
3346
### Basic Structure
3447

3548
A minimal ToC defines the top level `root` key, for a single root document file:
@@ -113,11 +126,27 @@ Each subtree can be configured with a number of options (see also [sphinx `toctr
113126
By default it is appended to the end of the document, but see also the `tableofcontents` directive for positioning of the ToC.
114127
- `maxdepth` (integer): A maximum nesting depth to use when showing the ToC within the document (default -1, meaning infinite).
115128
- `numbered` (boolean or integer): Automatically add numbers to all documents within a subtree (default `False`).
116-
If set to `True`, all sub-trees will also be numbered based on nesting (e.g. with `1.1` or `1.1.1`),
117-
or if set to an integer then the numbering will only be applied to that depth.
129+
If set to `True`, all subtrees will also be numbered based on nesting (e.g. with `1.1` or `1.1.1`),
130+
or if set to an integer then the numbering will only be applied until that depth. Warning: This can lead to unexpected results if not carefully managed, for example references created using `numref` may fail. Internally this options is always converted to an integer, with `True` -> `999` (effectively unlimited depth) and `False` -> `0` (no numbering).
118131
- `reversed` (boolean): If `True` then the entries in the subtree will be listed in reverse order (default `False`).
119132
This can be useful when using `glob` entries.
120133
- `titlesonly` (boolean): If `True` then only the first heading in the document will be shown in the ToC, not other headings of the same level (default `False`).
134+
- `style` (string or list of strings): The section numbering style to use for this subtree (default `numerical`).
135+
If a single string is given, this will be used for the top level of the subtree.
136+
If a list of strings is given, then each entry will be used for the corresponding level of section numbering.
137+
If styles are not given for all levels, then the remaining levels will be `numerical`.
138+
If too many styles are given, the extra ones will be ignored.
139+
The first time a style is used at the top level in a subtree, the numbering will start from 1, 'a', 'A', 'I' or 'i' depending on the style.
140+
Subsequent times the same style is used at the top level in a subtree, the numbering will continue from the last number used for that style, unless `restart_numbering` is set to `True`.
141+
Available styles:
142+
- `numerical`: 1, 2, 3, ...
143+
- `romanlower`: i, ii, iii, iv, v, ...
144+
- `romanupper`: I, II, III, IV, V, ...
145+
- `alphalower`: a, b, c, d, e, ..., aa, ab, ...
146+
- `alphaupper`: A, B, C, D, E, ..., AA, AB, ...
147+
- `restart_numbering` (boolean): If `True`, the numbering for the top level of this subtree will restart from 1 (or 'a', 'A', 'I' or 'i' depending on the style). If `False` the numbering for the top level of this subtree will continue from the last letter/number/symbol used in a previous subtree with the same style. The default value of this option is `not use_multitoc_numbering`. This means that:
148+
- if `use_multitoc_numbering` is `True` (the default), the numbering for each part will continue from the last letter/number/symbol used in a previous part with the same style, unless `restart_numbering` is explicitly set to `True`.
149+
- if `use_multitoc_numbering` is `False`, the numbering of each subtree will restart from 1 (or 'a', 'A', 'I' or 'i' depending on the style), unless `restart_numbering` is explicitly set to `False`.
121150

122151
These options can be set at the level of the subtree:
123152

@@ -130,6 +159,8 @@ subtrees:
130159
numbered: True
131160
reversed: False
132161
titlesonly: True
162+
style: [alphaupper, romanlower]
163+
restart_numbering: True
133164
entries:
134165
- file: doc1
135166
subtrees:
@@ -149,6 +180,8 @@ options:
149180
numbered: True
150181
reversed: False
151182
titlesonly: True
183+
style: [alphaupper, romanlower]
184+
restart_numbering: True
152185
entries:
153186
- file: doc1
154187
options:
@@ -169,21 +202,14 @@ options:
169202
maxdepth: 1
170203
numbered: True
171204
reversed: False
205+
style: [alphaupper, romanlower]
206+
restart_numbering: True
172207
entries:
173208
- file: doc1
174209
entries:
175210
- file: doc2
176211
```
177212

178-
:::{warning}
179-
`numbered` should not generally be used as a default, since numbering cannot be changed by nested subtrees, and sphinx will log a warning.
180-
:::
181-
182-
:::{note}
183-
By default, title numbering restarts for each subtree.
184-
If you want want this numbering to be continuous, check-out the [sphinx-multitoc-numbering extension](https://github.com/executablebooks/sphinx-multitoc-numbering).
185-
:::
186-
187213
### Using different key-mappings
188214

189215
For certain use-cases, it is helpful to map the `subtrees`/`entries` keys to mirror e.g. an output [LaTeX structure](https://www.overleaf.com/learn/latex/sections_and_chapters).
@@ -424,13 +450,13 @@ meta: {}
424450

425451
Questions / TODOs:
426452

427-
- Add additional top-level keys, e.g. `appendices` (see https://github.com/sphinx-doc/sphinx/issues/2502) and `bibliography`
453+
- ~~Add additional top-level keys, e.g. `appendices` (see https://github.com/sphinx-doc/sphinx/issues/2502) and `bibliography`.~~ Can be replaced by setting the numbering style and (possibly) restarting the numbering.
428454
- Using `external_toc_exclude_missing` to exclude a certain file suffix:
429455
currently if you had files `doc.md` and `doc.rst`, and put `doc.md` in your ToC,
430456
it will add `doc.rst` to the excluded patterns but then, when looking for `doc.md`,
431457
will still select `doc.rst` (since it is first in `source_suffix`).
432458
Maybe open an issue on sphinx, that `doc2path` should respect exclude patterns.
433-
- Integrate https://github.com/executablebooks/sphinx-multitoc-numbering into this extension? (or upstream PR)
459+
- ~~Integrate https://github.com/executablebooks/sphinx-multitoc-numbering into this extension? (or upstream PR).~~ Included and enforced in this fork.
434460
- document suppressing warnings
435461
- test against orphan file
436462
- https://github.com/executablebooks/sphinx-book-theme/pull/304

docs/user_guide/sphinx.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Add to your `conf.py`:
66

77
```python
88
extensions = ["sphinx_external_toc"]
9+
use_multitoc_numbering = True # optional, default: True
910
external_toc_path = "_toc.yml" # optional, default: _toc.yml
1011
external_toc_exclude_missing = False # optional, default: False
1112
```
@@ -95,11 +96,27 @@ Each subtree can be configured with a number of options (see also [sphinx `toctr
9596
By default it is appended to the end of the document, but see also the `tableofcontents` directive for positioning of the ToC.
9697
- `maxdepth` (integer): A maximum nesting depth to use when showing the ToC within the document (default -1, meaning infinite).
9798
- `numbered` (boolean or integer): Automatically add numbers to all documents within a subtree (default `False`).
98-
If set to `True`, all sub-trees will also be numbered based on nesting (e.g. with `1.1` or `1.1.1`),
99+
If set to `True`, all subtrees will also be numbered based on nesting (e.g. with `1.1` or `1.1.1`),
99100
or if set to an integer then the numbering will only be applied to that depth.
100101
- `reversed` (boolean): If `True` then the entries in the subtree will be listed in reverse order (default `False`).
101102
This can be useful when using `glob` entries.
102103
- `titlesonly` (boolean): If `True` then only the first heading in the document will be shown in the ToC, not other headings of the same level (default `False`).
104+
- `style` (string or list of strings): The section numbering style to use for this subtree (default `numerical`).
105+
If a single string is given, this will be used for the top level of the subtree.
106+
If a list of strings is given, then each entry will be used for the corresponding level of section numbering.
107+
If styles are not given for all levels, then the remaining levels will be `numerical`.
108+
If too many styles are given, the extra ones will be ignored.
109+
The first time a style is used at the top level in a subtree, the numbering will start from 1, 'a', 'A', 'I' or 'i' depending on the style.
110+
Subsequent times the same style is used at the top level in a subtree, the numbering will continue from the last number used for that style, unless `restart_numbering` is set to `True`.
111+
Available styles:
112+
- `numerical`: 1, 2, 3, ...
113+
- `romanlower`: i, ii, iii, iv, v, ...
114+
- `romanupper`: I, II, III, IV, V, ...
115+
- `alphalower`: a, b, c, d, e, ..., aa, ab, ...
116+
- `alphaupper`: A, B, C, D, E, ..., AA, AB, ...
117+
- `restart_numbering` (boolean): If `True`, the numbering for the top level of this subtree will restart from 1 (or 'a', 'A', 'I' or 'i' depending on the style). If `False` the numbering for the top level of this subtree will continue from the last letter/number/symbol used in a previous subtree with the same style. The default value of this option is `not use_multitoc_numbering`. This means that:
118+
- if `use_multitoc_numbering` is `True` (the default), the numbering for each part will continue from the last letter/number/symbol used in a previous part with the same style, unless `restart_numbering` is explicitly set to `True`.
119+
- if `use_multitoc_numbering` is `False`, the numbering of each subtree will restart from 1 (or 'a', 'A', 'I' or 'i' depending on the style), unless `restart_numbering` is explicitly set to `False`.
103120

104121
These options can be set at the level of the subtree:
105122

@@ -112,6 +129,8 @@ subtrees:
112129
numbered: True
113130
reversed: False
114131
titlesonly: True
132+
style: [alphaupper, romanlower]
133+
restart_numbering: True
115134
entries:
116135
- file: doc1
117136
subtrees:
@@ -131,6 +150,8 @@ options:
131150
numbered: True
132151
reversed: False
133152
titlesonly: True
153+
style: [alphaupper, romanlower]
154+
restart_numbering: True
134155
entries:
135156
- file: doc1
136157
options:
@@ -151,21 +172,14 @@ options:
151172
maxdepth: 1
152173
numbered: True
153174
reversed: False
175+
style: [alphaupper, romanlower]
176+
restart_numbering: True
154177
entries:
155178
- file: doc1
156179
entries:
157180
- file: doc2
158181
```
159182

160-
:::{warning}
161-
`numbered` should not generally be used as a default, since numbering cannot be changed by nested subtrees, and sphinx will log a warning.
162-
:::
163-
164-
:::{note}
165-
By default, title numbering restarts for each subtree.
166-
If you want want this numbering to be continuous, check-out the [sphinx-multitoc-numbering extension](https://github.com/executablebooks/sphinx-multitoc-numbering).
167-
:::
168-
169183
## Using different key-mappings
170184

171185
For certain use-cases, it is helpful to map the `subtrees`/`entries` keys to mirror e.g. an output [LaTeX structure](https://www.overleaf.com/learn/latex/sections_and_chapters).

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies = [
2929
"click>=7.1",
3030
"pyyaml",
3131
"sphinx>=5",
32+
"sphinx-multitoc-numbering>=0.1.3"
3233
]
3334

3435
[project.urls]

sphinx_external_toc/__init__.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
"""A sphinx extension that allows the project toctree to be defined in a single file."""
22

3-
__version__ = "1.0.1"
4-
5-
63
from typing import TYPE_CHECKING
74

85
if TYPE_CHECKING:
96
from sphinx.application import Sphinx
107

8+
__version__ = "1.1.0-dev"
9+
1110

1211
def setup(app: "Sphinx") -> dict:
12+
app.setup_extension("sphinx_multitoc_numbering")
13+
1314
"""Initialize the Sphinx extension."""
15+
from .collectors import (
16+
TocTreeCollectorWithStyles,
17+
disable_builtin_toctree_collector,
18+
)
1419
from .events import (
1520
InsertToctrees,
1621
TableofContents,
@@ -19,10 +24,21 @@ def setup(app: "Sphinx") -> dict:
1924
parse_toc_to_env,
2025
)
2126

27+
# collectors
28+
disable_builtin_toctree_collector(app)
29+
app.add_env_collector(TocTreeCollectorWithStyles)
30+
2231
# variables
2332
app.add_config_value("external_toc_path", "_toc.yml", "env")
2433
app.add_config_value("external_toc_exclude_missing", False, "env")
2534

35+
# Register use_multitoc_numbering if not already registered (e.g., by JupyterBook)
36+
try:
37+
app.add_config_value("use_multitoc_numbering", True, "env")
38+
except Exception:
39+
# Already registered, likely by JupyterBook
40+
pass
41+
2642
# Note: this needs to occur after merge_source_suffix event (priority 800)
2743
# this cannot be a builder-inited event, since if we change the master_doc
2844
# it will always mark the config as changed in the env setup and re-build everything

sphinx_external_toc/_compat.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Compatibility for using dataclasses instead of attrs."""
2+
23
from __future__ import annotations
34

45
import dataclasses as dc
@@ -121,7 +122,8 @@ def _validator(inst, attr, value):
121122

122123

123124
def deep_iterable(
124-
member_validator: ValidatorType, iterable_validator: ValidatorType | None = None
125+
member_validator: ValidatorType,
126+
iterable_validator: ValidatorType | None = None,
125127
) -> ValidatorType:
126128
"""
127129
A validator that performs deep validation of an iterable.
@@ -147,3 +149,21 @@ def findall(node: Element):
147149
# findall replaces traverse in docutils v0.18
148150
# note a difference is that findall is an iterator
149151
return getattr(node, "findall", node.traverse)
152+
153+
154+
def validate_style(instance, attribute, value):
155+
allowed = [
156+
"numerical",
157+
"romanupper",
158+
"romanlower",
159+
"alphaupper",
160+
"alphalower",
161+
]
162+
if isinstance(value, list):
163+
for v in value:
164+
if v not in allowed:
165+
raise ValueError(
166+
f"{attribute.name} must be one of {allowed}, not {v!r}"
167+
)
168+
elif value not in allowed:
169+
raise ValueError(f"{attribute.name} must be one of {allowed}, not {value!r}")

sphinx_external_toc/api.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Defines the `SiteMap` object, for storing the parsed ToC."""
2+
23
from collections.abc import MutableMapping
34
from dataclasses import asdict, dataclass
45
from typing import Any, Dict, Iterator, List, Optional, Set, Union
@@ -11,6 +12,7 @@
1112
matches_re,
1213
optional,
1314
validate_fields,
15+
validate_style,
1416
)
1517

1618
#: Pattern used to match URL items.
@@ -61,6 +63,15 @@ class TocTree:
6163
)
6264
reversed: bool = field(default=False, kw_only=True, validator=instance_of(bool))
6365
titlesonly: bool = field(default=False, kw_only=True, validator=instance_of(bool))
66+
# Add extra field for style of toctree rendering
67+
style: Union[List[str], str] = field(
68+
default="numerical", kw_only=True, validator=validate_style
69+
)
70+
# add extra field for restarting numbering for the set style
71+
# Only allow True, False or None. None is the default value.
72+
restart_numbering: Optional[bool] = field(
73+
default=None, kw_only=True, validator=optional(instance_of(bool))
74+
)
6475

6576
def __post_init__(self):
6677
validate_fields(self)
@@ -213,9 +224,11 @@ def _replace_items(d: Dict[str, Any]) -> Dict[str, Any]:
213224
d[k] = _replace_items(v)
214225
elif isinstance(v, (list, tuple)):
215226
d[k] = [
216-
_replace_items(i)
217-
if isinstance(i, dict)
218-
else (str(i) if isinstance(i, str) else i)
227+
(
228+
_replace_items(i)
229+
if isinstance(i, dict)
230+
else (str(i) if isinstance(i, str) else i)
231+
)
219232
for i in v
220233
]
221234
elif isinstance(v, str):

0 commit comments

Comments
 (0)