Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support custom ordering of the auto TOC tree #18

Merged
merged 7 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@ To run your doc-tests, use the `sphinx-ape test` command:
sphinx-ape test .
```

## Auto-TOC Tree

The `sphinx-ape init` command creates an `index.rst` file.
This file represents the table of contents for the docs site.
Any files not included in the TOC are not included in the documentation.
`sphinx-ape` generates a simple default file with the contents:

```rst
.. dynamic-toc-tree::
```

To customize the files included in the TOC, specify each respective guide-set name (e.g. `userguides`).
Also use this feature to control the ordering of the guides; otherwise the default is to include all guides in the directory in alphabetized order.

```rst
.. dynamic-toc-tree::
:userguides: guide0, guide1, final
```

You can also specify the guides in a list pattern:

```rst
.. dynamic-toc-tree::
:userguides:
- quickstart
- guide0
- guide1
- final
```

## GitHub Action

This GitHub action is meant for building the documentation in both core Ape as well any Ape plugin.
Expand Down
82 changes: 78 additions & 4 deletions sphinx_ape/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,29 @@
from typing import Optional

from sphinx_ape._utils import get_package_name
from sphinx_ape.types import TOCTreeSpec


class Documentation:
def __init__(self, base_path: Optional[Path] = None, name: Optional[str] = None) -> None:
"""
The base-documentation class for working with a sphinx-ape project.
"""

def __init__(
self,
base_path: Optional[Path] = None,
name: Optional[str] = None,
toc_tree_spec: Optional[TOCTreeSpec] = None,
) -> None:
self.base_path = base_path or Path.cwd()
self._name = name or get_package_name()
self._toc_tree_spec = toc_tree_spec or TOCTreeSpec()

@property
def docs_path(self) -> Path:
"""
The root documentation folder.
"""
return self.base_path / "docs"

@property
Expand All @@ -20,37 +34,75 @@ def root_build_path(self) -> Path:

@property
def build_path(self) -> Path:
"""
The build location.
"""
return self.root_build_path / self._name

@property
def latest_path(self) -> Path:
"""
The build location for ``latest/``.
"""
return self.build_path / "latest"

@property
def stable_path(self) -> Path:
"""
The build location for ``stable/``.
"""
return self.build_path / "stable"

@property
def userguides_path(self) -> Path:
"""
The path to the userguides.
"""
return self.docs_path / "userguides"

@property
def commands_path(self) -> Path:
"""
The path to the generated CLI documentation.
"""
return self.docs_path / "commands"

@property
def methoddocs_path(self) -> Path:
"""
The path to the autodoc generated documentation.
"""
return self.docs_path / "methoddocs"

@property
def conf_file(self) -> Path:
"""
The path to sphinx's ``conf.py`` file.
"""
return self.docs_path / "conf.py"

@property
def index_file(self) -> Path:
def index_html_file(self) -> Path:
"""
The path to the index HTML file.
"""
return self.build_path / "index.html"

@property
def index_docs_file(self) -> Path:
"""
The path to the root docs index file.
"""
return self.docs_path / "index.rst"

def init(self, include_quickstart: bool = True):
"""
Initialize documentation structure.

Args:
include_quickstart (bool): Set to ``False`` to ignore
creating the quickstart guide. Defaults to ``True``.
"""
if not self.docs_path.is_dir():
self.docs_path.mkdir()

Expand All @@ -68,7 +120,7 @@ def _ensure_conf_exists(self):
self.conf_file.write_text(content)

def _ensure_index_exists(self):
index_file = self.docs_path / "index.rst"
index_file = self.index_docs_file
if index_file.is_file():
return

Expand All @@ -86,6 +138,9 @@ def _ensure_quickstart_exists(self):

@cached_property
def quickstart_name(self) -> Optional[str]:
"""
The name of the quickstart guide, if it exists.
"""
guides = self._get_filenames(self.userguides_path)
for guide in guides:
if guide == "quickstart":
Expand All @@ -97,6 +152,9 @@ def quickstart_name(self) -> Optional[str]:

@property
def userguide_names(self) -> list[str]:
"""
An ordered list of all userguides.
"""
guides = self._get_filenames(self.userguides_path)
if not (quickstart := self.quickstart_name):
# Guides has no quickstart.
Expand All @@ -106,14 +164,30 @@ def userguide_names(self) -> list[str]:

@property
def cli_reference_names(self) -> list[str]:
"""
An ordered list of all CLI references.
"""
return self._get_filenames(self.commands_path)

@property
def methoddoc_names(self) -> list[str]:
"""
An ordered list of all method references.
"""
return self._get_filenames(self.methoddocs_path)

def _get_filenames(self, path: Path) -> list[str]:
if not path.is_dir():
return []

return sorted([g.stem for g in path.iterdir() if g.suffix in (".md", ".rst")])
filenames = {p.stem for p in path.iterdir() if _is_doc(p)}
if spec := self._toc_tree_spec.get(path.name):
# Adhere to configured order and filtering.
return [f for f in spec if f in filenames]

# Default to a sorted order.
return sorted(filenames)


def _is_doc(path: Path) -> bool:
return path.suffix in (".md", ".rst")
13 changes: 9 additions & 4 deletions sphinx_ape/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sphinx_ape._base import Documentation
from sphinx_ape._utils import extract_source_url, git, replace_tree, sphinx_build
from sphinx_ape.exceptions import ApeDocsBuildError, ApeDocsPublishError
from sphinx_ape.types import TOCTreeSpec

REDIRECT_HTML = """
<!DOCTYPE html>
Expand Down Expand Up @@ -67,9 +68,10 @@ def __init__(
base_path: Optional[Path] = None,
name: Optional[str] = None,
pages_branch_name: Optional[str] = None,
toc_tree_spec: Optional[TOCTreeSpec] = None,
) -> None:
self.mode = BuildMode.LATEST if mode is None else mode
super().__init__(base_path, name)
super().__init__(base_path, name, toc_tree_spec=toc_tree_spec)
self._pages_branch_name = pages_branch_name or "gh-pages"

def build(self):
Expand Down Expand Up @@ -105,7 +107,10 @@ def build(self):
self._setup_redirect()

def clean(self):
shutil.rmtree(self.root_build_path)
"""
Clean build directories.
"""
shutil.rmtree(self.root_build_path, ignore_errors=True)

def publish(self, repository: Optional[str] = None, push: bool = True):
"""
Expand Down Expand Up @@ -214,8 +219,8 @@ def _setup_redirect(self):
redirect = f"{redirect}userguides/{quickstart}.html"

# We replace it to handle the case when stable has joined the chat.
self.index_file.unlink(missing_ok=True)
self.index_file.write_text(REDIRECT_HTML.format(redirect))
self.index_html_file.unlink(missing_ok=True)
self.index_html_file.write_text(REDIRECT_HTML.format(redirect))

def _sphinx_build(self, dst_path: Path):
shutil.rmtree(dst_path, ignore_errors=True)
Expand Down
42 changes: 37 additions & 5 deletions sphinx_ape/sphinx_ext/directives.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from pathlib import Path
from typing import Optional

from docutils.parsers.rst import directives
from sphinx.util.docutils import SphinxDirective

from sphinx_ape.build import DocumentationBuilder
from sphinx_ape.types import TOCTreeSpec


class DynamicTocTree(SphinxDirective):
Expand All @@ -14,6 +16,10 @@ class DynamicTocTree(SphinxDirective):

option_spec = {
"title": directives.unchanged,
"plugin-prefix": directives.unchanged,
"userguides": directives.unchanged,
"commands": directives.unchanged,
"methoddocs": directives.unchanged,
}

@property
Expand All @@ -25,35 +31,54 @@ def _base_path(self) -> Path:
def title(self) -> str:
if res := self.options.get("title"):
# User configured the title.
return res
return res.strip()

# Deduced: "Ape-Docs" or "Ape-Vyper-Docs", etc.
name = self._base_path.parent.name
name_parts = [n.capitalize() for n in name.split("-")]
capped_name = "-".join(name_parts)
return f"{capped_name}-Docs"

@property
def plugin_prefix(self) -> Optional[str]:
return self.options.get("plugin-prefix", "").strip()

@property
def _title_rst(self) -> str:
title = self.title
bar = "=" * len(title)
return f"{title}\n{bar}"

@property
def toc_tree_spec(self) -> TOCTreeSpec:
return TOCTreeSpec(
userguides=_parse_spec(self.options.get("userguides")),
methoddocs=_parse_spec(self.options.get("methoddocs")),
commands=_parse_spec(self.options.get("commands")),
)

@property
def builder(self) -> DocumentationBuilder:
return DocumentationBuilder(base_path=self._base_path.parent)
return DocumentationBuilder(
base_path=self._base_path.parent,
toc_tree_spec=self.toc_tree_spec,
)

def run(self):
userguides = self._get_userguides()
cli_docs = self._get_cli_references()
methoddocs = self._get_methoddocs()
plugin_methoddocs = [d for d in methoddocs if d.startswith("ape-")]
if plugin_prefix := self.plugin_prefix:
plugin_methoddocs = [d for d in methoddocs if Path(d).stem.startswith(plugin_prefix)]
else:
plugin_methoddocs = []

methoddocs = [d for d in methoddocs if d not in plugin_methoddocs]
sections = {"User Guides": userguides, "CLI Reference": cli_docs}
if plugin_methoddocs:
# Core (or alike)
# Core (or alike).
sections["Core Python Reference"] = methoddocs # Put _before_ plugins!
sections["Plugin Python Reference"] = plugin_methoddocs
sections["Core Python Reference"] = methoddocs
else:
# Plugin or regular package.
sections["Python Reference"] = methoddocs
Expand Down Expand Up @@ -84,3 +109,10 @@ def _get_cli_references(self) -> list[str]:

def _get_methoddocs(self) -> list[str]:
return [f"methoddocs/{n}" for n in self.builder.methoddoc_names]


def _parse_spec(value) -> list[str]:
if value is None:
return []

return [n.strip(" -\n\t,") for n in value.split(" ") if n.strip(" -\n\t")]
28 changes: 28 additions & 0 deletions sphinx_ape/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from typing import Optional


class TOCTreeSpec(dict[str, list[str]]):
"""
Specify the structure of the auto-generated TOC-tree
by specifying names of guides via the keys ``"userguides"``,
``"commands"``, and ``"methoddocs"``. The TOC-tree
will lay out the contents in that order and exclude
any missing guides. It's meant to be a workaround when
the default behavior is inadequate.
"""

def __init__(
self,
userguides: Optional[list[str]] = None,
commands: Optional[list[str]] = None,
methoddocs: Optional[list[str]] = None,
**kwargs,
):
super().__init__(
{
"userguides": userguides or [],
"commands": commands or [],
"methoddocs": methoddocs or [],
**kwargs,
}
)
Loading