taolib.doc 源代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""文档构建相关任务模块。

包含文档清理、构建、国际化、测试等相关任务以及文档站点配置集合创建功能。"""

from pathlib import Path
import sys
import logging
from shutil import rmtree
from tempfile import mkdtemp
from invoke.tasks import task
from invoke.context import Context
from invoke.collection import Collection


[文档] def _get_sphinx_paths(ctx: Context, source: str | None, target: str | None) -> tuple[str, str]: """安全地从上下文获取 sphinx 的 source/target 路径,若没有 ctx.sphinx 则使用默认值。""" sphinx_cfg = getattr(ctx, 'sphinx', None) src = source or (getattr(sphinx_cfg, "source", "doc") if sphinx_cfg else "doc") dst = target or (getattr(sphinx_cfg, "target", "doc/_build/html") if sphinx_cfg else "doc/_build/html") return str(src), str(dst)
[文档] logger = logging.getLogger(__name__)
# 在非 Windows 平台上使用 pty
[文档] PTY = sys.platform != 'win32'
@task
[文档] def clean(ctx: Context) -> None: """清除文档构建目标目录,以便下次构建是干净的。 Args: ctx: Invoke 上下文对象,包含 sphinx 配置信息""" # 尝试通过 _get_sphinx_paths 获取目标(更稳健) try: _, target = _get_sphinx_paths(ctx, None, None) except Exception: # 回退到旧的属性访问方式,但要安全地处理缺失的配置 sphinx_cfg = getattr(ctx, "sphinx", None) target = getattr(sphinx_cfg, "target", None) if sphinx_cfg is not None else None if not target: logger.warning("无法确定 sphinx.target,跳过 clean 操作") return output = Path(target) if output.exists(): logger.info(f'删除 {output}') rmtree(output)
@task(default=True)
[文档] def build(ctx: Context, builder: str = "html", opts: str = "", language: str | None = None, source: str | None = None, target: str | None = None, nitpick: bool = False, jobs: str = "auto", keep_going: bool = True) -> None: """构建项目的 Sphinx 文档。 Args: ctx: Invoke 上下文对象,包含 sphinx 配置信息 builder: Sphinx 构建器类型,默认为 'html' opts: Sphinx 构建额外选项/参数 language: 文档语言,默认为 None source: 源目录,覆盖配置设置 target: 输出目录,覆盖配置设置 nitpick: 是否启用更严格的警告/错误检查""" source, target = _get_sphinx_paths(ctx, source, target) if language: opts = f"{opts} -D language={language}".strip() target = f"{target}/{language}" if nitpick: opts = f"{opts} -n -W -T".strip() opts = f"{opts} -j {jobs}".strip() if keep_going: opts = f"{opts} --keep-going".strip() # 执行构建命令 cmd = f"sphinx-build -b {builder} {opts} {source} {target}" logger.info(f"{builder}@{source} => {target}") try: ctx.run(cmd, pty=PTY) except ValueError as e: # 在 Windows 上,Invoke 处理 KeyboardInterrupt 时可能会遇到 stdin 已关闭的情况 # 此时会抛出 ValueError: I/O operation on closed file if sys.platform == 'win32' and "closed file" in str(e): logger.warning("构建被用户中断") else: raise except KeyboardInterrupt: logger.warning("构建被用户中断")
@task
[文档] def intl(ctx: Context, language: str = 'en') -> None: """更新 POT 文件并调用 `sphinx-intl update` 命令。 用于更新多语言文档翻译。 Args: ctx: Invoke 上下文对象,包含 sphinx 配置信息 language: 目标语言代码,默认为 'en'(英语)""" opts = "-b gettext" _, configured_target = _get_sphinx_paths(ctx, None, None) target = Path(configured_target).parent / 'gettext' if language == 'en': if target.exists(): rmtree(target) build(ctx, target=str(target), opts=opts) elif language: if not target.exists(): build(ctx, target=str(target), opts=opts) ctx.run(f'sphinx-intl update -p {str(target)} -l {language}')
# 以下代码已注释掉,因为当前项目可能不需要 # for DIR in ['pages', 'posts', 'shop']: # rmtree(f'locales/{language}/LC_MESSAGES/{DIR}/') @task
[文档] def doctest(ctx: Context) -> None: """运行 Sphinx 的 doctest 构建器进行文档测试。 这将像测试运行一样,显示测试结果,如果所有测试未通过,则以非零状态退出。 使用临时目录作为构建目标,因为唯一的输出是自动打印的文本文件。 Args: ctx: Invoke 上下文对象,包含 sphinx 配置信息""" tmpdir = mkdtemp() try: opts = "-b doctest" target = tmpdir # 使用临时目录进行 doctest 构建。不要清理常规模板输出目录以避免意外删除用户构建结果。 logger.info(f"使用临时目录进行 doctest: {tmpdir}") build(ctx, target=target, opts=opts) finally: try: rmtree(tmpdir) except Exception: logger.warning(f"无法删除临时目录 {tmpdir}")
@task
[文档] def tree(ctx: Context) -> None: ignore = ".git|*.pyc|*.swp|dist|*.egg-info|_static|_build|_templates" try: ctx.run(f'tree -Ca -I "{ignore}" {ctx.sphinx.source}', pty=PTY) except Exception: sphinx_cfg = getattr(ctx, 'sphinx', None) root = Path(getattr(sphinx_cfg, 'source', 'doc') if sphinx_cfg else 'doc') for p in sorted(root.rglob("*")): rel = p.relative_to(root) if any(part in ignore.split("|") for part in rel.parts): continue print(rel)
# 文档站点配置集合创建函数 from typing import Optional, List, Dict, Union, Any, overload # Collection is imported from invoke.collection at the top of the file @overload
[文档] def create_docs(source: str = 'doc', target: str = '.temp/html', children: str = '') -> Collection: ...
@overload def create_docs(source: Union[List[Dict[str, str]], Dict[str, Dict[str, str]]]) -> Collection: ... def create_docs( source: Union[str, List[Dict[str, str]], Dict[str, Dict[str, str]], None] = None, target: str = 'doc/_build/html', children: str = '' ) -> Collection: """创建文档站点配置集合。 统一的文档站点配置创建函数,支持单个项目和多个项目的配置。 Args: source: 文档源代码目录或项目配置。可以是: - 字符串:文档源代码目录,默认为 'doc' - 列表:多个项目配置的列表,每个元素是包含 source、target、name 的字典 - 字典:多个项目配置的字典,键为项目名称,值为包含 source、target 的字典 - None:使用默认值 'doc' target: 文档构建输出目录,仅在处理单个项目时有效,默认为 '.temp/html' children: 子文档目录,仅在处理单个项目时有效,如果指定则覆盖 source Returns: 配置好的 Invoke Collection 对象,包含 doc 子命令或多个命名的 doc 子命令集合""" # 内部 helper:生成并返回一个为单个项目配置的 Collection def _make_project_namespace(name: str, proj_source: str, proj_target: str, proj_children: str) -> Collection: project_namespace = Collection(name) # 使用 Collection.from_module 确保传入的是一个 Collection 对象(而不是 module) project_namespace.add_collection(Collection.from_module(sys.modules[__name__])) actual_source = proj_children if proj_children else proj_source project_config = { "sphinx": { "source": actual_source, "target": f"{proj_target}/{proj_children}" if proj_children else proj_target } } project_namespace.collections[__name__.split('.')[-1]].configure(project_config) # type: ignore[attr-defined] return project_namespace # 处理单个项目的情况 if source is None or isinstance(source, str): # 确保 source 有值 actual_source_str = source if source is not None else 'doc' actual_source = children if children else actual_source_str # 创建并配置命令集合(与以前保持行为一致) _config = { "sphinx": { "source": actual_source, "target": f"{target}/{children}" if children else target } } namespace = Collection() namespace.add_collection(Collection.from_module(sys.modules[__name__])) namespace.collections[__name__.split('.')[-1]].configure(_config) # type: ignore[attr-defined] return namespace # 处理多个项目的情况(列表或字典) project_configs = source # type: ignore main_namespace = Collection() if isinstance(project_configs, list): # 列表形式:需要包含 name 字段 for i, config in enumerate(project_configs): name = config.get('name', f'doc_{i+1}') proj_source = config.get('source', 'doc') proj_target = config.get('target', f'doc/_build/html/{name}') proj_children = config.get('children', '') project_namespace = _make_project_namespace(name, proj_source, proj_target, proj_children) main_namespace.add_collection(project_namespace) else: # 字典形式:键为项目名称 for name, config in project_configs.items(): proj_source = config.get('source', 'doc') proj_target = config.get('target', f'doc/_build/html/{name}') proj_children = config.get('children', '') project_namespace = _make_project_namespace(name, proj_source, proj_target, proj_children) main_namespace.add_collection(project_namespace) return main_namespace
[文档] def sites(source: str = 'doc', target: str = 'doc/_build/html', children: str = '') -> Collection: """创建文档站点配置集合。 为不同的文档站点创建配置好的 Invoke 集合,用于构建 Sphinx 文档。 Args: source: 文档源代码目录,默认为 'doc' target: 文档构建输出目录,默认为 '.temp/html' children: 子文档目录,如果指定则覆盖 source Returns: 配置好的 Invoke Collection 对象,包含 doc 子命令""" return create_docs(source, target, children)
[文档] def multi_sites(project_configs: Union[List[Dict[str, str]], Dict[str, Dict[str, str]]]) -> Collection: """创建多文档站点配置集合。 支持同时对多个 doc 项目进行构建的功能。 Args: project_configs: 项目配置列表或字典。可以是: - 列表形式:每个元素是包含 source、target、name 的字典 - 字典形式:键为项目名称,值为包含 source、target 的字典 Returns: 配置好的 Invoke Collection 对象,包含多个命名的 doc 子命令集合""" return create_docs(project_configs) # type: ignore