目录

13 · loader.py:加载流水线与插件执行顺序

核对基线 · 范围 · 依赖

核对基线:beancount 仓库 commit 97472138(2026-08-22)。路径相对仓库根目录,引用格式 文件:起-止行 (名称),行号已逐条核对。 本篇范围beancount/loader.py(847 行)与 beancount/loader_test.py(740 行)。 上游依赖beancount/parser/parser.pyparse_file/parse_string)、beancount/parser/booking.pybook)、beancount/ops/validation.pyvalidate)、beancount/parser/options.pyOPTIONS_DEFAULTS)、beancount/utils/encryption.pybeancount/parser/printer.pyprint_errors)、beancount/core/data.pyentry_sortkeynew_metadata)、beancount/utils/misc_utils.pylog_timeuniquify)。 下游使用者:包内 9 个非测试文件引用本模块。beancount/api.py:77-79 再导出 load_doc/load_encrypted_file/load_filebeancount/scripts/check.py:72beancount/scripts/doctor.py:135 等是实际调用点;beancount/parser/parser.py:136beancount/ops/summarize.py:41 只在 TYPE_CHECKING 下取 OptionsMap 别名。

1. 模块解决什么问题

parser.parse_file 只负责一个文件的文本:产出的指令里还留着待推算的数字,padbalance 这类要看到全局余额才能兑现的指令还是原样,include 只是记在 options 里的一串文件名。loader.py 是把这些补齐的唯一入口,对外只做一件事——给一个顶层文件名或一段字符串,拿回 (entries, errors, options_map)。它决定四件别处决定不了的事:阶段顺序(解析 → booking → 插件 → 校验)、内置插件相对用户插件的前后位置、多份文件的选项如何并成一份、能否复用上次的加载结果。

2. 结构一览

名称 位置 作用
class LoadError beancount/loader.py:45-50 加载期错误类型,source/message/entry=None
PLUGINS_PRE beancount/loader.py:53-56 用户插件之前跑:beancount.ops.documents
DEFAULT_PLUGINS_AUTO / PLUGINS_AUTO beancount/loader.py:58-63 --auto 的插件表与实际插入位(默认空)
PLUGINS_POST beancount/loader.py:65-68 用户插件之后跑:beancount.ops.padbeancount.ops.balance
RENAMED_MODULES beancount/loader.py:71 改名插件映射,现为空
PICKLE_CACHE_FILENAME / PICKLE_CACHE_THRESHOLD beancount/loader.py:74-79 文件名模板 .{filename}.picklecache、写盘耗时门槛 1.0 秒
_load_file(模块级名字) beancount/loader.py:81-85,833-843 initialize 重绑定:缓存版或删缓存版
load_file beancount/loader.py:89-128 文件入口:路径归一化 + 加密/缓存分支
load_encrypted_file beancount/loader.py:131-161 解密成字符串后转交 load_string
_log_errors beancount/loader.py:164-177 把错误打到文件或回调
get_cache_filename beancount/loader.py:180-195 由模板与顶层文件名算出缓存路径
pickle_cache_function / delete_cache_function beancount/loader.py:198-277 / beancount/loader.py:280-301 读写缓存 / 删除缓存两种装饰器
needs_refresh / compute_input_hash beancount/loader.py:309-320 / beancount/loader.py:323-338 缓存失效判定与输入指纹
load_string beancount/loader.py:341-373 字符串入口,不经缓存
_parse_recursive beancount/loader.py:376-529 include 展开、去重、options 合并
aggregate_options_map beancount/loader.py:532-560 跨文件聚合 operating_currencydcontextpythonpath
_load beancount/loader.py:563-636 四阶段流水线本体
run_transformations beancount/loader.py:639-742 组装插件序列逐个执行
combine_plugins beancount/loader.py:745-757 把多个插件模块的 __plugins__ 拼成一个
load_doc beancount/loader.py:760-808 把测试函数 docstring 当账本的装饰器工厂
initialize beancount/loader.py:811-844,847 装配缓存策略,模块导入时按环境变量执行一次

3. 三个入口

3.1 load_file:路径归一化后二选一

filename = path.expandvars(path.expanduser(filename))
if not path.isabs(filename):
    filename = path.normpath(path.join(os.getcwd(), filename))

if encryption.is_encrypted_file(filename):
    # Note: Caching is not supported for encrypted files.
    entries, errors, options_map = load_encrypted_file(
        filename, log_timings, log_errors, extra_validations, False, encoding
    )
else:
    entries, errors, options_map = _load_file(
        filename, log_timings, extra_validations, encoding
    )
    _log_errors(errors, log_errors)

beancount/loader.py:114-127。环境变量与 ~ 展开来自 commit 8fb6af0d;转绝对路径是 _parse_recursiveassert path.isabs(source)beancount/loader.py:432)的前提。加密文件走单独分支且明确不进缓存。非加密分支调的是模块级名字 _load_file,由 initialize 绑定(第 4 节),这层间接使缓存可开关、可替换。两个分支打印错误的位置不同:加密分支由它转交的 load_string_log_errors

3.2 load_stringload_encrypted_file

load_stringbeancount/loader.py:341-373)可选先 textwrap.dedent(供 load_doc 用缩进的 docstring),再调 _load([(string, False)], ...),元组第二位 False 表示这是内容而非文件名。load_encrypted_filebeancount/loader.py:131-161)解密后把明文交给 load_string,丢掉了 filename 上下文;自身的 dedent 参数未转给这次 load_string 调用(beancount/loader.py:154-161),传什么值都无效。这与被 include 的加密文件不同——后者在 _parse_recursive 里仍把 source_filename 传给 parse_string(见 5.3),只有顶层加密文件丢失文件名上下文。两者均不经 _load_file,与缓存无关。

4. pickle 磁盘缓存

4.1 缓存位置与命中条件

def needs_refresh(options_map: OptionsMap) -> bool:
    if options_map is None:
        return True
    input_hash = compute_input_hash(options_map["include"])
    return "input_hash" not in options_map or input_hash != options_map["input_hash"]

beancount/loader.py:309-320。缓存路径由 get_cache_filenamebeancount/loader.py:180-195)算出:模板是绝对路径就直接用,否则拼到顶层文件目录下,{filename} 填 basename,默认落在账本旁成为 .apples.beancount.picklecachecompute_input_hashbeancount/loader.py:323-338)对排序后的文件名逐个喂 md5:文件名本身,以及 struct.pack("dd", stat.st_mtime_ns, stat.st_size);文件不存在时只喂名字,不读文件内容——路径、mtime_ns、大小三项不变而内容被改,缓存不会失效(CHANGES:2829-2833 加入 mtime+size 哈希时称"paves the way for hashing the contents",此后未实现)。options_map["include"] 在解析末尾被覆写成本次真正读过的全部绝对路径(beancount/loader.py:525),已记录在案的文件——包括深层 include——都参与这份指纹,input_hash 在流水线末尾算出存进 options(beancount/loader.py:634),二者一起被 pickle 进缓存。beancount/loader_test.py:593-625 只覆盖顶层文件被移动后 needs_refresh 返回真,深层 include 的元数据变化无对应测试。大小与 mtime 一起哈希是为绕开只有一秒精度的文件系统(CHANGES:2829-2832)。

4.2 写入门槛与三处失败容忍

pickle_cache_functionbeancount/loader.py:198-277)的包装按序做:读缓存 → 反序列化 → needs_refresh 判定 → 命中即 return result。三处容错都不抛异常:反序列化 except Exception 全兜(注释说损坏或过期的 pickle 会以各种异常类型冒出来)后 logging.error 重算(beancount/loader.py:231-241),删旧缓存失败与写缓存失败各只 warning(beancount/loader.py:250-259beancount/loader.py:266-274)。写盘还有门槛:耗时 >= PICKLE_CACHE_THRESHOLD(1.0 秒)才落盘(beancount/loader.py:264-273)。delete_cache_functionbeancount/loader.py:280-301,缓存关闭时启用)同样调 os.remove,但无 try 包裹(beancount/loader.py:295-296):删除失败直接抛出,与 pickle_cache_functionos.remove 的容错(beancount/loader.py:252-258)不同。

4.3 initialize:缓存开关与两个环境变量

cache_pattern = (
    cache_filename or os.getenv("BEANCOUNT_LOAD_CACHE_FILENAME") or PICKLE_CACHE_FILENAME
)
cache_getter = functools.partial(get_cache_filename, cache_pattern)
if use_cache:
    _load_file = pickle_cache_function(cache_getter, PICKLE_CACHE_THRESHOLD, _uncached_load_file)
else:
    ...
    _load_file = delete_cache_function(cache_getter, _uncached_load_file)

beancount/loader.py:826-843(有省略)。模块末尾 initialize(os.getenv("BEANCOUNT_DISABLE_LOAD_CACHE") is None)beancount/loader.py:847)在导入时执行一次:默认开缓存,设了该环境变量就关。关掉时绑的不是"直接调用"而是 delete_cache_functionbeancount/loader.py:280-301),先删已有缓存再计算;理由见 commit d9a70473:用 --no-cache 调试插件后再启用缓存,会用回旧缓存,令人困惑。bean-check--no-cache/--cache-filename 靠再调一次 initialize 覆盖(beancount/scripts/check.py:65-67);use_cache=False 却给了文件名时打 warning 说该参数被忽略(beancount/loader.py:838-842)。

5. _parse_recursive:include 展开与选项合并

5.1 队列驱动,广度优先

source_stack = list(sources)
filenames_seen = set()
while source_stack:
    source, is_file = source_stack.pop(0)
    is_top_level = options_map is None

beancount/loader.py:406-415。变量叫 stack,实际 pop(0) 配末尾 appendbeancount/loader.py:518)是 FIFO 队列,include 广度优先展开。"是否顶层"用 options_map is None 判断,只有最先处理的源的 options 被采纳。

5.2 去重、缺失、glob

文件源在这里被断言为绝对路径(assert path.isabs(source)beancount/loader.py:432),normpath 归一后查 filenames_seen:命中就追加 Duplicate filename parsed 并跳过(beancount/loader.py:435-445),这是防 include 成环的唯一手段;文件不存在则追加 File "..." does not exist 并跳过(beancount/loader.py:447-455)。include 项先做 glob 展开(beancount/loader.py:492-509):相对模式拼到当前文件目录下,glob.glob(..., recursive=True) 支持 **,匹配不到报 File glob "..." does not match any files。相对基准分两种:文件源用自身所在目录(beancount/loader.py:419beancount/loader.py:459),字符串源用进程工作目录(beancount/loader.py:427)。commit 793d8be9 之前用 chdir() 包住 glob.glob,改显式拼路径为线程安全。

5.3 加密 include 的特殊路径

if is_file:
    cwd = path.dirname(source)
    source_filename = source
    if encryption.is_encrypted_file(source):
        source = encryption.read_encrypted_file(source)
        is_file = False

beancount/loader.py:417-423。被 include 的加密文件就地解密并降级成字符串源,随后走 parse_string(source, source_filename)beancount/loader.py:474-477)——文件名仍传给解析器用作 metadata,但 is_file 已是 False,它不进 filenames_seen,因此既不参与重复检测也不出现在 options_map["include"] 里。cwd 在降级前已按文件目录设好,其中的相对 include 仍相对它自己解析(commit 957001b8,#325)。

5.4 选项:顶层独占,三项聚合

顶层文件的 options_map 被整份采用,其余收进 other_options_mapbeancount/loader.py:487-490),注释写明 "No merging of options should occur"。若无一个源解析成功,退回 options.OPTIONS_DEFAULTS.copy()beancount/loader.py:521-522);随后 options_map["include"] = sorted(filenames_seen)beancount/loader.py:525)覆写原始 include 声明。aggregate_options_mapbeancount/loader.py:532-560)只聚合三样:operating_currency 去重合并、dcontextupdate_from 把各文件的显示精度并进顶层那一份(beancount/loader.py:548-551)、由开了 insert_pythonpath 的文件所在目录生成 pythonpath 列表(beancount/loader.py:553-558)。pythonpath 不是用户可写选项,是这里合成的中间值。

6. _load:四阶段流水线

with misc_utils.log_time("parse", log_timings, indent=1):
    entries, parse_errors, options_map = _parse_recursive(sources, log_timings, encoding)
    entries.sort(key=data.entry_sortkey)
with misc_utils.log_time("booking", log_timings, indent=1):
    entries, balance_errors = booking.book(entries, options_map)
    parse_errors.extend(balance_errors)
with misc_utils.log_time("run_transformations", log_timings, indent=1):
    saved_pythonpath = list(sys.path)
    try:
        if "pythonpath" in options_map:
            sys.path[0:0] = options_map["pythonpath"]
        entries, errors = run_transformations(entries, parse_errors, options_map, log_timings)
    finally:
        sys.path[:] = saved_pythonpath
with misc_utils.log_time("beancount.ops.validate", log_timings, indent=1):
    valid_errors = validation.validate(entries, options_map, log_timings, extra_validations)
    errors.extend(valid_errors)
options_map["input_hash"] = compute_input_hash(options_map["include"])

beancount/loader.py:598-634。各阶段产物:解析给出仍带未完成金额的指令,按 entry_sortkey(日期、类型序、行号)排一次——提前到 booking 之前是 commit 51226691(#84)的修正,因跨文件指令顺序影响库存匹配;booking.book 补齐仓位与推算数字并顺带跑 validate_missing_eliminatedbeancount/parser/booking.py:26-54),返回 (entries, errors) 二元组;run_transformations 执行全部插件,同样返回二元组;validation.validate 运行标准校验加调用方传入的 extra_validations 并收集错误,只返回错误列表(beancount/ops/validation.py:410-436),bean-checkHARDCORE_VALIDATIONS。四段接口不统一,_load 逐段拆包、显式汇总,末尾才组装成三元组返回(beancount/loader.py:634);booking 调用本就在插件之前,commit 0e46c762booking.interpolate 换成 booking.book(),并入 inventory-booking validation。标准校验按只读用途编写,但接口和调用方都不强制校验函数保持 entries 不变——loader 注释提到可比较前后哈希确认,"目前没做"(beancount/loader.py:629-631)。sys.path 改动包在 try/finally,只在插件阶段生效(commit bb87d7e5 从 grammar 层移来);末行把输入指纹写进 options 供缓存判定。

7. run_transformations:插件顺序与异常隔离

7.1 三段拼接与两种模式

if options_map["plugin_processing_mode"] == "raw":
    plugins_iter = options_map["plugin"]
elif options_map["plugin_processing_mode"] == "default":
    plugins_iter = itertools.chain(
        PLUGINS_PRE, options_map["plugin"], PLUGINS_AUTO, PLUGINS_POST
    )

beancount/loader.py:662-667。默认顺序是 documents → 用户插件 → --auto 插件 → padbalanceraw 模式内置插件全不跑,选项文档说其用途是让用户完全掌控顺序(beancount/parser/options.py:616-621)。分前后两组的理由在两端各自职责:documents(注册于 beancount/ops/documents.py:17,35-36)生成 Document 指令,放前面让用户插件看得到产物;padbalance(注册于 beancount/ops/pad.py:18,29-34beancount/ops/balance.py:17,48-60)依据全部交易算余额、补齐、校验,任何插件增改交易都会改变结果,所以放最后,且 pad 先于 balancepad 原本在 PLUGINS_PRE,commit 60f1c40f(#742)移到 PLUGINS_POST:插件生成的交易此前不被 pad 计入,与"padding 让随后的 balance 断言成立"的语义不符,同一提交在 beancount/ops/pad_test.py 补了回归测试。PLUGINS_AUTO 默认空,只有 bean-check --auto 临时 extendDEFAULT_PLUGINS_AUTO 并在 finally 还原(beancount/scripts/check.py:53-85)。

7.2 导入与执行的异常边界

导入阶段只吞 ImportError,转成一条 Error importing "..." 并继续下一个插件;其它 import 期异常按注释是有意让整次运行失败(beancount/loader.py:683-700)。没有 __plugins__ 的模块被静默跳过。执行阶段每个回调单独包 try,捕获 Exception 转成带完整 traceback 的 Error applying plugin "..."beancount/loader.py:717-736)。__plugins__ 的元素可以是函数名字符串,也可以是函数对象(beancount/loader.py:705-711),后者是 commit 312a6e88beancount.plugins.auto 这类元插件加的,combine_pluginsbeancount/loader.py:745-757)把若干模块的函数对象拼成一个列表。配置为 None 时回调只收两参数,否则三个(beancount/loader.py:713-715,TODO 称 v3 想统一)。每个插件模块跑完(不是每个函数跑完)重排一次 entries,注释是 "Don't trust the plugins themselves"(beancount/loader.py:738-740)。回调抛异常时外层赋值 entries, plugin_errors = callback(...) 不会执行,但若回调抛错前已对 entries 做过原地修改,那部分改动仍留在后续流程——这里没有快照或回滚(beancount/loader.py:717-724)。

8. 设计决策与理由

决策 理由 证据
加载入口最终返回 (entries, errors, options_map),常规错误累积不中断 一处语法或插件错误不该让整本账加载失败;错误带 metadata 供 printer 定位;内部各阶段接口不必统一,由 _load 汇总 beancount/loader.py:598-636,164-177
插件异常转成错误而不抛出,导入只吞 ImportError 坏插件不该阻断加载;插件模块自身的初始化异常是真 bug,让它冒出来 beancount/loader.py:683-687,717-736beancount/loader_test.py:81-93
documents 在前、pad/balance 在最后,三段只是一个列表的拼接 前者产出指令供用户插件消费,后者依赖全部交易的最终形态;raw 模式能整体关掉内置段 beancount/loader.py:53-68,662-673;commit 60f1c40f
每个插件模块跑完重排一次 插件可任意插入指令,后续阶段依赖有序 beancount/loader.py:738-740
排序提前到 booking 之前 多文件的指令顺序影响库存匹配结果 beancount/loader.py:602;commit 51226691(#84)
缓存按输入指纹判定,且只在耗时超过 1 秒时写 时间戳精度不足,改名与深层 include 变更都要能察觉;小账本写缓存不划算 beancount/loader.py:77-79,264-273,309-338CHANGES:2829-2832
缓存层做成可替换的模块级 _load_file 一次装配、全进程生效,脚本能覆盖、测试能替换 beancount/loader.py:81-85,811-844beancount/loader_test.py:526-535
加密顶层文件明确绕过缓存 源码注释直接写明"Caching is not supported for encrypted files";缓存本身用 pickle.dump 写盘,未加密 beancount/loader.py:118-120,269
顶层 options 独占、只聚合三项 booking 方法、账户类型这类选项一本账里只能有一份语义 beancount/loader.py:484-490,532-558
校验放在插件之后 插件可能修复或制造不平衡交易,最终的交易平衡检查必须验证插件处理后的结果,而不是原始输入 beancount/ops/validation.py:350-372
sys.path 改动包在 finally 加载是库调用,不留全局副作用 beancount/loader.py:612-620
include 展开在 loader 而非解析器 去重、glob、成环检测需要全局视野 beancount/loader.py:376-529

9. 行为细节与边界

现象 后果 证据
非法 plugin_processing_mode 分支写的是 assert "字符串" 断言恒真,之后 plugins_iter 未绑定触发 UnboundLocalError;经由 _load/load_string/load_file 的正常解析路径,非法值会被选项转换器挡下(只接受 raw/default),此分支不可达;但 run_transformations 是可直接调用的公开函数,options_map 也能被调用方修改后传入,此时分支仍可达 beancount/loader.py:639-673beancount/parser/options.py:24-36beancount/parser/grammar_test.py:724-731
执行插件的 except Exception 里判 SystemExit SystemExit 继承 BaseException,不会被 except Exception 捕获,这段 re-raise 是死代码;sys.exit() 穿透的行为与注释一致 beancount/loader.py:721-724beancount/loader_test.py:96-102
被 include 的加密文件不进 filenames_seen 不出现在 options_map["include"],改它不使缓存失效,也不参与重复检测 beancount/loader.py:417-423,456,525
字符串源的 include 相对进程 cwd 解析 同一段字符串在不同工作目录下结果不同 beancount/loader.py:424-428beancount/loader_test.py:377-404(该测试名为 test_load_string_with_relative_include,但第 394 行实际调用的是 loader.load_file,并未覆盖 load_string 这一行为)
重复 include 报错但不回滚 先解析的那份指令保留 beancount/loader.py:435-445beancount/loader_test.py:345-375
缺失文件与空 glob 是两条不同消息 does not exist / does not match any files,测试用一个正则同时容纳 beancount/loader.py:447-455,502-509beancount/loader_test.py:254
options_map["include"] 被覆写 加载后它是本次读过的全部绝对路径排序列表,不是用户写的 include 声明 beancount/loader.py:525beancount/parser/options.py:241-246
aggregate_options_mapcopy.copy 返回浅拷贝,但 dcontext 共享,update_from 原地改 beancount/loader.py:545,550
缓存存的是 pickle 后的三元组 DisplayContext 等对象;反序列化失败一律按损坏处理 beancount/loader.py:231-259,268-269
options 退回 OPTIONS_DEFAULTS.copy() 仅当没有任何源进入 parser(如唯一顶层文件不存在)时成立;文件存在但语法出错,parser.parse_file 仍经 builder.finalize 返回真实 options_map,不落入这一分支 beancount/loader.py:445-453,480-490,520-522beancount/parser/parser.py:224-227beancount/loader_test.py:142-147,234-238
validation.validatevalidation_tests = VALIDATIONS+= extra_validations += 对列表原地扩展,validation_tests 与模块级 VALIDATIONS(即 BASIC_VALIDATIONS)是同一对象;带 extra_validations 调用一次就把它们永久追加进全局列表,重复调用持续累积 beancount/ops/validation.py:390-407,423-425
--auto 靠修改模块级 PLUGINS_AUTO 生效 进程内全局状态,bean-checkfinally 还原,注释说是为同进程跑测试套件 beancount/scripts/check.py:53-85
【文档漂移】source_stack 名为 stack pop(0) + append 实际是 FIFO 队列 beancount/loader.py:404-414,518
【文档漂移】aggregate_options_map 的 docstring 写 "This value is mutated in-place" 函数第一行就 copy.copy 并返回新 map;只有 dcontext 是真的原地更新 beancount/loader.py:538-539,545
【文档漂移】注释 "chdir() for glob, which uses it indirectly" chdir 已被移除,注释未同步 beancount/loader.py:492-493;commit 793d8be9

10. 测试锁定了什么

loader_test.py 共 32 个测试方法,分五个类。

测试类 / 测试 行号 锁定的行为
TestLoader.test_import_exception 58-67 无效插件名产生一条错误,消息里含 ModuleNotFoundError
test_import_other_exception 69-76 import 期的非 ImportError 异常向上抛出
test_run_transformation_exception 81-93 插件运行抛 ValueError 转成一条错误,消息含类型名
test_run_transformation_systemexit 96-102 插件里的 SystemExit 穿透到调用方
test_load / test_load_string / test_load_nonexist 112-147 两入口都返回 list/list/dict;不存在的文件给空 entries + does not exist
test_renamed_plugin_warnings 149-164 RENAMED_MODULES 命中时发 warning 且不报错
TestLoadDoc 四个 167-212 覆盖 docstring 加载的默认无错误路径、expect_errors=Trueinsert_pythonpath 让同目录插件可导入;expect_errors=None 未覆盖;test_load_doc(167-175)把待测函数直接传给 load_doc 当成 expect_errors 参数,只生成了 decorator/wrapper 两层函数对象,未真正执行内部断言
TestLoadIncludes 十二个 216-518 绝对/相对/多级/.. include、重复 include 报错且只解析一次、返回的 include 全为绝对且已归一、三种 glob
TestLoadCache.test_load_cache 545-591 写出 .apples.beancount.picklecache;重复加载不重算;touch 顶层文件后重算
test_load_cache_moved_file / ..._read_only_fs 593-651 改名后 needs_refresh 为真;os.removeOSError 时不崩溃
两个 ..._override_filename_pattern_... / ..._disable 653-711 环境变量与参数两种方式覆盖模板;use_cache=False 时不留缓存文件
TestOptionsAggregation 715-737 三个文件的 operating_currency 合并成一个集合

覆盖方式两点:缓存类的 setUpbeancount/loader_test.py:524-535)用 mock.patch_load_file 换成阈值为 0 的 pickle_cache_function 并包一层计数器,是否命中缓存靠 self.num_calls 断言;test_load_cache_read_only_fs 的两个 mock 形参名与实际相反(装饰器自下而上,第一个形参对应 logging.warning),被断言调用一次的 warn_mock 其实是 os.remove,它锁定的是失效时尝试删除一次且不崩溃。

本文件未覆盖:load_encrypted_file 本身及 load_file 对顶层加密文件的分派,无测试直接调用(加密 include 路径由 beancount/utils/encryption_test.py:179-211 覆盖,但顶层文件本身未加密)、raw 模式的插件序列(beancount/parser/grammar_test.py:711-731 只测选项解析,raw 的效果散见于用它做隔离手段的 beancount/ops/documents_test.py)、combine_pluginsdcontext 聚合、内置插件相对顺序(回归测试在 beancount/ops/pad_test.py)、插件配置参数传递、缓存损坏分支。

11. 演变史

2017-04-30 commit 859f341e 之前本文件路径是 src/python/beancount/loader.py,下表中更早的提交需按旧路径检索。

日期 提交 / 记录 变化
2015-07-26 0e46c762 插值调用(原 booking.interpolate)本已在插件之前;此提交改经 booking.book() 间接调用,并把 inventory-booking validation 并入其中
2015-11-09 / 11-29 2d68cf72661bfbeaCHANGES:2965-2975 实现 pickle 缓存,初期由 BEANCOUNT_LOAD_CACHE 开启,称大文件快约 15 倍;随后默认开启并加 1 秒写入门槛
2015-12-01 51226691 排序提前到 booking 之前(#84)
2015-12-06 至 12-21 794a7b544f87d1a3eba4a664223403652f4ff7976c3434d78fb6af0d include 改成全量文件列表并据此让 include 变更触发失效;缓存改用文件名+mtime+size 哈希;跨 include 聚合 options(#91);缓存命中时也输出错误(#92);加载加密文件;文件名支持变量与 ~ 展开
2016 年 f7f0d5ce4e1aaccd845e9caabe478072 移动过的文件不再抛异常(#94,01-09);损坏或过期 pickle 的处理(03-05);只读文件系统删缓存失败不报错(04-17);BEANCOUNT_LOAD_CACHE_FILENAME(04-18)
2017-05-24 312a6e88 __plugins__ 支持函数对象,元插件 auto/pedantic 成为可能
2018-03 / 08 c16864f444dfdd3d957001b8 include 支持通配符(#241、PR59,当时用 chdir 包住 glob);支持 include 加密文件(#325)
2020-05-25 / 06-02 5fa98017d9a70473CHANGES:266-277 bean-check 加入 --no-cache/--cache-filename;随后 --no-cache 改为顺带删除既有缓存
2020-12-14 628acbfa 插件异常处理重写(#588):导入只吞 ImportError,执行期逐个回调捕获并输出完整 traceback;此前是一个 except (ImportError, TypeError) 包住整段
2021 年 c46eaacdbb87d7e5cb3526a1 跨文件聚合 DisplayContext(05-15);PYTHONPATH 插入从 grammar 层移到 loader 层(09-26);bean-check --autoPLUGINS_AUTO 插入位(12-05)
2024-01-06 60f1c40f beancount.ops.padPLUGINS_PRE 移到 PLUGINS_POST(#742)
2024-11-28 793d8be9 用显式路径拼接替换 chdir,glob 展开变为线程安全
2024-12-04 0265c999 修 win32 测试失败之余,把写缓存的判定从 > 改成 >=,耗时恰好等于门槛也落盘
2024-12-21 / 12-23(合入序) 85950542955876a9 LoadError 先由 namedtuple 改为 NamedTuple 类(entry: Any 无默认值),955876a9 再把 entry 改标注为 None 并给默认值

12. 与其他模块的关系

13. 参考索引

beancount/loader.py:45-50 LoadError;53-68 三个插件常量;71 RENAMED_MODULES;74-79 缓存常量;81-85 _load_file 声明;89-128 load_file;114-127 入口分支;131-161 load_encrypted_file;164-177 _log_errors;180-195 get_cache_filename;198-277 pickle_cache_function;231-241 损坏缓存;243-259 删除失败;264-273 写入门槛;280-301 delete_cache_function;304-306 _uncached_load_file;309-320 needs_refresh;323-338 compute_input_hash;341-373 load_string;376-529 _parse_recursive;404-415 队列与去重集合;417-423 加密降级;430-455 去重与缺失;457-477 两种解析调用;484-490 options 采纳;492-518 glob 与入队;521-527 收尾;532-560 aggregate_options_map;563-636 _load;598-634 四阶段;639-742 run_transformations;662-673 模式与拼接;683-700 导入异常;702-716 回调解析与参数;717-736 执行异常;738-740 重排;745-757 combine_plugins;760-808 load_doc;811-844 initialize;847 模块级装配。

beancount/loader_test.py:57-164 TestLoader;167-212 TestLoadDoc;216-518 TestLoadIncludes;523-711 TestLoadCache(524-535 setUp 的 mock 装配);714-737 TestOptionsAggregation

其它beancount/parser/options.py:24-36 模式校验器、beancount/parser/options.py:233-247 include、beancount/parser/options.py:248-258 input_hash、beancount/parser/options.py:259-266 dcontext、beancount/parser/options.py:273-297 plugin、beancount/parser/options.py:614-631 plugin_processing_mode、beancount/parser/options.py:718-724 insert_pythonpath、beancount/parser/options.py:741 只读选项;beancount/parser/booking.py:26-54beancount/ops/validation.py:410-436beancount/ops/documents.py:17beancount/ops/pad.py:18beancount/ops/balance.py:17beancount/plugins/auto.py:17beancount/scripts/check.py:53-85beancount/api.py:77-79beancount/parser/grammar_test.py:711-731CHANGES:266-277,2829-2832,2965-2975

commit0e46c762(2015-07-26)、2d68cf72(2015-11-09)、661bfbea(2015-11-29)、51226691(2015-12-01)、794a7b54(2015-12-06)、4f87d1a3(2015-12-07)、eba4a664(2015-12-12)、22340365(2015-12-12)、2f4ff797(2015-12-13)、6c3434d7(2015-12-16)、8fb6af0d(2015-12-21)、f7f0d5ce(2016-01-09)、4e1aaccd(2016-03-05)、845e9caa(2016-04-17)、be478072(2016-04-18)、859f341e(2017-04-30)、312a6e88(2017-05-24)、c16864f4(2018-03-23)、44dfdd3d(2018-03-27)、957001b8(2018-08-23)、5fa98017(2020-05-25)、d9a70473(2020-06-02)、628acbfa(2020-12-14)、c46eaacd(2021-05-15)、bb87d7e5(2021-09-26)、cb3526a1(2021-12-05)、60f1c40f(2024-01-06)、793d8be9(2024-11-28)、0265c999(2024-12-04)、955876a9(2024-12-16,合入序在 85950542 之后)、85950542(2024-12-22)。