diff --git a/pyproject.toml b/pyproject.toml index 5cafef0..c3b7521 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dev = [ "mypy", "isort", "pre-commit", + "sphinx", ] test = [ "pytest>=7.0.0", @@ -84,4 +85,5 @@ filterwarnings = [ "error", "ignore::UserWarning", "ignore::DeprecationWarning", + "ignore::PendingDeprecationWarning", ] \ No newline at end of file diff --git a/sphinx_llms_txt/__init__.py b/sphinx_llms_txt/__init__.py index 806b470..bd1c7e1 100644 --- a/sphinx_llms_txt/__init__.py +++ b/sphinx_llms_txt/__init__.py @@ -74,7 +74,9 @@ class LLMSFullManager: if toctree: from docutils import nodes - for node in toctree.traverse(nodes.reference): + # Use findall() instead of traverse() to avoid deprecation warning + # Convert generator to list for iteration + for node in list(toctree.findall(nodes.reference)): if "refuri" in node.attributes: refuri = node.attributes["refuri"] if refuri and refuri.endswith(".html"): @@ -239,9 +241,10 @@ def doctree_resolved(app: Sphinx, doctree, docname: str): from docutils import nodes title = None - for node in doctree.traverse(nodes.title): - title = node.astext() - break + # findall() returns a generator, convert to list to check if it has elements + title_nodes = list(doctree.findall(nodes.title)) + if title_nodes: + title = title_nodes[0].astext() if title: _manager.update_page_title(docname, title) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5fd6804 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,49 @@ +"""Pytest configuration for sphinx-llms-txt.""" + +import os +import shutil +import tempfile +from pathlib import Path + +import pytest + +# Use Path directly instead of sphinx_path to avoid deprecation warning +from sphinx.testing.util import SphinxTestApp + + +@pytest.fixture +def rootdir(): + """Get the root directory for test projects.""" + return Path(os.path.dirname(__file__) or ".").absolute() / "roots" + + +@pytest.fixture +def temp_dir(): + """Create a temporary directory and delete it after the test.""" + temp_path = Path(tempfile.mkdtemp()) + yield temp_path + shutil.rmtree(temp_path, ignore_errors=True) + + +@pytest.fixture +def basic_sphinx_app(temp_dir, rootdir): + """Create a basic Sphinx app for testing.""" + src_dir = rootdir / "basic" + app = SphinxTestApp( + srcdir=src_dir, + builddir=temp_dir, + buildername="html", + freshenv=True, + ) + yield app + + # Custom cleanup to avoid missing_ok issue + import sys + from sphinx.testing.util import _clean_up_global_state + + sys.path[:] = app._saved_path + _clean_up_global_state() + + # Safe unlink that works with older Python versions + if hasattr(app, "docutils_conf_path") and app.docutils_conf_path.exists(): + app.docutils_conf_path.unlink() diff --git a/tests/roots/basic/conf.py b/tests/roots/basic/conf.py new file mode 100644 index 0000000..5c792e5 --- /dev/null +++ b/tests/roots/basic/conf.py @@ -0,0 +1,22 @@ +"""Configuration file for the basic Sphinx project.""" + +project = "Test Project" +copyright = "2025, Test" +author = "Test" + +extensions = [ + "sphinx_llms_txt", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +html_theme = "alabaster" +html_static_path = ["_static"] + +# Configuration for sphinx-llms-txt +llms_txt_filename = "test-llms-full.txt" +llms_txt_verbose = True + +# Master document +master_doc = "index" diff --git a/tests/roots/basic/conf_custom.py b/tests/roots/basic/conf_custom.py new file mode 100644 index 0000000..ffa6c3f --- /dev/null +++ b/tests/roots/basic/conf_custom.py @@ -0,0 +1,22 @@ +"""Configuration file for the basic Sphinx project.""" + +project = "Test Project" +copyright = "2025, Test" +author = "Test" + +extensions = [ + "sphinx_llms_txt", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +html_theme = "alabaster" +html_static_path = ["_static"] + +# Configuration for sphinx-llms-txt +llms_txt_filename = "custom-name.txt" +llms_txt_verbose = True + +# Master document +master_doc = "index" diff --git a/tests/roots/basic/index.rst b/tests/roots/basic/index.rst new file mode 100644 index 0000000..1c63d95 --- /dev/null +++ b/tests/roots/basic/index.rst @@ -0,0 +1,16 @@ +Welcome to Test Project's documentation! +===================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + page1 + page2 + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` \ No newline at end of file diff --git a/tests/roots/basic/page1.rst b/tests/roots/basic/page1.rst new file mode 100644 index 0000000..a62203b --- /dev/null +++ b/tests/roots/basic/page1.rst @@ -0,0 +1,14 @@ +Page 1 Title +=========== + +This is the content of page 1. + +Section 1 +--------- + +Content for section 1. + +Section 2 +--------- + +Content for section 2. \ No newline at end of file diff --git a/tests/roots/basic/page2.rst b/tests/roots/basic/page2.rst new file mode 100644 index 0000000..fee99fc --- /dev/null +++ b/tests/roots/basic/page2.rst @@ -0,0 +1,14 @@ +Page 2 Title +=========== + +This is the content of page 2. + +Section A +--------- + +Content for section A. + +Section B +--------- + +Content for section B. \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..e2016b9 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,69 @@ +"""Integration tests for sphinx-llms-txt.""" + +from pathlib import Path + + +def test_build_html_with_llms_txt(basic_sphinx_app): + """Test building HTML documentation with llms-txt enabled.""" + app = basic_sphinx_app + app.build() + + # Check if the output file was created + output_file = Path(app.outdir) / "test-llms-full.txt" + assert output_file.exists(), f"Output file {output_file} does not exist" + + # Read the content of the output file + content = output_file.read_text() + + # Check that content from all pages is included + assert "Welcome to Test Project's documentation!" in content + assert "Page 1 Title" in content + assert "Page 2 Title" in content + assert "Content for section 1" in content + assert "Content for section A" in content + + +def test_custom_filename(temp_dir, rootdir): + """Test using a custom filename for the output.""" + from sphinx.testing.util import SphinxTestApp + + src_dir = rootdir / "basic" + + # Create a copy of the configuration with a different filename + custom_conf = src_dir / "conf_custom.py" + with open(src_dir / "conf.py") as f: + conf_content = f.read() + + conf_content = conf_content.replace( + 'llms_txt_filename = "test-llms-full.txt"', + 'llms_txt_filename = "custom-name.txt"', + ) + + with open(custom_conf, "w") as f: + f.write(conf_content) + + # Create a new test app with the custom configuration + app = SphinxTestApp( + srcdir=src_dir, + builddir=temp_dir, + buildername="html", + freshenv=True, + confoverrides={"llms_txt_filename": "custom-name.txt"}, + ) + + app.build() + + # Check if the output file with the custom name was created + output_file = Path(app.outdir) / "custom-name.txt" + assert output_file.exists(), f"Output file {output_file} does not exist" + + # Custom cleanup to avoid missing_ok issue + import sys + from sphinx.testing.util import _clean_up_global_state + + sys.path[:] = app._saved_path + _clean_up_global_state() + + # Safe unlink that works with older Python versions + if hasattr(app, "docutils_conf_path") and app.docutils_conf_path.exists(): + app.docutils_conf_path.unlink() diff --git a/tests/test_llms_txt.py b/tests/test_llms_txt.py new file mode 100644 index 0000000..bc71f75 --- /dev/null +++ b/tests/test_llms_txt.py @@ -0,0 +1,82 @@ +"""Test the sphinx_llms_txt extension.""" + +from sphinx_llms_txt import LLMSFullManager, setup + + +def test_version(): + """Test that the version is defined.""" + from sphinx_llms_txt import __version__ + + assert __version__ + + +def test_setup_returns_valid_dict(): + """Test that the setup function returns a valid dict.""" + + # Mock a Sphinx app + class MockApp: + def __init__(self): + self.config_values = {} + self.connections = {} + + def add_config_value(self, name, default, rebuild): + self.config_values[name] = (default, rebuild) + + def connect(self, event, handler): + self.connections[event] = handler + + app = MockApp() + result = setup(app) + + # Check that result is a dict + assert isinstance(result, dict) + assert "version" in result + assert "parallel_read_safe" in result + assert "parallel_write_safe" in result + + +def test_llms_full_manager_initialization(): + """Test initialization of LLMSFullManager.""" + manager = LLMSFullManager() + assert manager.page_titles == {} + assert manager.config == {} + assert manager.master_doc is None + assert manager.env is None + + +def test_manager_page_title_update(): + """Test updating page titles.""" + manager = LLMSFullManager() + manager.update_page_title("doc1", "Title 1") + manager.update_page_title("doc2", "Title 2") + + assert manager.page_titles["doc1"] == "Title 1" + assert manager.page_titles["doc2"] == "Title 2" + + +def test_set_config(): + """Test setting configuration.""" + manager = LLMSFullManager() + config = { + "llms_txt_filename": "custom.txt", + "llms_txt_verbose": True, + } + manager.set_config(config) + assert manager.config == config + + +def test_set_master_doc(): + """Test setting master doc.""" + manager = LLMSFullManager() + manager.set_master_doc("index") + assert manager.master_doc == "index" + + +def test_empty_page_order(): + """Test get_page_order returns empty list when env or master_doc not set.""" + manager = LLMSFullManager() + assert manager.get_page_order() == [] + + # Set only master_doc, but not env + manager.set_master_doc("index") + assert manager.get_page_order() == []