Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.
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
9 changes: 9 additions & 0 deletions .github/workflows/ci_python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ jobs:
echo "::endgroup::"
deactivate
done
- name: Lint reference docs
run: |
poetry env use 3.11
source $(poetry env info --path)/bin/activate
poetry install --with=docs
pip install ${WHEEL} --force-reinstall
cd docs
python _scripts/lint_reference.py
deactivate
- name: Set up Quarto
uses: quarto-dev/quarto-actions/setup@v2
- name: Build docs
Expand Down
21 changes: 6 additions & 15 deletions python/docs/_scripts/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import sys
from pathlib import Path
from typing import Any, Union
from typing import Any

from pydantic import ValidationError
from quartodoc import blueprint, collect, layout
Expand All @@ -12,6 +12,7 @@
from renderer import Renderer
from summarizer import Summarizer


# `preview()` can be used to help debug doc generation.
# use it on a `section` or `page` element to see a visual
# representation of the element contents. Use the `max_depth`
Expand Down Expand Up @@ -137,12 +138,6 @@ def build(self):
inv = create_inventory(self.package, "0.0.9999", self.items)
convert_inventory(inv, self.out_inventory)

def get_package(self, item: Union[layout.Section, layout.Page]) -> str:
if item.package and f"{item.package}" != "":
return item.package
else:
return self.package

def write_pages(self):
root = layout.Section(
title=self.title,
Expand Down Expand Up @@ -181,16 +176,14 @@ def write_pages(self):
_log.info(f"Rendering {page.path}")
# preview(page, max_depth=4)
page_text = self.renderer.render(page)
page_path = location / \
(page.path + self.out_page_suffix)
page_path = location / (page.path + self.out_page_suffix)
self.write_page_if_not_exists(page_path, page_text)
if page.path in self.page_map:
del self.page_map[page.path]

self.update_page_items(page, location, is_flat)
else:
raise NotImplementedError(
f"Unsupported section item: {type(page)}")
raise NotImplementedError(f"Unsupported section item: {type(page)}")

if len(self.page_map.keys()) > 0:
_log.warning(f"Extra pages: {self.page_map.keys()}")
Expand All @@ -214,8 +207,7 @@ def update_page_items(self, page: layout.Page, location: Path, is_flat: bool):
)
self.update_items(doc, page_path)
else:
raise NotImplementedError(
f"Unsupported page item: {type(doc)}")
raise NotImplementedError(f"Unsupported page item: {type(doc)}")

def update_items(self, doc: layout.Doc, page_path: str):
name = doc.obj.path
Expand Down Expand Up @@ -293,8 +285,7 @@ def from_quarto_config(cls, quarto_cfg: "str | dict"):

cfg = quarto_cfg.get("quartodoc")
if cfg is None:
raise KeyError(
"No `quartodoc:` section found in your _quarto.yml.")
raise KeyError("No `quartodoc:` section found in your _quarto.yml.")

return Builder(
**{k: v for k, v in cfg.items()},
Expand Down
19 changes: 19 additions & 0 deletions python/docs/_scripts/lint_reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import logging
import sys

from linter import Linter


if __name__ == "__main__":
root = logging.getLogger("quartodoc")
root.setLevel(logging.WARNING)

handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
root.addHandler(handler)

Linter.from_quarto_config("_reference.yml").lint()
120 changes: 120 additions & 0 deletions python/docs/_scripts/linter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from __future__ import annotations

import logging
import sys
from pathlib import Path
from typing import Any

from pydantic import ValidationError
from quartodoc import blueprint, collect, layout
from quartodoc.validation import fmt


_log = logging.getLogger("quartodoc")


def load_layout(sections: dict, package: str, options=None):
try:
return layout.Layout(sections=sections, package=package, options=options)
except ValidationError as e:
msg = "Configuration error for YAML:\n - "
errors = [fmt(err) for err in e.errors() if fmt(err)]
first_error = errors[
0
] # we only want to show one error at a time b/c it is confusing otherwise
msg += first_error
raise ValueError(msg) from None


class Linter:
"""Base class for linting API docs.

Parameters
----------
package: str
The name of the package.
sections: ConfigSection
A list of sections, with items to document.
options:
Default options to set for all pieces of content (e.g. include_attributes).
source_dir:
A directory where source files to be documented live. This is only necessary
if you are not documenting a package, but collection of scripts. Use a "."
to refer to the current directory.
parser:
Docstring parser to use. This correspond to different docstring styles,
and can be one of "google", "sphinx", and "numpy". Defaults to "numpy".

"""

package: str
sections: list[Any]
options: dict | None
source_dir: str | None
parser: str

def __init__(
self,
package: str,
sections: list[Any] = tuple(),
options: dict | None = None,
source_dir: str | None = None,
parser="google",
):
self.package = package
self.sections = sections
self.options = options
self.parser = parser

if source_dir:
self.source_dir = str(Path(source_dir).absolute())
sys.path.append(self.source_dir)

def get_items(self, use_sections: bool):
sections = self.sections if use_sections else []

layout = load_layout(
sections=sections, package=self.package, options=self.options
)

_, items = collect(blueprint(layout, parser=self.parser), base_dir="")

return [item.name for item in items]

def lint(self):
"""Lints the config and lets you know about any missing items"""

ref_items = self.get_items(True)
pkg_items = self.get_items(False)

issue_count = 0
for pkg_item in pkg_items:
if pkg_item not in ref_items:
_log.warning(f"Missing item: {pkg_item}")
issue_count += 1

if issue_count > 0:
_log.error("Encountered un-documented items. Please fix.")
sys.exit(1)

@classmethod
def from_quarto_config(cls, quarto_cfg: "str | dict"):
"""Construct a Builder from a configuration object (or yaml file)."""

# TODO: validation / config model loading
if isinstance(quarto_cfg, str):
import yaml

quarto_cfg = yaml.safe_load(open(quarto_cfg))

cfg = quarto_cfg.get("quartodoc")
if cfg is None:
raise KeyError("No `quartodoc:` section found in your _quarto.yml.")

return Linter(
**{
k: v
for k, v in cfg.items()
if k in ["package", "sections", "options", "parser"]
},
)
Loading