From 75eb76a513f80fb99d59edbf3a373ed6e87b17ed Mon Sep 17 00:00:00 2001 From: Stuart Mumford Date: Tue, 14 Oct 2025 15:31:26 +0100 Subject: [PATCH 1/3] Explicit priority with sphinx-gallery --- src/sphinx_tags/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sphinx_tags/__init__.py b/src/sphinx_tags/__init__.py index 95e4e73..a23f47a 100644 --- a/src/sphinx_tags/__init__.py +++ b/src/sphinx_tags/__init__.py @@ -453,10 +453,9 @@ def setup(app): ) # Update tags - # TODO: tags should be updated after sphinx-gallery is generated, and the - # gallery is also connected to builder-inited. Are there situations when - # this will not work? - app.connect("builder-inited", update_tags) + # tags should be updated after sphinx-gallery is generated, and the + # sphinx-gallery plugin uses default priority so we use a higher one + app.connect("builder-inited", update_tags, priority=1000) app.add_directive("tags", TagLinks) return { From 5685bd93a42e79f3f46900ea16cb8172d03ccd08 Mon Sep 17 00:00:00 2001 From: Stuart Mumford Date: Tue, 14 Oct 2025 16:09:06 +0100 Subject: [PATCH 2/3] Add a test --- test/test_general_tags.py | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/test_general_tags.py b/test/test_general_tags.py index a50081f..25caea0 100644 --- a/test/test_general_tags.py +++ b/test/test_general_tags.py @@ -106,3 +106,43 @@ def test_empty_taglinks(): msg = "No tags passed to 'tags' directive" with pytest.raises(ExtensionError, match=msg): tag_links.run() + + +@pytest.mark.sphinx("text", testroot="rst", confoverrides={"tags_create_tags": True}) +def test_tag_page_cache(app: SphinxTestApp): + """ + This test attempts to check that when doing an incremental rebuild + of sphinx the built tags are updated. + + We do a build, then modify the tag list in one of the tag files + and it should change the output on the second build. + """ + app.build(force_all=True) + build_dir = Path(app.srcdir) / "_build" / "text" + + # Check all expected tag pages + for tag in ["tag_1", "tag2", "tag-3", "tag-4", "tag_5", "test-tag-please-ignore"]: + contents = build_dir / "_tags" / f"{tag}.txt" + expected_contents = OUTPUT_DIR / "_tags" / f"{tag}.txt" + with open(contents, "r") as actual, open(expected_contents, "r") as expected: + assert actual.readlines() == expected.readlines() + + # Modify a source file + with open(Path(app.srcdir) / "page_1.rst") as fobj: + contents = fobj.readlines() + contents[-1] = ".. tags:: tag_1, tag2" + + with open(Path(app.srcdir) / "page_1.rst", mode="w+") as fobj: + fobj.writelines(contents) + + app.build(force_all=True) + build_dir = Path(app.srcdir) / "_build" / "text" + + # Check all expected tag pages + for tag in ["tag-3", "tag-4"]: + contents = build_dir / "_tags" / f"{tag}.txt" + expected_contents = OUTPUT_DIR / "_tags" / f"{tag}.txt" + with open(contents, "r") as actual, open(expected_contents, "r") as expected: + actual_lines = actual.readlines() + assert "* Page 1\n" not in actual_lines + assert actual_lines != expected.readlines() From c79641053c4ad7d2948eb59ccae47bf4f279b3a8 Mon Sep 17 00:00:00 2001 From: Stuart Mumford Date: Tue, 14 Oct 2025 16:14:29 +0100 Subject: [PATCH 3/3] Support rebuilding modified (not new) tag pages on incremental builds --- src/sphinx_tags/__init__.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/sphinx_tags/__init__.py b/src/sphinx_tags/__init__.py index a23f47a..0db2f04 100644 --- a/src/sphinx_tags/__init__.py +++ b/src/sphinx_tags/__init__.py @@ -192,6 +192,10 @@ def create_file( tag_intro_text: str the words after which the tags of a given page are listed (e.g. "Tags: programming, python") + Returns + ------- + filename : str + The path of the generated file. """ # Get sorted file paths for tag pages, relative to /docs/_tags @@ -228,10 +232,10 @@ def create_file( content.append(f" ../{path}") content.append("") - with open( - os.path.join(srcdir, tags_output_dir, filename), "w", encoding="utf8" - ) as f: + output_filepath = os.path.join(srcdir, tags_output_dir, filename) + with open(output_filepath, "w", encoding="utf8") as f: f.write("\n".join(content)) + return output_filepath class Entry: @@ -316,6 +320,12 @@ def tagpage(tags, outdir, title, extension, tags_index_head): This page contains a list of all available tags. + Returns + ------- + + filename : str + Filename of generated file. + """ tags = list(tags.values()) @@ -365,6 +375,8 @@ def tagpage(tags, outdir, title, extension, tags_index_head): with open(filename, "w", encoding="utf8") as f: f.write("\n".join(content)) + return filename + def assign_entries(app): """Assign all found entries to their tag.""" @@ -388,6 +400,7 @@ def assign_entries(app): def update_tags(app): """Update tags according to pages found""" + generated_files = [] if app.config.tags_create_tags: tags_output_dir = Path(app.config.tags_output_dir) @@ -402,7 +415,7 @@ def update_tags(app): tags, pages = assign_entries(app) for tag in tags.values(): - tag.create_file( + filepath = tag.create_file( [item for item in pages if tag.name in item.tags], app.config.tags_extension, tags_output_dir, @@ -410,20 +423,33 @@ def update_tags(app): app.config.tags_page_title, app.config.tags_page_header, ) + generated_files.append(filepath) # Create tags overview page - tagpage( + filepath = tagpage( tags, os.path.join(app.srcdir, tags_output_dir), app.config.tags_overview_title, app.config.tags_extension, app.config.tags_index_head, ) + generated_files.append(filepath) logger.info("Tags updated", color="white") else: logger.info( "Tags were not created (tags_create_tags=False in conf.py)", color="white" ) + return [os.path.relpath(gf.split(".")[0], app.srcdir) for gf in generated_files] + + +def refresh_tags_on_incremental_build(app, env, added, changed, removed): + generated_files = set(update_tags(app)) + if new_files := generated_files.difference(env.found_docs): + logger.warning( + "The following new tag files were generated on an incremental build, they will not be built by sphinx %s", + new_files, + ) + return generated_files def setup(app): @@ -456,6 +482,7 @@ def setup(app): # tags should be updated after sphinx-gallery is generated, and the # sphinx-gallery plugin uses default priority so we use a higher one app.connect("builder-inited", update_tags, priority=1000) + app.connect("env-get-outdated", refresh_tags_on_incremental_build) app.add_directive("tags", TagLinks) return {