目录

12 · ops/validation.py:插件处理完毕后的指令流校验

核对基线 · 范围 · 依赖

核对基线:beancount 仓库 commit 97472138(2026-08-22)。路径相对仓库根目录,引用格式 文件:起-止行 (名称),行号已逐条核对。 本篇范围beancount/ops/validation.py(436 行)与 beancount/ops/validation_test.py(457 行)。 上游依赖beancount/core/datasanity_check_types、各指令类型)、beancount/core/gettersget_entry_accounts)、beancount/core/interpolatecompute_residualinfer_tolerances,见 07 篇)、beancount/utils/misc_utilslog_time)。 下游使用者beancount/loader.py:623-628 在加载流程末尾调用 validation.validatebeancount/scripts/check.py:76-77 额外传入 HARDCORE_VALIDATIONS

1. 模块解决什么问题

"""Validation checks.

These checks are intended to be run after all the plugins have transformed the
list of entries, just before serving them or generating reports from them. The
idea is to ensure a reasonable set of invariants and generate errors if those
invariants are violated. They are not sanity checks--user data is subject to
constraints which are hopefully detected here and which will result in errors
trickled up to the user.
"""

validation.py:1-9。加载一个账本要经过四个阶段:parse → booking + interpolate → plugins → validation(CHANGES:3311-3320 的流程图)。解析阶段负责解析并收集语法错误;booking/interpolate 阶段补全 posting、执行 lot 匹配并收集 booking 错误(loader.py:596-607);前序阶段只要正常返回 entries 和 errors,loader 就会继续运行插件和 validation(loader.py:609-628)——test_validatevalidation_test.py:348-377)里 booking 先报出一条 "No position matches",随后 validation 照常运行并另外报出一条 "Invalid currency";但未被阶段内部转换为 errors 的异常仍会中断流水线,loader.py:605-607booking.book 的调用没有异常捕获(例见 09 篇记录的 CostSpec/validate_missing_eliminated 边界:beancount/parser/booking_full.py:986-995beancount/parser/booking.py:72-77)。插件可以任意增删改指令。validation.py 检查的是"整条指令流作为一个整体是否自洽"——账户在被引用时是否处于开启区间、每笔交易最终是否平账、Balance 断言有没有自相矛盾。

关键限定词是 docstring 里的 "not sanity checks":这些不是防御程序员写错的内部断言,而是用户数据本身的约束,检查结果要以错误列表的形式回传给用户。

2. 结构一览

名称 位置 作用
class ValidationError(NamedTuple) validation.py:29-34 三字段错误记录:sourcemessageentry
ALLOW_AFTER_CLOSE validation.py:37-47 (Balance, Document, Note),账户关闭后仍允许出现的指令类型
validate_open_close :50-118 Open/Close 指令自身的约束
validate_duplicate_balances :121-157 同日同账户同币种的 Balance 断言金额必须一致
validate_duplicate_commodities :160-191 每个币种最多一条 Commodity 指令
validate_active_accounts :194-247 检查非 Open/Close 指令中经 getters.get_entry_accounts 提取的账户引用是否落在开启区间内;曾开启账户的 Balance、Document、Note 可出现在关闭之后
validate_currency_constraints :250-300 posting 币种必须在 Open 声明的币种列表里
validate_documents_paths :303-320 Document 的 filename 必须是绝对路径
validate_data_types :323-347 逐条跑 data.sanity_check_types
validate_check_transaction_balances :350-386 重新验算每笔交易的残差
BASIC_VALIDATIONS :389-398 默认永远运行的 7 个校验器
HARDCORE_VALIDATIONS :400-404 慢校验,只含 validate_data_types
VALIDATIONS = BASIC_VALIDATIONS :406-407 实际使用的清单
validate :410-436 依次调用清单里的函数并汇总错误

3. 各校验器分别拦截什么

3.1 validate_open_close:Open/Close 指令自身

三条约束(docstring 写的是 "two kinds",实际列了三条):同一账户不得重复 Open、不得重复 Close;Close 必须有对应的 Open;Close 日期不得早于 Open 日期。

try:
    open_entry = open_map[entry.account]
    if entry.date < open_entry.date:
        errors.append(ValidationError(
            entry.meta,
            "Internal error: closing date for {} "
            "appears before opening date".format(entry.account), entry))
except KeyError:
    errors.append(ValidationError(
        entry.meta, "Unopened account {} is being closed".format(entry.account), entry))

validation.py:96-114。比较用的是严格小于,所以同一天 Open 又 Close 是合法的:2023-03-19 commit 1bf7fa41<= 改成 <,理由是 beancount 视 Open 为当日开始、Close 为当日结束,而当天既可以有交易也可以有 Close,两者同日应当一致(docstring 第 3 条仍写 "strictly greater than",属于文档漂移,见第 6 节)。

try 分支只有在遍历到当前 Close 之前、open_map 里已经登记过同账户的 Open 时才会进入。loader 调用 validation 之前始终按 data.entry_sortkey 排序(loader.py:602),日期更早的 Close 会先于日期更晚的 Open 被遍历到,此时 open_map 还没有这个账户,走的是 except KeyError 分支,报 "Unopened account ... is being closed"——test_validate_open_close__orderingvalidation_test.py:72-81)把 Open 写在源文本靠前、Close 写在靠后,但 Close 的日期更早,加载排序后仍是这条路径。"Internal error: closing date ... appears before opening date" 只有在传入 validate_open_close 的 entries 违反了这条排序不变式——即先遍历到 Open、后遍历到日期更早的同账户 Close——时才会触发,正常 loader 流程不会走到它。

open_mapclose_map 都只在首次登记(:84,116),重复出现的第二条不覆盖第一条,因此 Close 日期永远与最早那条 Open 比较。

3.2 validate_duplicate_balances:Balance 断言的一致性

key 是 (account, currency, date);同 key 的首条 Balance 被保存进 balance_entries 并作为基准不再更新,之后每一条同 key 的 Balance 都与这条基准比较,金额不同则报错、完全相同则放行(validation.py:143-155)。docstring 给的理由:beancount 不支持时间,指令的书写顺序被设计成无关紧要,所以同一天同一账户不可能有两个不同的余额;但完全相同的两条断言是允许的,因为导入流程可能重复生成。

3.3 validate_duplicate_commodities:Commodity 指令唯一

key 是 entry.currency,第二条起报错(validation.py:177-187)。

3.4 validate_active_accounts:引用必须落在开启区间

for account in getters.get_entry_accounts(entry):
    if account not in active_set:
        # Allow document and note directives that occur after an
        # account is closed.
        if isinstance(entry, ALLOW_AFTER_CLOSE) and account in opened_accounts:
            continue
        error_pairs.append((account, entry))

validation.py:226-235。单趟遍历维护两个集合:active_set 随 Open 加入、随 Close 移除,opened_accounts 只增不减。Open/Close 自身不参与检查,其余指令的账户由 getters.get_entry_accounts 提取(beancount/core/getters.py:147)。

docstring 强调这比单纯比日期更严:因为遍历的是已排序的指令流,同一天里 Open 排在交易之前(beancount/core/data.py:705 (SORT_ORDER)Open 权重 -2、Balance -1、Document 1、Close 2,Transaction 不在表里、取默认权重 0)。同一天的文本书写顺序不影响结果:loader 在调用 validation 之前已按 entry_sortkeydata.py:708-719)排序(loader.py:602),所以哪怕交易在源文件里写在同日 Open 之前,排序后 Open 仍在该交易之前,不会被判定为账户未开启。只有绕过 loader、直接向本函数传入未按 entry_sortkey 排序的 entries 时,遍历顺序才可能让同日引用出现在 Open 之前而被误报。

错误消息分两轮生成(:239-245):第一轮只记 (account, entry) 对,第二轮才根据 opened_accounts 的最终状态区分措辞——账户从未出现过 Open 是 "unknown account",出现过但当时不在区间内是 "inactive account"。

ALLOW_AFTER_CLOSE 放行 Balance、Document、Note(:37-47),注释给的理由是:Balance 可能来自晚于关户日的对账单,Document 可能事后才收到,Note 可能是关户后为解决问题补记的。放行条件里带 account in opened_accounts,所以对一个从未 Open 过的账户,Note 同样报错。

3.5 validate_currency_constraints:Open 声明的币种列表

先把带币种声明的 Open 收进 open_mapvalidation.py:266-270,条件是 entry.currencies 为真),然后逐笔交易逐条 posting 比对:

if posting.units.currency not in valid_currencies:
    errors.append(ValidationError(
        entry.meta,
        "Invalid currency {} for account '{}'".format(
            posting.units.currency, posting.account), entry))

validation.py:289-298。只看 posting.units.currency,不看 cost.currencyprice.currency。测试里 2014-01-04 那笔 Assets:Account2 1 HOOL {500 USD} 在账户只声明 HOOL 的情况下通过(validation_test.py:243-249),确认成本币种不受这条约束管辖。没有声明币种的账户直接跳过。

3.6 validate_documents_paths:路径必须绝对

return [
    ValidationError(entry.meta, "Invalid relative path for entry", entry)
    for entry in entries
    if (isinstance(entry, Document) and not path.isabs(entry.filename))
]

validation.py:316-320。docstring 说明相对路径应当在解析阶段就被消化掉——beancount/parser/grammar.py:816-819 (Builder.document) 对非绝对路径做 path.abspath(path.join(path.dirname(filename), ...));自动发现的文档来自 find_documentsbeancount/ops/documents.py:92-185):113-117 行把搜索根目录转换成绝对路径,account.walk 据此产生 root(127-129 行),175-183 行为每个匹配文件构造 Documentfilenamepath.join(root, filename)(179 行)生成,同样是绝对路径。走到这里还是相对路径,说明是插件生成的 Document 没遵守约定。

3.7 validate_data_types:把断言异常翻译成错误

try:
    data.sanity_check_types(
        entry, options_map["allow_deprecated_none_for_tags_and_links"])
except AssertionError as exc:
    errors.append(
        ValidationError(entry.meta, "Invalid data types: {}".format(exc), entry))

validation.py:339-346data.sanity_check_typesbeancount/core/data.py:603-643)是一串 assert isinstance:指令类型、meta 是 dict 且含 filename/linenodatedatetime.date,Transaction 还要查 flag/payee/narration/tags/links/postings 以及每条 posting 的 account/units/cost/price/flag。它的存在理由写在 docstring 里:用户可以写代码自由操纵这些元组,"With discipline, this mostly works, but I know better: check, just to make sure."

validate_open_close 也用 try/except 把 KeyError 转成错误对象(validation.py:96-114),所以"捕获异常生成错误"并非本函数独有;这里独有的是捕获 data.sanity_check_types 抛出的 AssertionError,也是唯一被列入 HARDCORE_VALIDATIONS 的校验器。sanity_check_types 全部由 assert 构成(data.py:616-643),以 python3 -O 或设置 PYTHONOPTIMIZE 运行时这些检查被整体移除,validate_data_types 便不再产生任何错误;若某个 entry 未通过第一条 assert isinstance(entry, ALL_DIRECTIVES)data.py:616)且自身没有 meta 属性,except AssertionError 分支里的 entry.meta 会转而抛出 AttributeError,不会被转换成 ValidationError

3.8 validate_check_transaction_balances:基于插件处理后的 postings 重新验算

# IMPORTANT: This validation is _crucial_ and cannot be skipped.
# This is where we actually detect and warn on unbalancing
# transactions. This _must_ come after the user routines, because
# unbalancing input is legal, as those types of transactions may be
# "fixed up" by a user-plugin. In other words, we want to allow
# users to input unbalancing transactions as long as the final
# transactions objects that appear on the stream (after processing
# the plugins) are balanced. See {9e6c14b51a59}.
residual = interpolate.compute_residual(entry.postings)
tolerances = interpolate.infer_tolerances(entry.postings, options_map)
if not residual.is_small(tolerances):
    errors.append(ValidationError(
        entry.meta, "Transaction does not balance: {}".format(residual), entry))

validation.py:365-384。这段注释解释了 validation 为什么排在插件之后调用:写入不平衡的交易、再靠插件事后补平,是 beancount 承认的合法用法。如果在插件之前判定平账,这类用法会被误报;只有等插件全部跑完,指令流上呈现的最终形态才是应当平账的东西。

计算方式与 07 篇的插值路径同源但独立重算一遍:compute_residual 把每条 posting 的 weight 加进一个 Inventory,跳过 meta 里带 __residual__ 标记的自动补差 posting(beancount/core/interpolate.py:88-92);infer_tolerances 按各币种小数位乘 tolerance_multiplier 推出容差字典;Inventory.is_small(tolerances) 逐 position 比对 abs(number) > tolerance,字典里查不到的币种容差取 ZERObeancount/core/inventory.py:152-169)。

infer_tolerancesmode 参数在这里用默认值 "max"beancount/core/interpolate.py:97),与 booking 阶段一致——beancount/parser/booking_full.py:170-178 只在 use_precise_interpolation 打开时才对插值单独用 "min",选项说明写着"插值用最细容差、校验用最松容差"(beancount/parser/options.py:701-707)。代码里挂着一句性能自省:注释说这遍重算有点慢,本可以用 loader 的哈希只检查未被改动的原始交易(validation.py:360-361)。

4. 两组清单与 validate 的驱动方式

BASIC_VALIDATIONS = [
    validate_open_close, validate_active_accounts, validate_currency_constraints,
    validate_duplicate_balances, validate_duplicate_commodities,
    validate_documents_paths, validate_check_transaction_balances,
]

# These are slow, and thus only turned on in the check() routine.
# We're hoping to optimize these and make them decently fast, so
# we're not providing an option at this moment, this can be enabled
# by modifying the 'VALIDATIONS' attribute below.
HARDCORE_VALIDATIONS = [validate_data_types]

VALIDATIONS = BASIC_VALIDATIONS

validation.py:389-407。两组的划分标准只有一条:速度。BASIC 里的 7 个每次加载都跑;HARDCORE 里的 validate_data_types 要对每条指令做几十次 isinstance,因此不进默认清单。注释明说没有对应的账本配置选项;开启方式包括直接修改 VALIDATIONS 模块属性、通过 extra_validations 传入(beancount/scripts/check.py:72-78 用的是这种方式),或者绕过 VALIDATIONS 直接调用 validate_data_types 本身。

validate 本身只是一个驱动器(:423-436):拼出清单,逐个调用,用 misc_utils.log_time 记录每个校验器的耗时(indent=2,嵌在 loader 那层 indent=1"beancount.ops.validate" 之下),把返回的错误列表拼接起来。所有校验器签名统一为 (entries, options_map) -> list[error],因此用户传入的 extra_validations 只要遵守同一签名即可。

在 loader 里的位置(beancount/loader.py:623-628)紧跟 run_transformations 之后、input_hash 计算之前。run_transformations 里的插件序列由 options_map["plugin_processing_mode"] 决定(loader.py:662-671):"default" 时是 PLUGINS_PREbeancount.ops.documents)+ 用户插件 + PLUGINS_AUTO + PLUGINS_POSTbeancount.ops.padbeancount.ops.balance),"raw" 时只跑用户插件(loader.py:54-68)。无论哪种模式,validation 都在其后运行,所以 "raw" 模式下 pad 不再合成补差交易、balance 不再做断言,但平账校验和账户区间校验照跑。

5. 设计决策与理由

决策 理由 证据
全部校验放在插件之后 插件可以任意改写指令流;"先写不平衡交易、靠插件补平"是合法用法,提前判定会误报 validation.py:1-9,365-372loader.py:623-628
返回错误列表而非抛异常 一次加载要报出全部问题;错误随 load_file 的返回值上浮,而不是中断加载 validation.py:428-436loader.py:628
错误对象是带 source meta 的 NamedTuple 与全仓库的 data.BeancountError 协议一致,printer.format_error 能统一渲染成 filename:lineno: 加消息加指令原文 validation.py:29-34data.py:35-43printer.py:556-572
按速度分 BASIC/HARDCORE 两组,不提供选项 作者希望日后优化 validate_data_types 后合并,因此不固化成用户选项 validation.py:400-404scripts/check.py:76-77
校验器签名统一为 (entries, options_map) extra_validations 与内置校验器走同一驱动循环 validation.py:424-433
validate_active_accounts 分两轮生成消息 单趟遍历时还不知道账户后面会不会被 Open,无法区分 "unknown" 与 "inactive" validation.py:214,237-245
Balance/Document/Note 允许出现在关户之后 对账单、文档、备注都可能晚于关户日产生;真正要禁止的是往已关账户记账 validation.py:37-47;commit c382d21f12f0e870
平账重算而不是复用 booking 的结论 插件改过 postings 之后,booking 阶段的结论已经失效 validation.py:360-375

6. 行为细节与边界

现象 后果 证据
validation_tests = VALIDATIONS 后用 += list 的 += 是原地扩展,VALIDATIONSBASIC_VALIDATIONS 是同一个对象,传 extra_validations 会把它们永久追加进全局清单;同一进程内反复调用会不断累积、重复执行 validation.py:423-425
validate_duplicate_commodities 里的 if previous_entry: 恒真(Commodity 是非空 NamedTuple),该分支等价于无条件报错 validation.py:180
gettersCustom 指令返回空账户集 Custom 指令里写的账户不受活跃区间检查 getters.py:116
validate_currency_constraints 直取 posting.units.currency 插件若产出 units=None 的 posting 会抛 AttributeError 而非报错 validation.py:289
成本与价格币种不受 Open 币种列表约束 1 HOOL {500 USD} 在只声明 HOOL 的账户上合法 validation.py:289validation_test.py:243-249
"Internal error: closing date ... appears before opening date" 只在 entries 违反排序不变式时触发 正常 loader 流程下,早于 Open 的 Close 因排序在前先被遍历,报的是 "Unopened account ... is being closed";只有先遍历到 Open、后遍历到日期更早的同账户 Close(未排序或手工构造的 entries)才会走到 "Internal error" 分支 validation.py:96-114loader.py:602
validate_open_close docstring 第 3 条写 Close 日期须 "strictly greater than" Open 日期 实现用 < 判断,允许同日 Open 与 Close;2023-03-19 commit 1bf7fa41 特意把 <= 改成 < 以放开同日 【文档漂移】validation.py:61-62,98;commit 1bf7fa41
sanity_check_types 全部由 assert 实现 python3 -O 或设置 PYTHONOPTIMIZE 运行时这些检查被整体移除,validate_data_types 不再报出任何 "Invalid data types" 错误 data.py:616-643validation.py:339-346
entry 未通过首条 assert isinstance(entry, ALL_DIRECTIVES) 且自身没有 meta 属性 except AssertionError 分支里的 entry.meta 转而抛出 AttributeError,不会被转换成 ValidationError data.py:616-620validation.py:339-346
validate 与各校验器本身都不排序 entries,只按接收顺序遍历 校验器假定调用方提供按 entry_sortkey 排序的 entries;loader 在解析后(:602)与每个插件运行后(:740)都重新排序以满足这一前置条件 loader.py:596-602,738-740validation.py:410-436
账户 Close 之后再 Open active_set 重新加入,后续引用合法;重复 Open 由 validate_open_close 单独报错 validation.py:218-223,74-84
Close 一个从未 Open 的账户 active_set.discard 无副作用,该账户不进 opened_accounts,后续引用报 "unknown" validation.py:222-223,241-244
path.isabs("")False 空 filename 的 Document 报"相对路径"错误 validation.py:319validation_test.py:310-312
【文档漂移】validate_open_close docstring 说 "two kinds of constraints" 正文列了 3 条 validation.py:53-62
【文档漂移】validate_duplicate_commodities 里的注释 "Mapping of (account, currency, date) to Balance entry" 从上一个函数复制而来,实际 key 是 currency、值是 Commodity validation.py:171-172
【文档漂移】validate_currency_constraintsvalidate_documents_paths 的形参名为 options_map 却未使用 其余函数用 unused_options_map 命名区分;validate_data_types 虽也叫 options_map,但第 341 行实际读取了它,不属于此列 validation.py:250,303,341

7. 测试锁定了什么

validation_test.py 共 10 个测试类、18 个测试方法:17 个用 @loader.load_doc 走完整加载流程,test_validate_documents_paths 手工构造 4 个 Document 对象,不经过 loader;18 个里有 13 个显式调用某个 validation.validate* 函数,其余 5 个(test_validate_balance_after_close 与四个容差测试)只依赖 load_doc 完整加载后返回的 errors 断言行为:

测试 行号 锁定的行为
test_validate_open_close__duplicate_open 17-35 同日重复 Open 与不同日重复 Open 都报错,只报第二条
test_validate_open_close__duplicate_close 37-59 同上,针对 Close
test_validate_open_close__close_unopened 62-70 关闭未开启账户
test_validate_open_close__ordering 72-81 Close 日期早于 Open
test_validate_duplicate_balances 85-115 同日同账户同币种不同金额报错;金额相同、日期不同、币种不同、账户不同均放行
test_validate_duplicate_commodities 118-132 第二次及以后的 Commodity 指令报错
test_validate_active_accounts 136-201 Open 前的交易/Note/Pad、未知账户、关户后的交易报错;关户后的 Note 与 Document 放行;消息区分 inactive 与 unknown
test_validate_active_accounts__unopened 203-215 两个未开启账户产生两条错误
test_validate_balance_after_close 217-224 关户后的 Balance 断言不产生任何错误(整个加载零错误)
test_validate_currency_constraints 228-278 #expected 标签标出应报错的交易,逐条比对;含单币种、多币种、带成本三种情形
test_validate_documents_paths 282-316 手工构造 4 条 Document,绝对路径放行,相对路径、.. 开头、空串报错
test_validate_data_types 320-332 narration 替换成 set 后产生一条 ValidationError
test_validate_check_transaction_balances 336-344 两条同号 posting 的交易报一条错误
test_validate 348-377 驱动器整体:booking 阶段报 "No position matches",validation 报 "Invalid currency",两个阶段的错误分别落位
TestValidateTolerances 四例 381-433 整数与小数混写、inferred_tolerance_default 全局与按币种设置、带精度的写法,均不产生平账错误

未覆盖的行为:ALLOW_AFTER_CLOSE 对从未 Open 账户的拒绝路径;validate_currency_constraints 遇到 units=None 的 posting;validate_data_types-O/PYTHONOPTIMIZE 下的行为;同日 Open 与 Close 合法这一行为——1bf7fa41 只改了 validation.py<=<,没有随附新增或修改 validation_test.py 的用例;"Internal error: closing date ... appears before opening date" 分支的错误文本——test_validate_open_close__orderingvalidation_test.py:72-81)经 loader 排序后实际走的是 "Unopened account" 路径,且只断言 error.entry.account、不断言 error.message,全仓库测试都没有出现过这条错误文本。

validation_test.py 未直接测试 extra_validations 参数与非 Nonelog_timings。全仓库范围内,example_test.py(19-22、30-33 行)通过 loader.load_string 间接执行了 extra_validationsHARDCORE_VALIDATIONS 的组合,loader_test.py(121-126、135-140 行)通过完整加载流程两次传入非 Nonelog_timings 做冒烟覆盖;但没有测试断言 extra_validations 不会污染 VALIDATIONS/BASIC_VALIDATIONS 全局列表,也没有测试校验器不会因这种污染而累积重复执行。生产代码里 scripts/check.py:72-78scripts/example.py:1167-1169 也会传入 HARDCORE_VALIDATIONS

8. 演变史

日期 提交 变化
2014-06-26 c382d21f Document 与 Note 允许出现在关户之后
2014-07-05 878741b2 加入 validate_data_typesvalidate_duplicates
2014-07-05 520c7fbd 拆出 BASIC_VALIDATIONS / HARDCORE_VALIDATIONSvalidate_check_balancesvalidate_duplicates 分入 HARDCORE;validate_data_types 此时仍同时留在两组清单里
2014-08-31 59ff7178 歧义价格检测移出,改为可选插件
2014-09-01 efad7ffd 平账检测(validate_check_transaction_balances)从 HARDCORE 移入 BASIC,并写下 "cannot be skipped" 的 IMPORTANT 注释
2014-09-20 69ec7cb8 加入 extra_validations 参数(引入了本篇第 6 节的原地 += 问题)
2014-10-18 c831388e 未使用账户检查移出为插件(今 plugins/nounused.py
2014-10-18 82c0a2d4 重复交易检查移出为插件(今 plugins/noduplicates.py),同时把 validate_data_typesBASIC_VALIDATIONS 中移除,使其成为 HARDCORE_VALIDATIONS 里唯一的校验器
2015-01-18 045b61d7 加入 validate_duplicate_commodities
2015-07-26 0ee94410 删除 validate_inventory_booking,负持仓检查上移到 booking 阶段
2020-11-17 12f0e870 Balance 加入 ALLOW_AFTER_CLOSE;提交信息说明仍然禁止往已关账户记账
2023-03-19 1bf7fa41 同日 Open + Close 合法,<= 改为 <(PR 751)

注:2017 年之前仓库布局带 src/python/ 前缀,上表 2015 年及更早的提交里,本文件路径为 src/python/beancount/ops/validation.py859f341e 是搬迁提交)。

9. 与其他模块的关系

10. 参考索引

beancount/ops/validation.py:1-9 模块 docstring;14-26 导入;29-34 ValidationError;37-47 ALLOW_AFTER_CLOSE;50-118 validate_open_close(96-114 Close 分支);121-157 validate_duplicate_balances(143-155 去重逻辑);160-191 validate_duplicate_commodities(171-172 复制来的注释,180 恒真分支);194-247 validate_active_accounts(217-235 单趟遍历,237-245 两轮消息);250-300 validate_currency_constraints(266-270 open_map,289-298 检查);303-320 validate_documents_paths;323-347 validate_data_types;350-386 validate_check_transaction_balances(360-361 性能注释,365-372 IMPORTANT 注释,375-384 残差判定);389-398 BASIC_VALIDATIONS;400-404 HARDCORE_VALIDATIONS;406-407 VALIDATIONS;410-436 validate(423-425 清单拼接)。

beancount/ops/validation_test.py:17-35、37-59、62-70、72-81、85-115、118-132、136-201、203-215、217-224、228-278、282-316、320-332、336-344、348-377、381-398、400-410、412-422、424-433。

其它beancount/loader.py:31,54-68,93,135,345,596-607,609-621,623-628,662-671,738-742beancount/core/data.py:35-43,603-643,705,708-719beancount/core/getters.py:59-71,73-82,84-92,94-102,104-112,115-116,147-158beancount/core/interpolate.py:72-94,97-231,236-239beancount/core/inventory.py:152-169beancount/parser/options.py:614-631,680-699,700-717beancount/parser/grammar.py:816-819beancount/parser/printer.py:543-552,556-572beancount/ops/documents.py:17,35-67,92-185beancount/ops/pad.py:18,21-26beancount/ops/balance.py:17beancount/plugins/auto_accounts.py:29-42beancount/plugins/pedantic.py:17-27beancount/scripts/check.py:72-78beancount/scripts/example.py:1167-1169beancount/scripts/example_test.py:19-22,30-33beancount/loader_test.py:121-126,135-140beancount/utils/misc_utils.py:15-36CHANGES:3277-3281,3311-3320

commitc382d21f(2014-06-26)、878741b2(2014-07-05)、520c7fbd(2014-07-05)、59ff7178(2014-08-31)、efad7ffd(2014-09-01)、69ec7cb8(2014-09-20)、82c0a2d4(2014-10-18)、c831388e(2014-10-18)、045b61d7(2015-01-18)、0ee94410(2015-07-26)、859f341e(仓库搬迁)、12f0e870(2020-11-17)、1bf7fa41(2023-03-19)。