Skip to content

Commit e13dbb5

Browse files
committed
feat(llms): add llms.txt generation via sphinx-llm
Wire NVIDIA's sphinx-llm extension into the docs build so projects can emit llms.txt / llms-full.txt alongside the HTML output. - pyproject: add `llms` extra (sphinx-llm>=0.4.1) and enable it for yardang's own docs via [tool.yardang.llms] - build.py: load [tool.yardang.llms] config and pass use_llms plus the llms_txt_* passthrough options into the conf template - conf.py.j2: register the sphinx_llm.txt extension and emit llms_txt_* settings when enabled - tests: cover config loading, extension wiring, and default-off behaviour
1 parent f066aeb commit e13dbb5

4 files changed

Lines changed: 198 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ wiki = [
6060
"sphinx-markdown-builder>=0.6.9",
6161

6262
]
63+
llms = [
64+
"sphinx-llm>=0.4.1",
65+
]
6366
themes = [
6467
"shibuya",
6568
"sphinxawesome-theme",
@@ -84,6 +87,7 @@ develop = [
8487
"sphinx-rust",
8588
"sphinx-js>=5.0.0",
8689
"sphinx-markdown-builder>=0.6.9",
90+
"sphinx-llm>=0.4.1",
8791
# Themes
8892
"shibuya",
8993
"sphinxawesome-theme",
@@ -223,3 +227,10 @@ generate-footer = true
223227
footer-docs-url = "https://yardang.python-templates.dev"
224228
footer-repo-url = "https://github.com/python-project-templates/yardang"
225229
markdown-flavor = "github"
230+
231+
[tool.yardang.llms]
232+
enabled = true
233+
description = "Easily generate sphinx documentation"
234+
build-parallel = true
235+
suffix-mode = "auto"
236+
full-build = true

yardang/build.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,31 @@ def customize(args):
520520
# Determine if wiki/markdown output should be generated
521521
use_wiki = wiki_args["wiki_enabled"]
522522

523+
# Load sphinx-llm (llms.txt) configuration from tool.yardang.llms
524+
llms_config_base = f"{config_base}.llms"
525+
llms_args = {}
526+
for config_option, default in {
527+
# yardang gate
528+
"llms_enabled": False,
529+
# sphinx-llm passthrough options
530+
"llms_txt_description": "",
531+
"llms_txt_build_parallel": True,
532+
"llms_txt_suffix_mode": "auto",
533+
"llms_txt_full_build": True,
534+
}.items():
535+
# config keys in toml use hyphens, not underscores, and drop the
536+
# llms_/llms_txt_ prefix
537+
if config_option.startswith("llms_txt_"):
538+
toml_key = config_option.replace("llms_txt_", "").replace("_", "-")
539+
else:
540+
toml_key = config_option.replace("llms_", "").replace("_", "-")
541+
llms_args[config_option] = get_config(section=toml_key, base=llms_config_base)
542+
if llms_args[config_option] is None:
543+
llms_args[config_option] = default
544+
545+
# Determine if llms.txt output should be generated
546+
use_llms = llms_args["llms_enabled"]
547+
523548
# create a temporary directory to store the conf.py file in
524549
with TemporaryDirectory() as td:
525550
templateEnv = Environment(loader=FileSystemLoader(searchpath=str(Path(__file__).parent.resolve())))
@@ -545,10 +570,12 @@ def customize(args):
545570
use_sphinx_rust=use_sphinx_rust,
546571
use_sphinx_js=use_sphinx_js,
547572
use_wiki=use_wiki,
573+
use_llms=use_llms,
548574
**breathe_args,
549575
**rust_args,
550576
**js_args,
551577
**wiki_args,
578+
**llms_args,
552579
**configuration_args,
553580
)
554581

yardang/conf.py.j2

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use_breathe = {{use_breathe}} # noqa: F821
4545
use_sphinx_rust = {{use_sphinx_rust}} # noqa: F821
4646
use_sphinx_js = {{use_sphinx_js}} # noqa: F821
4747
use_wiki = {{use_wiki}} # noqa: F821
48+
use_llms = {{use_llms}} # noqa: F821
4849

4950
#############################################################################################################
5051
# _____ _ _ _ __ _ _ #
@@ -89,6 +90,10 @@ if use_sphinx_js:
8990
if use_wiki:
9091
extensions.append("sphinx_markdown_builder")
9192

93+
# Add sphinx-llm extension if llms.txt generation is enabled
94+
if use_llms:
95+
extensions.append("sphinx_llm.txt")
96+
9297
if use_autoapi in (True, None):
9398
# add if it is set to true or if it is set to None
9499
# NOTE: bug in autoapi that requires
@@ -232,6 +237,15 @@ if use_wiki:
232237
markdown_bullet = "{{markdown_bullet}}"
233238
markdown_flavor = "{{markdown_flavor}}"
234239

240+
# sphinx-llm (llms.txt) configuration
241+
if use_llms:
242+
{% if llms_txt_description %}
243+
llms_txt_description = """{{llms_txt_description}}"""
244+
{% endif %}
245+
llms_txt_build_parallel = {{llms_txt_build_parallel}}
246+
llms_txt_suffix_mode = "{{llms_txt_suffix_mode}}"
247+
llms_txt_full_build = {{llms_txt_full_build}}
248+
235249
# autosummary
236250
autosummary_generate = True # if using autosummary, autogenerate
237251

yardang/tests/test_llms.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Tests for llms.txt generation (sphinx-llm) in yardang."""
2+
3+
import os
4+
from pathlib import Path
5+
6+
7+
class TestLlmsConfiguration:
8+
"""Tests for llms.txt configuration loading and generation."""
9+
10+
def test_llms_config_loading_from_pyproject(self, tmp_path):
11+
"""Test that llms configuration is loaded from pyproject.toml."""
12+
pyproject_content = """
13+
[project]
14+
name = "test-project"
15+
version = "1.0.0"
16+
17+
[tool.yardang]
18+
title = "Test Project"
19+
root = "README.md"
20+
use-autoapi = false
21+
22+
[tool.yardang.llms]
23+
enabled = true
24+
description = "A project for LLMs"
25+
build-parallel = false
26+
suffix-mode = "replace"
27+
full-build = false
28+
"""
29+
pyproject_path = tmp_path / "pyproject.toml"
30+
pyproject_path.write_text(pyproject_content)
31+
32+
readme_path = tmp_path / "README.md"
33+
readme_path.write_text("# Test Project\n\nTest content.")
34+
35+
original_cwd = os.getcwd()
36+
try:
37+
os.chdir(tmp_path)
38+
39+
from yardang.utils import get_config
40+
41+
assert get_config(section="enabled", base="tool.yardang.llms") is True
42+
assert get_config(section="description", base="tool.yardang.llms") == "A project for LLMs"
43+
assert get_config(section="build-parallel", base="tool.yardang.llms") is False
44+
assert get_config(section="suffix-mode", base="tool.yardang.llms") == "replace"
45+
assert get_config(section="full-build", base="tool.yardang.llms") is False
46+
finally:
47+
os.chdir(original_cwd)
48+
49+
def test_llms_config_defaults(self, tmp_path):
50+
"""Test that llms configuration returns None when not specified."""
51+
pyproject_content = """
52+
[project]
53+
name = "test-project"
54+
version = "1.0.0"
55+
56+
[tool.yardang]
57+
title = "Test Project"
58+
root = "README.md"
59+
"""
60+
pyproject_path = tmp_path / "pyproject.toml"
61+
pyproject_path.write_text(pyproject_content)
62+
63+
readme_path = tmp_path / "README.md"
64+
readme_path.write_text("# Test Project\n\nTest content.")
65+
66+
original_cwd = os.getcwd()
67+
try:
68+
os.chdir(tmp_path)
69+
70+
from yardang.utils import get_config
71+
72+
assert get_config(section="enabled", base="tool.yardang.llms") is None
73+
assert get_config(section="suffix-mode", base="tool.yardang.llms") is None
74+
finally:
75+
os.chdir(original_cwd)
76+
77+
def test_generate_docs_with_llms_enabled(self, tmp_path):
78+
"""Test that generate_docs_configuration wires in the sphinx-llm extension."""
79+
pyproject_content = """
80+
[project]
81+
name = "test-project"
82+
version = "1.0.0"
83+
84+
[tool.yardang]
85+
title = "Test Project"
86+
root = "README.md"
87+
use-autoapi = false
88+
89+
[tool.yardang.llms]
90+
enabled = true
91+
description = "A project for LLMs"
92+
suffix-mode = "replace"
93+
"""
94+
pyproject_path = tmp_path / "pyproject.toml"
95+
pyproject_path.write_text(pyproject_content)
96+
97+
readme_path = tmp_path / "README.md"
98+
readme_path.write_text("# Test Project\n\nTest content.")
99+
100+
original_cwd = os.getcwd()
101+
try:
102+
os.chdir(tmp_path)
103+
104+
from yardang.build import generate_docs_configuration
105+
106+
with generate_docs_configuration() as conf_dir:
107+
conf_content = (Path(conf_dir) / "conf.py").read_text()
108+
109+
assert "use_llms = True" in conf_content
110+
assert 'extensions.append("sphinx_llm.txt")' in conf_content
111+
assert "llms_txt_suffix_mode" in conf_content
112+
assert "replace" in conf_content
113+
assert "A project for LLMs" in conf_content
114+
finally:
115+
os.chdir(original_cwd)
116+
117+
def test_generate_docs_llms_disabled_by_default(self, tmp_path):
118+
"""Test that llms.txt generation is off unless explicitly enabled."""
119+
pyproject_content = """
120+
[project]
121+
name = "test-project"
122+
version = "1.0.0"
123+
124+
[tool.yardang]
125+
title = "Test Project"
126+
root = "README.md"
127+
use-autoapi = false
128+
"""
129+
pyproject_path = tmp_path / "pyproject.toml"
130+
pyproject_path.write_text(pyproject_content)
131+
132+
readme_path = tmp_path / "README.md"
133+
readme_path.write_text("# Test Project\n\nTest content.")
134+
135+
original_cwd = os.getcwd()
136+
try:
137+
os.chdir(tmp_path)
138+
139+
from yardang.build import generate_docs_configuration
140+
141+
with generate_docs_configuration() as conf_dir:
142+
conf_content = (Path(conf_dir) / "conf.py").read_text()
143+
144+
assert "use_llms = False" in conf_content
145+
finally:
146+
os.chdir(original_cwd)

0 commit comments

Comments
 (0)