diff --git a/hvplot/tests/scripts/test_llms_config.py b/hvplot/tests/scripts/test_llms_config.py new file mode 100644 index 000000000..f8e113ebd --- /dev/null +++ b/hvplot/tests/scripts/test_llms_config.py @@ -0,0 +1,62 @@ +"""Tests for the nbsite LLM documentation build config (scripts/llms_config.py). + +The ``llms_config`` module requires ``nbsite``, which is only a doc +dependency, so the tests are skipped when it is not installed. +""" + +import sys +from pathlib import Path + +import pytest + +pytest.importorskip('nbsite') + +REPO_ROOT = Path(__file__).parents[3] +sys.path.insert(0, str(REPO_ROOT / 'scripts')) + +import llms_config # noqa: E402 +from nbsite.scripts import LlmsBuildConfig # noqa: E402 + +SOURCE_SUFFIXES = ('.md', '.ipynb', '.rst') + + +def _test_paths() -> set[Path]: + return llms_config.PAGES | llms_config.ROOT_PAGES + + +def _resolve_doc_path(path: Path) -> Path | None: + """Return the source file in ``doc/`` a configured page maps to, if any.""" + stem = path.with_suffix('') + for suffix in SOURCE_SUFFIXES: + candidate = llms_config.DOC_DIR / f'{stem}{suffix}' + if candidate.exists(): + return candidate + return None + + +def _matches(section, path: Path) -> bool: + prefix = section.path_prefix + matches_prefix = prefix in {Path(), Path('.')} or path.is_relative_to(prefix) + return matches_prefix and section.path_filter(path) + + +def test_config_is_valid() -> None: + assert isinstance(llms_config.CONFIG, LlmsBuildConfig) + + +@pytest.mark.parametrize('path', sorted(_test_paths())) +def test_referenced_pages_exist(path: Path) -> None: + assert _resolve_doc_path(path) is not None + + +def test_sections_match_at_least_one_page() -> None: + for section in llms_config.CONFIG.sections: + assert any(_matches(section, path) for path in _test_paths()) + + +@pytest.mark.parametrize('section', llms_config.CONFIG.sections, ids=lambda s: s.title) +def test_section_labels_are_unique_and_non_empty(section) -> None: + paths = [path for path in _test_paths() if _matches(section, path)] + labels = [section.label_builder(path) for path in paths] + assert all(labels) + assert len(labels) == len(set(labels)) diff --git a/pixi.toml b/pixi.toml index e6cc65663..9fb4495e3 100644 --- a/pixi.toml +++ b/pixi.toml @@ -245,9 +245,10 @@ HVPLOT_PATCH_PLOT_DOCSTRING_SIGNATURE = "false" [feature.doc.tasks] docs-build-sphinx = 'sphinx-build -j auto -b html doc builtdocs' _docs-install = 'python -m pip install --no-deps --disable-pip-version-check -e .' +_docs_markdown = 'python -m nbsite build-llms --config scripts/llms_config.py' # Depends on _docs-install instead of install as install # in the default environment -docs-build = { depends-on = ["_docs-install", "docs-build-sphinx"] } +docs-build = { depends-on = ["_docs-install", "docs-build-sphinx", "_docs_markdown"] } docs-server = 'python -m http.server 5500 --directory ./builtdocs' # ================== BUILD ==================== diff --git a/scripts/llms_config.py b/scripts/llms_config.py new file mode 100644 index 000000000..4cbb0f545 --- /dev/null +++ b/scripts/llms_config.py @@ -0,0 +1,116 @@ +"""Config for building hvPlot markdown docs and llms.txt.""" + +from __future__ import annotations + +from pathlib import Path + +from nbsite.scripts import LlmsBuildConfig, LlmsSection, MarkdownSource + +ROOT = Path(__file__).parent.parent +DOC_DIR = ROOT / 'doc' +BUILTDOCS_DIR = ROOT / 'builtdocs' +OUTPUT_DIR = BUILTDOCS_DIR / 'markdown' +MARKDOWN_BASE_URL = '/markdown' + +PAGES = { + Path('index.md'), + Path('about.md'), + Path('developer_guide.md'), + Path('releases.md'), + Path('roadmap.md'), + Path('ref/index.md'), + Path('ref/deprecations.md'), + Path('ref/installation.md'), + Path('ref/api/index.md'), + Path('ref/api_compatibility/pandas/index.md'), + Path('ref/plotting_options/index.md'), + Path('ref/plotting_options/axis.md'), + Path('ref/plotting_options/data.md'), + Path('ref/plotting_options/geographic.md'), + Path('ref/plotting_options/interactivity.md'), + Path('ref/plotting_options/legend.md'), + Path('ref/plotting_options/size_layout.md'), + Path('ref/plotting_options/styling.md'), + Path('ref/api/manual/hvplot.hvPlot.area.md'), + Path('ref/api/manual/hvplot.hvPlot.bar.md'), + Path('ref/api/manual/hvplot.hvPlot.explorer.md'), + Path('ref/api/manual/hvplot.hvPlot.heatmap.md'), + Path('ref/api/manual/hvplot.hvPlot.line.md'), + Path('ref/api/manual/hvplot.hvPlot.points.md'), + Path('ref/api/manual/hvplot.hvPlot.scatter.md'), +} + +ROOT_PAGES = { + Path('index.md'), + Path('about.md'), + Path('developer_guide.md'), + Path('releases.md'), + Path('roadmap.md'), +} + + +def _index_label(path: Path) -> str: + return 'home' if path.parent == Path('.') else path.parent.as_posix().replace('-', ' ') + + +def _label(path: Path) -> str: + if path.stem == 'index': + return _index_label(path) + return path.stem.replace('_', ' ') + + +def _api_label(path: Path) -> str: + if path.stem == 'index': + return _index_label(path) + name = path.stem + for prefix in ('hvplot.hvPlot.', 'hvplot.plotting.', 'hvplot.ui.', 'hvplot.networkx.'): + if name.startswith(prefix): + name = name.removeprefix(prefix) + break + return name.replace('_', ' ') + + +CONFIG = LlmsBuildConfig( + project_title='hvPlot', + project_description='hvPlot documentation selected for LLM-friendly browsing.', + markdown_root=OUTPUT_DIR, + llms_output_path=BUILTDOCS_DIR / 'llms.txt', + markdown_base_url=MARKDOWN_BASE_URL, + sources=( + MarkdownSource( + source_dir=DOC_DIR, + output_dir=OUTPUT_DIR, + exclude_dir_names=('.ipynb_checkpoints', 'user_guide'), + ), + ), + sections=( + LlmsSection( + title='Home', + description='Top-level pages and project overview.', + path_prefix=Path('.'), + path_filter=lambda path: path in ROOT_PAGES, + label_builder=_label, + ), + LlmsSection( + title='Reference', + description='Installation notes and reference overview pages.', + path_prefix=Path('ref'), + path_filter=lambda path: path in PAGES, + label_builder=_api_label, + ), + LlmsSection( + title='Plotting Options', + description='Core plotting configuration topics.', + path_prefix=Path('ref/plotting_options'), + path_filter=lambda path: path in PAGES, + label_builder=_label, + ), + LlmsSection( + title='API Manual', + description='Representative hvPlot API examples.', + path_prefix=Path('ref/api/manual'), + path_filter=lambda path: path in PAGES, + label_builder=_api_label, + ), + ), +)