Sphinx扩展中获取的Toctree对象过时,热更新无法同步最新内容的问题求助
我写了一个Sphinx扩展,用来把文档的全局toctree替换成指定名称的toctree。现在遇到一个棘手的问题:设置了自定义toctree的页面标记是正常的,但当我在另一个.rst文件里更新实际的toctree对象时,render_named_toctree函数里获取到的toc_tree节点始终是旧版本,热更新没办法同步最新内容。
核心的render_named_toctree函数代码如下:
def render_named_toctree( app: Sphinx, docname: str, tocname: str, collapse: bool = True, **kwargs: Any ) -> str: builder = app.builder maxdepth = kwargs.get('maxdepth',0) includehidden = kwargs.get("includehidden", False) titles_only = kwargs.get("titles_only", False) toc_env = app.env.master_doctree.ids[tocname] toc_tree = toc_env.children[0] # 我也尝试过用遍历的方式查找,结果还是一样 # for toctree_node in app.env.master_doctree.findall(addnodes.toctree): # if tocname in toctree_node.parent.attributes["ids"]: # toc_tree = toctree_node resolved = _resolve_toctree(app.env, docname, builder, toc_tree, prune=True, collapse=collapse, maxdepth=int(maxdepth), includehidden=includehidden, titles_only=titles_only) if not resolved: resolved = None return builder.render_partial(resolved)["fragment"]
我搭了一个测试文档的小项目,当前的目录结构(忽略build文件)是这样的:
├───src │ └───sphinx_replace_htmlpage_toctree │ __init__.py │ └───test │ conf.py │ index.rst └───tutorial foo.rst index.rst
测试用的文档内容很简单,tutorial/index.rst和tutorial/foo.rst只有标题,主要看index.rst里的两个toctree:
Docs ===== Test document to see if the toctree changes .. toctree:: :name: maintoc self tutorial/index .. toctree:: :name: tutorialtree tutorial/index tutorial/foo
conf.py里的配置用来给tutorial/index.rst指定自定义toctree,内容如下:
project = 'replace toctree tester' copyright = '2024, Foo' author = 'Bar' release = '0.0.1' html_theme = 'alabaster' extensions = [ "sphinx_replace_htmlpage_toctree" ] replace_global_tocs = { "tutorial/*": "tutorialtree", }
我用这个命令启动热更新测试:
sphinx-autobuild test test/build/html -a
现在的问题是:当我修改index.rst里的tutorialtree(比如移除某个条目),网页上的导航树完全没变化,查看到render_named_toctree里拿到的还是旧的toctree内容。只有加上-E flag重新全量构建才会生效,但这样构建时间太长了,按理应该不用全量构建就能同步的。
我怀疑是没正确告诉Sphinx某个文档已更新,导致它用了旧的pickled doctree,而不是新的。我尝试过在多个Sphinx事件里标记index和tutorial/index为需要更新,比如连接了这些事件:
env-get-updatedenv-updatedenv-before-read-docsenv-get-outdated
但不管用,获取到的toctree还是旧的。
下面是扩展__init__.py的完整代码,供参考:
from typing import * import re from docutils import nodes from sphinx import addnodes from sphinx.util import logging from sphinx.application import Sphinx from sphinx.util.typing import ExtensionMetadata from sphinx.util.docutils import SphinxDirective from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.environment.adapters.toctree import _resolve_toctree from sphinx.environment import BuildEnvironment __version__ = "0.0.1" _LOGGER = logging.getLogger(__name__) EXTENSION_NAME = "replace-htmlpage-toctree" TOCLIST_ATTR = "replace_docname_toctree" def update_toctree_context(app: Sphinx, pagename, templatename, context, doctree): """Updates the toctree key in the context object if the pagename has a custom toc """ if pagename in app.env.replace_docname_toctree: context["toctree"] = lambda **kwargs: render_named_toctree(app, pagename, app.env.replace_docname_toctree[pagename], **kwargs) return def render_named_toctree( app: Sphinx, docname: str, tocname: str, collapse: bool = True, **kwargs: Any ) -> str: builder = app.builder maxdepth = kwargs.get('maxdepth',0) includehidden = kwargs.get("includehidden", False) titles_only = kwargs.get("titles_only", False) toc_env = app.env.master_doctree.ids[tocname] toc_tree = toc_env.children[0] # for toctree_node in app.env.master_doctree.findall(addnodes.toctree): # if tocname in toctree_node.parent.attributes["ids"]: # toc_tree = toctree_node resolved = _resolve_toctree(app.env, docname, builder, toc_tree, prune=True, collapse=collapse, maxdepth=int(maxdepth), includehidden=includehidden, titles_only=titles_only) if not resolved: resolved = None return builder.render_partial(resolved)["fragment"] def isold(app, env, added, changed, removed): return {"tutorial/index", "index"} def process_namedtocs(app: Sphinx, env: BuildEnvironment): app.env.replace_docname_toctree = {} for doc, treename in app.config.replace_global_tocs.items(): env_ids = app.env.master_doctree.ids if treename not in env_ids: _LOGGER.error(f"No toctree with name {treename} was found") continue named_toc = env_ids[treename] if "toctree-wrapper" not in named_toc.attributes["classes"]: _LOGGER.error(f"{treename} is not a toctree") continue if "*" in doc: expr = re.compile(doc) for found_doc in env.found_docs: if expr.match(found_doc): app.env.replace_docname_toctree[found_doc] = treename elif doc in env.found_docs: app.env.replace_docname_toctree[doc] = treename else: _LOGGER.error(f"{EXTENSION_NAME}: {doc} was not found and is not an expression.") return ["tutorial/index", "index"] def before_read(app: Sphinx, env: BuildEnvironment, docnames: list): if "tutorial/index" not in docnames: docnames.append("tutorial/index") if "index" not in docnames: docnames.append("index") return def return_updated(app, env): return ["tutorial/index", "index"] def setup(app: Sphinx) -> ExtensionMetadata: app.add_config_value('replace_global_tocs', {}, 'env') app.connect('html-page-context', update_toctree_context) app.connect('env-get-updated', process_namedtocs) app.connect("env-updated", return_updated) app.connect("env-get-outdated", isold) return ExtensionMetadata(version=__version__, parallel_read_safe=False, parallel_write_safe=False)
备注:内容来源于stack exchange,提问作者Slalamander

