"""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_validate(validation_test.py:348-377)里
booking 先报出一条 "No position matches",随后 validation
照常运行并另外报出一条 "Invalid currency";但未被阶段内部转换为 errors
的异常仍会中断流水线,loader.py:605-607 对
booking.book 的调用没有异常捕获(例见 09 篇记录的
CostSpec/validate_missing_eliminated
边界:beancount/parser/booking_full.py:986-995、beancount/parser/booking.py:72-77)。插件可以任意增删改指令。validation.py
检查的是"整条指令流作为一个整体是否自洽"——账户在被引用时是否处于开启区间、每笔交易最终是否平账、Balance
断言有没有自相矛盾。
关键限定词是 docstring 里的 "not sanity checks":这些不是防御程序员写错的内部断言,而是用户数据本身的约束,检查结果要以错误列表的形式回传给用户。
| 名称 | 位置 | 作用 |
|---|---|---|
class ValidationError(NamedTuple) |
validation.py:29-34 |
三字段错误记录:source、message、entry |
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 |
依次调用清单里的函数并汇总错误 |
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__ordering(validation_test.py:72-81)把
Open 写在源文本靠前、Close 写在靠后,但 Close
的日期更早,加载排序后仍是这条路径。"Internal error: closing date ...
appears before opening date" 只有在传入 validate_open_close
的 entries 违反了这条排序不变式——即先遍历到
Open、后遍历到日期更早的同账户 Close——时才会触发,正常 loader
流程不会走到它。
open_map 与 close_map
都只在首次登记(:84,116),重复出现的第二条不覆盖第一条,因此
Close 日期永远与最早那条 Open 比较。
validate_duplicate_balances:Balance 断言的一致性key 是 (account, currency, date);同 key 的首条 Balance
被保存进 balance_entries 并作为基准不再更新,之后每一条同
key 的 Balance
都与这条基准比较,金额不同则报错、完全相同则放行(validation.py:143-155)。docstring
给的理由:beancount
不支持时间,指令的书写顺序被设计成无关紧要,所以同一天同一账户不可能有两个不同的余额;但完全相同的两条断言是允许的,因为导入流程可能重复生成。
validate_duplicate_commodities:Commodity 指令唯一key 是
entry.currency,第二条起报错(validation.py:177-187)。
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_sortkey(data.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 同样报错。
validate_currency_constraints:Open 声明的币种列表先把带币种声明的 Open 收进
open_map(validation.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.currency 和
price.currency。测试里 2014-01-04 那笔
Assets:Account2 1 HOOL {500 USD} 在账户只声明
HOOL
的情况下通过(validation_test.py:243-249),确认成本币种不受这条约束管辖。没有声明币种的账户直接跳过。
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_documents(beancount/ops/documents.py:92-185):113-117
行把搜索根目录转换成绝对路径,account.walk 据此产生
root(127-129 行),175-183 行为每个匹配文件构造
Document,filename 由
path.join(root, filename)(179
行)生成,同样是绝对路径。走到这里还是相对路径,说明是插件生成的
Document 没遵守约定。
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-346。data.sanity_check_types(beancount/core/data.py:603-643)是一串
assert isinstance:指令类型、meta 是 dict 且含
filename/lineno、date 是
datetime.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。
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,字典里查不到的币种容差取
ZERO(beancount/core/inventory.py:152-169)。
infer_tolerances 的 mode 参数在这里用默认值
"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)。
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_PRE(beancount.ops.documents)+
用户插件 + PLUGINS_AUTO +
PLUGINS_POST(beancount.ops.pad、beancount.ops.balance),"raw"
时只跑用户插件(loader.py:54-68)。无论哪种模式,validation
都在其后运行,所以 "raw" 模式下 pad
不再合成补差交易、balance 不再做断言,但平账校验和账户区间校验照跑。
| 决策 | 理由 | 证据 |
|---|---|---|
| 全部校验放在插件之后 | 插件可以任意改写指令流;"先写不平衡交易、靠插件补平"是合法用法,提前判定会误报 | validation.py:1-9,365-372;loader.py:623-628 |
| 返回错误列表而非抛异常 | 一次加载要报出全部问题;错误随 load_file
的返回值上浮,而不是中断加载 |
validation.py:428-436;loader.py:628 |
错误对象是带 source meta 的 NamedTuple |
与全仓库的 data.BeancountError
协议一致,printer.format_error 能统一渲染成
filename:lineno: 加消息加指令原文 |
validation.py:29-34;data.py:35-43;printer.py:556-572 |
| 按速度分 BASIC/HARDCORE 两组,不提供选项 | 作者希望日后优化 validate_data_types
后合并,因此不固化成用户选项 |
validation.py:400-404;scripts/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
c382d21f、12f0e870 |
| 平账重算而不是复用 booking 的结论 | 插件改过 postings 之后,booking 阶段的结论已经失效 | validation.py:360-375 |
| 现象 | 后果 | 证据 |
|---|---|---|
validation_tests = VALIDATIONS 后用
+= |
list 的 += 是原地扩展,VALIDATIONS 与
BASIC_VALIDATIONS 是同一个对象,传
extra_validations
会把它们永久追加进全局清单;同一进程内反复调用会不断累积、重复执行 |
validation.py:423-425 |
validate_duplicate_commodities 里的
if previous_entry: |
恒真(Commodity 是非空 NamedTuple),该分支等价于无条件报错 | validation.py:180 |
getters 对 Custom 指令返回空账户集 |
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:289;validation_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-114;loader.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-643;validation.py:339-346 |
entry 未通过首条
assert isinstance(entry, ALL_DIRECTIVES) 且自身没有
meta 属性 |
except AssertionError 分支里的 entry.meta
转而抛出 AttributeError,不会被转换成
ValidationError |
data.py:616-620;validation.py:339-346 |
validate 与各校验器本身都不排序
entries,只按接收顺序遍历 |
校验器假定调用方提供按 entry_sortkey 排序的
entries;loader
在解析后(:602)与每个插件运行后(:740)都重新排序以满足这一前置条件 |
loader.py:596-602,738-740;validation.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:319;validation_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_constraints、validate_documents_paths
的形参名为 options_map 却未使用 |
其余函数用 unused_options_map
命名区分;validate_data_types 虽也叫
options_map,但第 341 行实际读取了它,不属于此列 |
validation.py:250,303,341 |
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__ordering(validation_test.py:72-81)经
loader 排序后实际走的是 "Unopened account" 路径,且只断言
error.entry.account、不断言
error.message,全仓库测试都没有出现过这条错误文本。
validation_test.py 未直接测试
extra_validations 参数与非 None 的
log_timings。全仓库范围内,example_test.py(19-22、30-33
行)通过 loader.load_string 间接执行了
extra_validations 与 HARDCORE_VALIDATIONS
的组合,loader_test.py(121-126、135-140
行)通过完整加载流程两次传入非 None 的
log_timings 做冒烟覆盖;但没有测试断言
extra_validations 不会污染
VALIDATIONS/BASIC_VALIDATIONS
全局列表,也没有测试校验器不会因这种污染而累积重复执行。生产代码里
scripts/check.py:72-78、scripts/example.py:1167-1169
也会传入 HARDCORE_VALIDATIONS。
| 日期 | 提交 | 变化 |
|---|---|---|
| 2014-06-26 | c382d21f |
Document 与 Note 允许出现在关户之后 |
| 2014-07-05 | 878741b2 |
加入 validate_data_types 与
validate_duplicates |
| 2014-07-05 | 520c7fbd |
拆出 BASIC_VALIDATIONS /
HARDCORE_VALIDATIONS,validate_check_balances、validate_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_types 从 BASIC_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.py(859f341e
是搬迁提交)。
beancount/loader.py:31
导入,:623-628 在 run_transformations
之后调用。extra_validations 从 load_file /
load_string
一路透传(loader.py:93,135,345)。interpolate.compute_residual
内部走 convert.get_weight。booking 阶段自己的错误(如 "No
position matches")在 validation
之前就已产生,validation_test.py:373
确认两类错误分属不同阶段。PLUGINS_POST(loader.py:65-68),pad
合成的补差交易与其账户引用会被
validate_active_accounts、validate_check_transaction_balances
一并检查。beancount/plugins/auto_accounts.py:29-40
只为 entries 里完全没有 Open 指令的账户,在其首次使用日期插入
Open(meta, date_first_used, account, None, None)
并重排(判定条件是
account not in opened_accounts,:34)。它因此通常消除完全没有
Open 指令账户的 unknown 错误;但账户如果已有一条
Open(哪怕日期晚于某次引用,或该账户后来被 Close
又在关闭后被引用),都不会获得补充
Open,validate_active_accounts 仍可能报 inactive。因为
currencies 传
None,validate_currency_constraints 对新插入
Open 的账户全部跳过。它由 --auto 经
DEFAULT_PLUGINS_AUTO
挂入(loader.py:59-63),也可由账本里的
plugin "beancount.plugins.auto_accounts"
显式加载,validation_test.py
的四个容差测试就用了后一种写法。process_documents(beancount/ops/documents.py:35-67,经
__plugins__ 于 :17 声明)在
PLUGINS_PRE(loader.py:54-56);其内部调用的
find_documents(documents.py:92-185)自动发现的
Document 已是绝对路径;validate_documents_paths
是这条不变式的事后检查。plugins/nounused.py)、重复交易(plugins/noduplicates.py)、歧义价格(plugins/unique_prices.py)都曾在本模块内,现在是可选插件,plugins/pedantic.py
把它们打包成一组。ValidationError 与
PadError(ops/pad.py:21-26)等一样满足
data.BeancountError(data.py:35-43)的三属性协议,因此能被
printer.print_errors 统一输出。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-742;beancount/core/data.py:35-43,603-643,705,708-719;beancount/core/getters.py:59-71,73-82,84-92,94-102,104-112,115-116,147-158;beancount/core/interpolate.py:72-94,97-231,236-239;beancount/core/inventory.py:152-169;beancount/parser/options.py:614-631,680-699,700-717;beancount/parser/grammar.py:816-819;beancount/parser/printer.py:543-552,556-572;beancount/ops/documents.py:17,35-67,92-185;beancount/ops/pad.py:18,21-26;beancount/ops/balance.py:17;beancount/plugins/auto_accounts.py:29-42;beancount/plugins/pedantic.py:17-27;beancount/scripts/check.py:72-78;beancount/scripts/example.py:1167-1169;beancount/scripts/example_test.py:19-22,30-33;beancount/loader_test.py:121-126,135-140;beancount/utils/misc_utils.py:15-36;CHANGES:3277-3281,3311-3320。
commit:c382d21f(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)。