diff --git a/README.md b/README.md index 1bdf491..3b647e1 100644 --- a/README.md +++ b/README.md @@ -7,25 +7,28 @@ The purpose of this plugin to share code for generating documentation across all - [python3](https://www.python.org/downloads) version 3.9 up to 3.12. -## Quick Usage +## Install -To use this sphinx plugin, create a `docs/` folder in your Python package. -Inside this folder, create a `conf.py` with the following content: +Install using `pip` or `uv` by either cloning this repo or accessing from pypi, e.g.: -```txt -extensions = ["sphinx_ape"] +```sh +pip install sphinx-ape ``` -Then, create an `index.rst` file with the following content: +Try `sphinx-ape --help` to check if it's installed. + +## Quick Usage + +To use this sphinx plugin, first generate the docs structure (ran from your project directory): -```txt -.. dynamic-toc-tree:: +```sh +sphinx-ape init ``` -You don't have to configure anything else; it will just work. +It will have generated a `docs/` folder with some necessary config file in it, along with a quick-start that links to your `README.md`. -Now, you can begin writing your documentation. -There are three directories you can create: +Now, you can begin writing your Sphinx documentation. +There are three directories you can place documentation sources in: 1. `userguides/` - a directory containing how-to guides for how to use your package. 2. `commands/` - `.rst` files for the `sphinx-click` plugin for CLI-based references. diff --git a/sphinx_ape/_cli.py b/sphinx_ape/_cli.py index e9bf478..803e89e 100644 --- a/sphinx_ape/_cli.py +++ b/sphinx_ape/_cli.py @@ -32,6 +32,17 @@ def package_name_option(): ) +@cli.command() +@click.argument("base_path", type=Path) +def init(): + """ + Initialize documentation structure + """ + # For this command, force the user to be in that dir. + builder = DocumentationBuilder(base_path=Path.cwd()) + builder.init() + + @cli.command() @click.argument("base_path", type=Path) @build_mode_option() diff --git a/sphinx_ape/build.py b/sphinx_ape/build.py index 921a2ab..3acf6c1 100644 --- a/sphinx_ape/build.py +++ b/sphinx_ape/build.py @@ -49,9 +49,12 @@ class DocumentationBuilder: """ def __init__( - self, mode: BuildMode, base_path: Optional[Path] = None, name: Optional[str] = None + self, + mode: Optional[BuildMode] = None, + base_path: Optional[Path] = None, + name: Optional[str] = None, ) -> None: - self.mode = mode + self.mode = BuildMode.LATEST if mode is None else mode self._base_path = base_path or Path.cwd() self._name = name or get_package_name() @@ -59,7 +62,7 @@ def __init__( def docs_path(self) -> Path: path = self._base_path / "docs" if not path.is_dir(): - raise ApeDocsBuildError("No `docs/` folder found.") + raise ApeDocsBuildError(f"No `docs/` folder found (checked {path}.") return path @@ -75,6 +78,26 @@ def latest_path(self) -> Path: def stable_path(self) -> Path: return self.build_path / "stable" + @property + def userguides_path(self) -> Path: + return self.docs_path / "userguides" + + @property + def commands_path(self) -> Path: + return self.docs_path / "commands" + + @property + def methoddocs_path(self) -> Path: + return self.docs_path / "methoddocs" + + def init(self): + if not self.docs_path.is_dir(): + self.docs_path.mkdir() + + self._ensure_quickstart_exists() + self._ensure_conf_exists() + self._ensure_index_exists() + def build(self): if self.mode is BuildMode.LATEST: # TRIGGER: Push to 'main' branch. Only builds latest. @@ -115,6 +138,24 @@ def build_release(self): for path in (self.stable_path, self.latest_path): replace_tree(build_dir, path) + @property + def userguide_names(self) -> list[str]: + guides = self._get_filenames(self.userguides_path) + quickstart_name = "userguides/quickstart" + if quickstart_name in guides: + # Make sure quick start is first. + guides = [quickstart_name, *[g for g in guides if g != quickstart_name]] + + return guides + + @property + def cli_reference_names(self) -> list[str]: + return self._get_filenames(self.commands_path) + + @property + def methoddoc_names(self) -> list[str]: + return self._get_filenames(self.methoddocs_path) + def _setup_redirect(self): self.build_path.mkdir(exist_ok=True, parents=True) @@ -128,3 +169,34 @@ def _setup_redirect(self): def _sphinx_build(self, dst_path): sphinx_build(dst_path, self.docs_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")]) + + def _ensure_conf_exists(self): + conf_file = self.docs_path / "conf.py" + if conf_file.is_file(): + return + + content = 'extensions = ["sphinx_ape"]\n' + conf_file.write_text(content) + + def _ensure_index_exists(self): + index_file = self.docs_path / "index.rst" + if index_file.is_file(): + return + + content = ".. dynamic-toc-tree::\n" + index_file.write_text(content) + + def _ensure_quickstart_exists(self): + quickstart_path = self.userguides_path / "quickstart.md" + if quickstart_path.is_file(): + # Already exists. + return + + self.userguides_path.mkdir(exist_ok=True) + quickstart_path.write_text("```{include} ../../README.md\n```\n") diff --git a/sphinx_ape/docs.py b/sphinx_ape/docs.py new file mode 100644 index 0000000..2d7dc94 --- /dev/null +++ b/sphinx_ape/docs.py @@ -0,0 +1,10 @@ +from pathlib import Path + + +class Documentation: + """ + A class abstracting access to all the documentation. + """ + + def __init__(self, base_path: Path) -> None: + self._base_path = base_path diff --git a/sphinx_ape/sphinx_ext/directives.py b/sphinx_ape/sphinx_ext/directives.py index 45523f4..092f819 100644 --- a/sphinx_ape/sphinx_ext/directives.py +++ b/sphinx_ape/sphinx_ext/directives.py @@ -3,6 +3,8 @@ from docutils.parsers.rst import directives from sphinx.util.docutils import SphinxDirective +from sphinx_ape.build import DocumentationBuilder + class DynamicTocTree(SphinxDirective): """ @@ -38,16 +40,8 @@ def _title_rst(self) -> str: return f"{title}\n{bar}" @property - def userguides_path(self) -> Path: - return self._base_path / "userguides" - - @property - def commands_path(self) -> Path: - return self._base_path / "commands" - - @property - def methoddocs_path(self) -> Path: - return self._base_path / "methoddocs" + def builder(self) -> DocumentationBuilder: + return DocumentationBuilder(base_path=self._base_path.parent) def run(self): userguides = self._get_userguides() @@ -83,24 +77,10 @@ def run(self): return self.parse_text_to_nodes(restructured_text) def _get_userguides(self) -> list[str]: - guides = self._get_doc_entries(self.userguides_path) - quickstart_name = "userguides/quickstart" - if quickstart_name in guides: - # Make sure quick start is first. - guides = [quickstart_name, *[g for g in guides if g != quickstart_name]] - - return guides + return [f"userguides/{n}" for n in self.builder.userguide_names] def _get_cli_references(self) -> list[str]: - return self._get_doc_entries(self.commands_path) + return [f"commands/{n}" for n in self.builder.cli_reference_names] def _get_methoddocs(self) -> list[str]: - return self._get_doc_entries(self.methoddocs_path) - - def _get_doc_entries(self, path: Path) -> list[str]: - if not path.is_dir(): - return [] - - return sorted( - [f"{path.name}/{g.stem}" for g in path.iterdir() if g.suffix in (".md", ".rst")] - ) + return [f"methoddocs/{n}" for n in self.builder.methoddoc_names] diff --git a/sphinx_ape/sphinx_ext/plugin.py b/sphinx_ape/sphinx_ext/plugin.py index 0c3cc71..f4f8922 100644 --- a/sphinx_ape/sphinx_ext/plugin.py +++ b/sphinx_ape/sphinx_ext/plugin.py @@ -2,31 +2,21 @@ import sys from pathlib import Path -from sphinx.util import logging from sphinx.application import Sphinx +from sphinx.util import logging +from sphinx_ape.build import DocumentationBuilder from sphinx_ape.sphinx_ext.directives import DynamicTocTree from sphinx_ape.utils import get_package_name - logger = logging.getLogger(__name__) -def ensure_quickstart_exists(app, cfg): - """ - This will generate the quickstart guide if needbe. - I recommend committing the files it generates. - """ - - userguides_path = Path(app.srcdir) / "userguides" - quickstart_path = userguides_path / "quickstart.md" - if quickstart_path.is_file(): - # Already exists. - return - - logger.info("Generating userguides/quickstart.md") - userguides_path.mkdir(exist_ok=True) - quickstart_path.write_text("```{include} ../../README.md\n```\n") +def config_init_hook(app, cfg): + base_path = Path(app.srcdir.parent) + builder = DocumentationBuilder(base_path) + # Ensure the necessary files exist. + builder.init() def setup(app: Sphinx): @@ -37,7 +27,7 @@ def setup(app: Sphinx): sys.path.insert(0, os.path.abspath("..")) # Register the hook that generates the quickstart file if needbe. - app.connect("config-inited", ensure_quickstart_exists) + app.connect("config-inited", config_init_hook) # Configure project and other one-off items. package_name = get_package_name()