首页
/ PPT Master Python 代码风格指南:scripts 脚本层 15 条规范与仓库源码印证

PPT Master Python 代码风格指南:scripts 脚本层 15 条规范与仓库源码印证

2026-09-07 11:49:55作者:宣聪麟

导读:PPT Master 项目将全部可执行逻辑收敛在 skills/ppt-master/scripts/ 这一层扁平脚本目录中,为了让数十个相互导入的 CLI 工具保持一致的风格、可测试的入口和可维护的共享逻辑,仓库在 docs/rules/code-style.md 中沉淀了一套完整的 Python 代码风格规范。本文以该文档为骨架,逐条展开 15 类规则,并结合 total_md_split.pyimage_search.pyconsole_encoding.pybackend_common.py 等真实源码给出可验证的落地证据,帮助你快速写出符合仓库惯例的新脚本——无论它只是一个拆分 notes 的小工具,还是一个多后端图片搜索分发器。

规范来源与适用边界

这份风格指南不是从零撰写的"通用最佳实践",而是从仓库现有代码中提炼的 de facto 约定:文档开头明确说明它"Derived from the de facto patterns in the existing codebase",适用对象是 skills/ppt-master/scripts/ 下以及随技能一并分发的所有 Python 代码。

两条重要的认知前提需要先对齐:

  • 务实而非穷尽:规范只捕捉读者在实际代码中会遇到的那些约定,PEP 8 已经免费给出的一切(如缩进、空行、if 书写)视为默认前提,不再赘述。
  • 既有文件优先(见原文 §15):如果现存的某个脚本与指南中的某条规则冲突,正确的动作是二选一——要么更新这份指南,要么重构该脚本,而不是粗暴地把现有文件"一次性改造成规范形状"。文档还给出了一张"照着谁写"的范本对照表(详见 §15)。

1. 文件头:每个脚本都必须有的标准头

文档规定,scripts/ 下的每一个脚本都以 #!/usr/bin/env python3 shebang 开头,并紧随一个包含五要素的模块 docstring:

#!/usr/bin/env python3
"""
PPT Master - Short Tool Name

One-paragraph description of what this script does.

Usage:
    python3 scripts/<name>.py <required_arg> [options]

Examples:
    python3 scripts/<name>.py projects/<project_name> -o output_dir

Dependencies:
    None (only uses standard library)        <-- or list third-party deps
"""

对应规则速查:

Element Rule
Shebang #!/usr/bin/env python3(永远保留——即使对非 CLI 的辅助模块也不例外)
Module docstring 工具名 + 用途 + Usage + Examples + Dependencies
Internal helper modules 可以额外增加一个提前的 --help 短路分支(见 §4)

仓库中的范本 total_md_split.py 是这一规范的最直接样本:第一行 shebang,随后 docstring 写明了工具全名 PPT Master - Speaker Notes Splitting Tool、功能描述(把 total.md 演讲稿按 SVG 页拆分)、两条 Usage、一条 Examples,以及 Dependencies: None (only uses standard library) 的声明。连辅助模块也不能省略 shebang 和头部 docstring,例如纯被导入的 slide_roster.py 同样带着完整的三段式头部。

2. 导入:三段分组 + 长度/字母排序

导入区被强制分成三组,组间以空行分隔:

# 1. Standard library
import os
import sys
import argparse
import re
from pathlib import Path
from typing import Optional

# 2. Third-party
import requests

# 3. Local — sometimes need sys.path injection first (see §3)
from image_sources.provider_common import (
    AssetCandidate,
    ImageSearchRequest,
)
Rule Note
Group order std → third-party → local,组间留空行
Within a group 少于 4 个导入时按长度排序;≥ 4 个时按字母序
from x import lists 名字 ≥ 4 个时每行一个,且带尾随逗号
from __future__ import annotations 当文件使用了 PEP 604 的 X | Y 联合语法、且可能跑在 Python < 3.10 上时,置于文件最顶部

实际代码同样遵守了这条分组纪律:image_search.py 先集中导入标准库(argparseconcurrent.futuresdataclassespathlibtyping…),空行后单独一行 import requests,再做 sys.path 注入、然后导入本地模块,且第三方组与本地组各只有一个导入来源时也严格保持视觉上的分组边界。from __future__ import annotations 的使用也能在 backend_common.pyslide_roster.py 中看到——它们都在使用 list[str]int | str 这类现代语法前先做了兼容性声明。

3. sys.path 注入:扁平目录下的项目级约定

scripts/ 不是一个 Python 包,而是一个扁平的脚本目录。任何需要导入兄弟模块的入口脚本,都要自己把 scripts/ 注入到 sys.path 上:

import sys
from pathlib import Path

_SCRIPTS_DIR = Path(__file__).resolve().parent
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))

from image_backends.backend_common import download_image  # noqa: E402
Rule Why
只在入口脚本注入 image_sources/ / image_backends/ 这类库模块之间是正常相互导入,无需注入
使用 Path(__file__).resolve().parent 在符号链接与别名环境下依然稳健
注入后的导入标注 # noqa: E402 诚实地抑制 lint 警告,而不是对整个文件开 noqa

这份约定的执行细节在源码里非常统一。入口级脚本 image_search.py 在顶部就完成了"解析 __file__ 所在目录 → 注入 sys.path# noqa: E402 导入 configbackend_commonprovider_common"的整套动作。而目录嵌套更深的库模块 backend_common.py 则用 Path(__file__).resolve().parents[1] 向上回溯一层拿到 scripts/ 根再注入——这正是"入口注入、库模块按需自己补齐路径"的灵活之处。

4. CLI 入口:可测试的 main + 严格 help/参数校验

文档给出了推荐的入口范式:

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="One-line description.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("query", help="...")
    parser.add_argument("-o", "--output", default=".", help="...")
    return parser


def main(argv: Optional[list[str]] = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    # ... do the thing ...
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
Rule Note
main(argv=None) -> int 返回退出码;通过传入 argv 即可做可测试设计
raise SystemExit(main()) 优于 sys.exit(main())
formatter_class=argparse.RawDescriptionHelpFormatter 让 docstring 的多行排版原样呈现在 --help
Internal helpers --help 模块级提前判断:if __name__ == "__main__" and any(arg in {"-h", "--help", "help"} for arg in sys.argv[1:]): print(__doc__); raise SystemExit(0)
输出分流 进度/状态输出到 stderr;脚本的主产物(如有)才输出到 stdout

total_md_split.py 是这条规范的忠实执行者:build_parser 逻辑内联在 main() 中、使用 RawDescriptionHelpFormatter 并把长篇 epilog 塞进 help、位置参数 project_path 与选项 -o/--output-q/--quiet 一应俱全。而 backend_common.py 则示范了辅助模块被当作 CLI 直接执行时的兜底:命中 -h/--help/help 就打印 docstring 并以 0 退出,否则打印 "use via..." 提示并以非 0 退出,绝不会抛出一个导入 traceback

help 与参数校验的硬性要求

文档对 help/校验提出了五个必须满足的行为契约:

  • -h / --help 必须交给 argparse(首选)或显式的早期 guard 处理,且必须在任何副作用发生之前完成——不得先建目录、写文件、发起网络调用、安装包或启动长驻服务。
  • help 标志绝不能作为位置参数的值被吞掉。例如 init --help 不得创建一个名为 --help 的项目;export --help 不得写出名为 --help 的文件。
  • 带子命令的脚本使用 argparse subparsers,每个子命令都必须能独立 --help,并在干活前校验自己必需的参数。
  • 缺少必需参数、出现未知标志时,必须打印 usage/error 信息并以非 0 退出;不得静默忽略未知标志,也不得用部分默认值继续执行。
  • 仅用于诊断、可直接执行的内嵌辅助模块:非 help 调用要么运行一个已记录的诊断命令,要么打印一段简短的 "use via ..." 提示并以非 0 退出,不得以 import traceback 失败

控制台编码:configure_utf8_stdio()

每个可直接运行的入口脚本都必须在启动时调用一次 configure_utf8_stdio()(定义见 console_encoding.py),调用时机在首次面向用户的 print 之前、或导入任何可能打印的可选依赖之前:

  • 该函数会把 stdout / stderr 强制重配为 UTF-8 且 errors="replace",从而避免非 UTF-8 的 Windows locale(例如 GBK)在输出 Unicode 状态信息时崩溃;
  • 系统本身已是 UTF-8 时,它不改变有效编码;
  • 子目录脚本先按 §3 注入 scripts/ 根路径,再导入该 helper;
  • 纯库模块(没有 __main__ 入口)不需要调用它;文件级 I/O 仍然需要显式传 encoding="utf-8"(见 §13)——这条规则只管控制台。

从实现上看,console_encoding.py_reconfigure_stream() 首选调用 stream.reconfigure(encoding="utf-8", errors="replace"),对不支持 reconfigure 的流回退到用 io.TextIOWrapper 包住 stream.buffer,遇到 OSError / ValueError 时则原样返回流——典型的"尽力而为、绝不崩溃"策略。多个入口脚本在导入完兄弟模块后立刻调用它,例如 total_md_split.pyproject_utils.pyerror_helper.py

5. 类型标注:必填、现代、但禁止过度限定

所有新的公共函数都要求类型标注;内部 _helpers 可选。

Pattern Use
def f(x: str, *, y: int = 0) -> bool: 公共函数
tuple[int, int] | None PEP 604 联合类型(兼容性需要时配合 from __future__ import annotations
Optional[X](来自 typing X | None 的可接受替代
list[X]dict[K, V] 内置泛型(Python 3.9+)
Any 克制使用——仅在对接真正异构的数据时(如 dataclass 中存放上游 JSON 的 raw: Any

禁止过度限定(over-specification) 的两条红线:

  • Callable[[int, str], dict[str, list[Optional[Union[int, str]]]]] 这种写法是被禁止的——应拆成带类型的 dataclass;
  • 到处滥用 Literal["a", "b", "c"] 同样不被鼓励——除非类型本身就是这个 API 的语义,否则用一个常量 + 普通 str 即可。

仓库中可以看到相当一致的执行:total_md_split.py 的辅助函数签名都写明了参数与返回类型(如 def extract_leading_number(text: str) -> int | Nonedef build_match_maps(svg_stems: list[str]) -> tuple[set[str], dict[str, list[str]], dict[int, list[str]]]),而 slide_roster.py 的排序键函数则返回了带 int | str 联合类型的嵌套元组。

6. 命名约定

Kind Convention Examples
Module file snake_case.py image_search.pysvg_to_pptx.py
Script entrypoint 动词或名词短语 finalize_svg.pynotes_to_audio.py
Public function snake_case download_imageparse_results
Private helper _snake_case _load_dotenv_if_available_measure_actual_image
Constant UPPER_SNAKE_CASE API_URLDEFAULT_PAGE_SIZELICENSE_TIER_NO_ATTRIBUTION
Class PascalCase AssetCandidateSVGQualityChecker
Dataclass field snake_case license_tierdownload_url
Module-private regex _PATTERN_RE(私有 + _RE 后缀) _TAG_REHEADING_RE

对照源码可以逐条印证:slide_roster.py 里的正则命名为 _NUMBER_RE,常量表见 backend_common.pyMAX_RETRIESRETRY_BASE_DELAY_TRANSIENT_CLIENT_STATUSES 等;而 provider_common.py 中同名的 AssetCandidateImageSearchRequest 类以及贯穿脚本导出的 USER_AGENTbuild_attribution_text 等命名,正是这份表格在真实代码中的镜像。

7. 错误处理:永不裸 except,禁止安全相关静默降级

按场景选择的错误处理模式:

Situation Pattern
Optional dependency try: import x; HAS_X = True / except ImportError: HAS_X = False
Optional sibling module try: from project_utils import CANVAS_FORMATS / except ImportError: CANVAS_FORMATS = {}; print("Warning: ...")
可恢复的运行时失败 捕获具体异常、记录到 stderr、return/continue——绝不中断整条流水线
面向用户的错误 main()print("...", file=sys.stderr); return 1
编程错误 直接 raise——不要掩盖 bug

硬性规则:永远不允许裸写 except:,必须点名异常类。backend_common.py 中对 Pillow 的可选导入(try: from PIL import Image ... HAS_PIL = True / except ImportError: HAS_PIL = False)与 project_utils.py 中对 config.CANVAS_FORMATS 的可选兄弟模块导入,都精确复刻了上表前两行的写法——后者在 ImportError 时提供最小回退常量并打 Warning。

对于安全相关代码,还有两条禁止的静默降级

  • 禁用 SSL 校验但没有任何域名白名单 + WARNING——禁止;
  • 在下载路径上捕获所有异常却不记录 cause——禁止。

也就是说:你可以降级,但必须"被记录、被白名单约束地"降级,而不是无声吞错。

8. 依赖分层:越重的依赖越要懒加载

不同依赖允许出现的"档位":

Tier Where it can be required
Standard library 任意位置
requestsPillowlxml 公共依赖,主脚本中可以直接要求
Provider SDKs(google-genaiopenaianthropic 等) 在使用它的函数内部懒加载ImportError 时软失败并转成包含安装指引的 RuntimeError
python-dotenv 可选——import 用 try/except 包住,不可用时 no-op

与依赖配套的还有一个贯穿性的错误消息准则:错误信息必须包含修复方法——"该设置哪个环境变量"、"去哪里拿 key"、"该装哪个包"。文档给出的样板:

def _require_api_key() -> str:
    key = os.environ.get("PEXELS_API_KEY") or ""
    if not key:
        raise RuntimeError(
            "PEXELS_API_KEY is not set. Add it to your environment or .env file. "
            "Get one at https://www.pexels.com/api/"
        )
    return key

这条"给修复指引"的原则也贯穿到项目里专门做错误信息的 error_helper.py:其 ERROR_SOLUTIONS 字典为每类错误(missing_readmemissing_specinvalid_svg_naming 等)都同时携带 message、一组可操作的 solutionsseverity 分级,本质上就是把"必须告诉用户怎么修"固化成数据驱动的基础设施。

9. 共享辅助层:公共逻辑只放一处

公共功能统一沉淀在下面这些指定子模块中,新脚本必须复用它们,而不是各自复制一份

Module Owns
image_backends/backend_common.py HTTP 下载、重试、图片格式探测、Pillow 转码保存
image_sources/provider_common.py 许可证分级、查询简化、评分、署名文本、dataclass
project_utils.py 画布格式、项目路径约定
slide_roster.py 幻灯片数字文件名排序与 SVG 花名册发现
error_helper.py 面向用户的错误消息模板
console_encoding.py configure_utf8_stdio()——为 CLI 入口强制 UTF-8 控制台(§4)

禁止行为:复制共享 helper 中已有的逻辑。如果 helper 缺某个能力,正确的动作是扩展该 helper,而不是在新脚本里 fork 一份。从实现看,provider_common.py 确实同时是类型(classify_licenseImageSearchRequestAssetCandidate)与行为(评分、署名、许可证判定)的单一所有者;slide_roster.pydiscover_slide_svgs(directory)_NUMBER_RE 切分文件名数值段做自然排序,导出/校验/预览/动画/配音工具全部引用它——"一处定义、处处复用"在这里是字面意义上的事实。

10. Docstring:短而祈使,需要时才写区块

默认要求简短且用祈使语气;除非签名确实复杂,否则不要写 Args/Returns/Raises 区块。文档给出的范本:

def classify_license(
    license_name: str,
    license_url: str = "",
    provider: str = "",
) -> Optional[str]:
    """Classify a license string into one of the two tiers, or reject it.

    Returns:
        ``"no-attribution"`` / ``"attribution-required"`` / ``None``.

    The provider hint lets us treat Pexels and Pixabay's own licenses as
    ``no-attribution`` even when the upstream API only returns a short
    label like ``"Pexels"``.
    """

什么时候该用 Google/Sphinx 风格区块、什么时候该省掉:

使用区块(Google/Sphinx 风格) 省掉区块
函数有多个语义分支的返回 函数名本身已说明一切的单行注释即可
参数 > 3 且职责不明显 单一职责的辅助函数
维护着非平凡的不变量 纯格式化器 / 访问器

slide_roster.pydiscover_slide_svgs 的 "Return direct child SVG files in numeric filename order."、total_md_split.pyextract_leading_number 的 "Extract leading slide number if present." 都是"一句话说清职责"的教科书式短 docstring。

11. 测试约定:仓库刻意不携带自动化测试

这是一条可能与直觉相悖、但被明文写死的硬性项目约定:本仓库不随代码发布自动化测试。因此以下内容被明确禁止:

  • tests/ 目录
  • test_*.py 文件
  • unittest / pytest 导入
  • if __name__ == "__main__": 中运行自测套件的块

取而代之的验证方式是三类:

  • 通过 python3 -c "..." 对真实项目样本做内联 smoke 命令,并在对话 / PR 描述中展示输出;
  • runbook 中的人工核验步骤;
  • 针对 projects/_smoke_* 目录(已被 gitignore)的 live-API smoke 运行。

文档还特别提醒:外部贡献者如果附带测试,PR review 时应请其移除。这一约定与参考文档侧的规则成对存在——详见 docs/rules/prompt-style.md §11 中对参考文档的并行规定。这也反过来解释了为什么 §4 要求 main(argv=None) -> int 的可测试形态:测试不随仓库发布,但代码形态始终为"可随时被 -c 冒烟调用"而准备

12. Dataclass:优先 plain @dataclass,保持哑数据

值类型优先使用朴素 @dataclass,而不是 pydantic / attrs,并保持简单:

@dataclass
class AssetCandidate:
    provider: str
    title: str
    asset_id: str = ""
    license_tier: str = ""
    width: int = 0
    height: int = 0
    raw: Any = None
Rule Note
@dataclass 默认选择;除非可变性真是风险,否则不需要 frozen=True
字段顺序 所有必填字段在前,带默认值的可选字段在后
field(default_factory=...) 仅当默认值需要"每个实例一个全新容器"时才用
禁止遗留的位置参数 shim 新 dataclass 一律是关键字参数 API——YAGNI,不兼容位置传参
方法 dataclass 保持"哑",计算逻辑放到模块级函数

对照 provider_common.py 中真实的 AssetCandidateImageSearchRequest,可以看到必填字段在前、可选字段带默认值、raw 留作对接上游异构 JSON 的出口——与规范表格完全同构。

13. 文件编码与行尾:UTF-8 / LF / 无 BOM

Property Value
Encoding UTF-8
Line endings LF
Final newline 文件末尾始终保留空行
BOM 禁止
Indentation 4 空格(禁 tab)
Max line length 软上限 100;硬上限 120。docstring 中的散文可更长

这条规范不只是纸面要求——console_encoding.py 里就内置了对 BOM 的运行时校验:其身份校验逻辑在解码 UTF-8 前会先检查文件是否以 b"\xef\xbb\xbf" 开头,一旦命中直接抛 ValueError("UTF-8 BOM is not allowed"),并把 \r\n / \r 统一折叠为 \n 再做后续比对。可以说,UTF-8 + LF + 无 BOM 是这个仓库连"自检代码"都在强制的基线。

14. 双向交叉引用:Python 文件与参考文档成对链接

当一个 Python 文件镜像了一份参考文档时,需要双向交叉引用:

  • 脚本 docstring 中提及参考文档,例如:See references/image-searcher.md for the on-slide attribution rules.
  • 参考文档反过来以反引号包裹的相对链接引用该脚本。

这样设计的目的在于保持 prompt-style.mdcode-style.md(即本文档)以一对的形式运作——两层规范谁都不会漂移离谁太远:参考文档定义了"Agent 应该产出什么样的文件",本文定义了"产出这些文件的 Python 工具该长什么样"。配套的参考侧规则可以继续阅读 docs/rules/prompt-style.md

15. 冲突处理:既有文件优先 + 照着范本写

指南最后给出的仲裁原则是:既有文件优先。如果当前某个脚本与这里的规则相矛盾,就二选一——要么更新这份指南,要么重构该脚本。同时文档给出了一张"如果你要写 X,就照着 Y 写"的范本对照表:

If you're writing... Model after
一个小型 CLI 工具 total_md_split.pygemini_watermark_remover.py
多后端 / 分发型 CLI image_search.pyimage_gen.py
库 / 共享辅助模块 image_sources/provider_common.pyimage_backends/backend_common.py
基于类的检查器 / 校验器 svg_quality/checker.py

这条对照表恰好把前 14 节串成了一条可操作的"写作路径":要新写一个小 CLI,就打开 total_md_split.py 抄它的头部、导入分组与 main() 骨架;要写图片搜索/生成这类多后端分发器,就研究 image_search.py 如何通过 sys.path 注入 + 子目录模块 + 懒加载后端 SDK 组装出可插拔的架构;要抽公共逻辑,则回到 §9 的共享辅助层去扩展而不是复制

落地清单:把 15 条规范压缩成写脚本前的检查项

把全文压缩成一段可自检的 checklist,新脚本提交前逐条过一遍即可:

  1. 文件头:shebang + 五要素 docstring(工具名/用途/Usage/Examples/Dependencies);
  2. 导入:std → 第三方 → 本地三段分组,各按规范排序,必要时先做 sys.path 注入并补 # noqa: E402
  3. 入口:build_parser + main(argv=None) -> int + raise SystemExit(main()),help 必须在任何副作用前可处理且绝不被当成位置参数;
  4. 启动即调用一次 configure_utf8_stdio()(可直接运行的入口脚本),文件 I/O 显式 encoding="utf-8"
  5. 公共函数全量类型标注,克制 Any 与过度限定的泛型/Literal
  6. 命名遵守 §6 表格;正则统一 _..._RE 私有 + 后缀;
  7. 永不裸 except:;安全相关代码不做无记录的静默降级;
  8. 越重依赖越懒加载,错误消息必须带修复指引;
  9. 公共逻辑一律落到 §9 的共享 helper,缺功能就扩展它;
  10. docstring 短而祈使,仅在签名复杂时写区块;
  11. 不新增 tests/test_*.py、pytest/unittest——用 python3 -c 冒烟 + 真实样本输出代替;
  12. 值类型用 plain @dataclass,必填在前、可选在后、字段全 keyword;
  13. UTF-8 / LF / 结尾空行 / 无 BOM / 4 空格缩进 / 行宽软 100 硬 120;
  14. 与参考文档成对时做好双向交叉引用;
  15. 与既有文件冲突时,按 §15 范本表决定"改文档"还是"重构脚本",以既有文件为最终仲裁。

这份指南的价值在于它不是一个空泛的 PEP 8 复读机,而是对 skills/ppt-master/scripts/ 下数百个脚本真实形态的精确抽象——任何新加入的 Python 工具,只要能按上述 15 条落地,就能自然融入现有 CLI 生态,在编码、帮助文本、控制台输出、错误指引与共享复用等维度与既有工具保持完全一致的质感。

登录后查看全文
热门项目推荐
相关项目推荐