目录

14 · data.py 与 compare.py:指令数据结构、排序与比较

核对基线 · 范围 · 依赖

核对基线:beancount 仓库 commit 97472138(2026-08-22)。路径相对仓库根目录,引用格式 文件:起-止行 (名称),行号已逐条核对。 本篇范围beancount/core/data.py(848 行)与 beancount/core/data_test.py(453 行)、beancount/core/compare.py(225 行)与 beancount/core/compare_test.py(173 行),附 beancount/core/flags.py(14 行)。 上游依赖beancount/core/account.pyhas_component)、amount.pyAmount,01 篇)、number.pyD,02 篇)、position.pyCost/CostSpec,03 篇)、beancount/utils/bisect_key.pybisect_left_with_key)。compare.py 的项目内依赖只有 core.data;标准库依赖为 hashlibtypingAnyNamedTuple)与 __future__.annotationscompare.py:3,8-13)。 下游使用者:46 个非测试文件导入 datacompare 的调用点是 beancount/parser/cmptest.py:160,202,235beancount/plugins/noduplicates.py:20beancount/parser/context.py:90beancount/scripts/doctor.py:166,175,182

1. 模块解决什么问题

data.py 定义账本被解析成什么:12 种指令各是一个 NamedTuple,加上 PostingTxnPosting 两个附属结构,再加上作用于指令流的一组工具函数——排序键、日期窗口迭代、按文件位置定位、类型自检。整个 beancount 管线(解析 → 插件 → 校验 → 报表)传递的就是 list[Directive] 加一个 options 字典,data.py 是这条流水线的公共数据模型。

compare.py 回答另一个问题:两条指令什么时候算"同一条"。元组自带的 == 会把 meta 里的 filename/lineno 一并算入,来自两个文件的等价指令永远不相等。它的办法是给每条指令算一个递归的 MD5 稳定哈希,再用哈希集合做集合运算。

2. 结构一览

名称 位置 作用
Account/Currency/Flag/Meta data.py:29-32 类型别名,均为 strdict[str, Any]
BeancountError :35-43 Protocol,规定错误对象有 source/message/entry
EMPTY_SET :46-49 共享的空 frozenset,tags/links 的空值
Booking :52-77 7 种记账消歧方法的枚举
14 个 NamedTuple 类 :93-448 12 种指令 Open Close Commodity Pad Balance Transaction Note Event Query Price Document Custom,加附属的 PostingTxnPosting
ALL_DIRECTIVES / Directive / dtypes :451-498 指令元组、类型 Union、名字空间对象
new_metadata :508-521 {"filename":…, "lineno":…} 并合入 kvlist
create_simple_posting(_with_cost) :524-597 测试与插件用的 Posting 构造器
sanity_check_types :603-643 运行期类型自检,供 ops/validation.py:340 调用
posting_has_conversion / transaction_has_conversion :646-677 "有 price 无 cost"判定
SORT_ORDER / entry_sortkey / sorted / posting_sortkey :695-747 排序规则
filter_txns / has_entry_account_component / remove_account_postings :750-828 指令流过滤
find_closest / iter_entry_dates :783-806,831-848 按文件位置、按日期窗口取指令
CompareError compare.py:16-21 重复指令错误
IGNORED_FIELD_NAMES :24-25 {"meta", "diff_amount"}
stable_hash_namedtuple :28-65 递归 MD5
hash_entry / hash_entries :68-135 单条 / 整批哈希,附重复检测
compare_entries / includes_entries / excludes_entries :138-225 相等 / 包含 / 不相交

3. 指令为什么全是不可变 NamedTuple

# All possible types of entries. These are the main data structures in use
# within the program. They are all treated as immutable.

data.py:80-81。12 个指令类连同 PostingTxnPosting 都用 class X(NamedTuple) 的类语法写(:93,118,133,155,177,206,239,272,287,313,350,372,397,427),没有一个定义方法。NamedTuple 的字段绑定不可重新赋值,替换字段值的常规做法是 _replace() 生成新对象,remove_account_postings:822-826)和 ops/balance.py:181 都是这个写法;但字段绑定的对象若本身可变(meta 字典、Open.currenciesTransaction.postingsCustom.values 等列表),仍可被原地修改,见第 4、8 节。不可变换来的是:指令可被多个索引结构同时持有而无需防拷贝,可 pickledata_test.py:421-435);但指令自身通常不可哈希——metadict,NamedTuple 的默认哈希会递归到它并抛出 TypeError,因此 compare.py(第 9 节)另行计算字符串哈希作为字典键。

这不是一开始就有的形态。2020-10-25 commit 7ee06ff7 删除了 new_directive() 工厂函数——此前每个指令类都由该工厂调用 NamedTuple(clsname, fields) 生成,改成直接声明的 class X(NamedTuple);2020-11-01 commit e8412d1bPosting/TxnPosting 做同样转换,同日 commit 40d2e38a 把字段注释搬进类 docstring。注解不只给类型检查器看:data_test.py:438-449 (test_directive_typed_named_tuples)typing.get_type_hints 遍历 ALL_DIRECTIVES,注释说明 beanquery 靠这些注解推导查询表的列类型。

Posting 不带日期也不带指向父交易的引用(:231-236),只在 Transaction.postings 列表里存在;要把两者一起传递就用 TxnPosting(txn, posting):272-284)。Posting.entry 字段在 2015-07-09 commit febfd3ab 被删掉,同日 commit 4c37f19a'entry'IGNORED_FIELD_NAMES 里去掉——它存在时会让哈希无限递归,stable_hash_namedtupleignore 参数 docstring 至今写着 "For instance, circular references to objects"(compare.py:41-42)。

4. meta:可变成员与复制约定

def new_metadata(filename: str, lineno: int, kvlist: Meta | None = None) -> Meta:
    meta = {"filename": filename, "lineno": lineno}
    if kvlist:
        meta.update(kvlist)
    return meta

data.py:508-521Meta = dict[str, Any]:32),所以每条指令里都嵌着一个可变字典。data.py:83-90 的公共字段注释指出 filenamelineno 是"always present on all directives"的两个特殊键:sanity_check_types 把它们的存在写成断言(:618-619),entry_sortkeylineno 当次级排序键(:719),printer.py:142META_IGNORE = {"filename", "lineno"} 在打印时把它们藏起来,printer.py:553 用它们拼 文件:行号: 前缀。meta 不是唯一的可变成员:Open.currencies:114)、Transaction.postings:269)、Custom.values:448)同样是 list,可以不经 _replace 直接原地修改;第 8 节的 create_simple_posting* 就是对 entry.postings 调用 .append()

不可变元组里塞一个可变字典的后果是:_replace 出来的新指令与旧指令共享同一个 meta 对象。多个核心路径在写入前显式复制:ops/balance.py:181 写作 entry._replace(meta=entry.meta.copy(), diff_amount=diff_amount)parser/booking_full.py:238-240meta = entry.meta.copy() 再写入 AUTOMATIC_TOLERANCES 键,core/interpolate.py:257 给 rounding posting 传 meta.copy()。但这不是全仓库一致遵守的约束:plugins/currency_accounts.py:68-75 只用 entry._replace(postings=new_postings) 替换 postings,仍与原指令共享同一个 meta,随后直接执行 entry.meta[META_PROCESSED] = True,同时改到了原指令引用的那个字典。data.py 本身不提供强制复制的机制;测试里反过来利用共享,data_test.py:13,288-300 让多条指令共用一个 meta。

5. tags / links:frozenset 与共享空值

# An immutable constant for all empty sets. This is used to set links and tags
# and ensure that they never has a None value. This makes some of the processing
# code a bit simpler.
EMPTY_SET: frozenset[str] = frozenset()

data.py:46-49。2016-12-04 commit 70b08ba3TransactionDocument 的 tags/links 从"可能是 None"改成"空时也是 frozenset"。CHANGES:1621-1637 记下迁移代价:插件若仍传 None 会失败,临时出路是选项 allow_deprecated_none_for_tags_and_links。该选项至今仍在(parser/options.py:692-696,标注 deprecated=),ops/validation.py:340-342 把它传给 sanity_check_types 的第二参数(data.py:604),后者据此决定 set_types 是否含 NoneType:625-635)。

解析器侧的归一化在 parser/grammar.py:1009-1023 (_finalize_tags_links)frozenset(tags) if tags else EMPTY_SET,Transaction、Note、Document 三种指令共用它。类型注解上 Transaction.tags/linksfrozenset[str]:267-268,2024-05-22 commit af6a3224 收紧),NoteDocument 的仍写作 Optional[frozenset[str]]:309-310,423-424)。

6. 排序:SORT_ORDER 与 entry_sortkey

SORT_ORDER = {Open: -2, Balance: -1, Document: 1, Close: 2}

def entry_sortkey(entry: Directive) -> tuple[datetime.date, int, int]:
    return (entry.date, SORT_ORDER.get(type(entry), 0), entry.meta["lineno"])

data.py:705-719:695-704 的注释给出全部理由:Open 必须最前;Balance 排在 Transaction 之前,因为余额断言按定义作用于当天开始的时刻;Document 排在 Transaction 之后,因为对账单日期那天往往也有交易;Close 永远最后。其余类型(TransactionPricePadNoteEventQueryCommodityCustom)都取默认 0,彼此之间只按 lineno 定序。

第三项 lineno 把"同一天同一类"的次序确定为源文件书写顺序(:88-90 也写明这点)。data_test.py:88-157 锁定结果:同为 2014-01-18 的五条按 Open(1002)、Balance(1001)、Transaction(1008)、Transaction(1009)、Close(1000) 排列——类型优先于行号。

data.sorted:722-730)只是 builtins.sorted(entries, key=entry_sortkey) 的包装,名字与内置函数冲突所以顶部要 import builtins:8)。posting_sortkey:733-747)接受 TxnPosting 或指令,先取出 txn 再算同样的三元组,键计算是复制粘贴而非调用 entry_sortkey

7. 日期窗口与位置定位

iter_entry_dates(entries, date_begin, date_end):831-848)对已按日期排好序的列表做两次二分,取 [begin, end) 半开区间:

getdate = lambda entry: entry.date
index_begin = bisect_left_with_key(entries, date_begin, key=getdate)
index_end = bisect_left_with_key(entries, date_end, key=getdate)

bisect_left_with_keybeancount/utils/bisect_key.py:25-52)是手写的二分,因为标准库 bisect 直到 Python 3.10 才有 key 参数。左闭右开意味着 date_begin == date_end 恒返回空,data_test.py:326-344 对"该日期存在"和"不存在"两种情况都锁定了空结果。函数不校验输入是否有序。

find_closest(entries, filename, lineno):783-806)名为"最近",实为线性扫描:只看 filename 完全相等且 lineno > 0 的指令,在 0 <= lineno - entry_lineno 中取差值最小者,也就是"给定行之前(含该行)最靠近的那条"。lineno > 0 这个过滤把插件生成的、行号为 0 的指令排除在外。data_test.py:232-285 锁定精确命中、向后就近、跨条切换、跨文件隔离与无解返回 None。调用点 parser/context.py:37scripts/doctor.py:318 都是"用户给了文件:行号,找出对应指令"。

8. 派生判定与构造辅助

posting_has_conversion:646-657)判定只有一行:posting.cost is None and posting.price is not None。docstring 解释这就是"换汇"——带价格但不带成本;transaction_has_conversion:660-677)在 postings 上求或,内部调用 posting_has_conversion:675),注释说这类 posting 是"non-zero conversion balances"的来源。posting_has_conversiontransaction_has_conversion 内部调用;transaction_has_conversion 本身在仓库内除测试外没有调用者,也没有其它生产路径调用这组函数。

create_simple_posting:537-562)与 create_simple_posting_with_cost:565-597)把新 posting 追加进 entry.postings 再返回——直接改了 Transaction 里那个 list,是第 4 节所说可变成员的一处直接体现。两者开头都有一句空语句 if isinstance(account, str): pass:551-552,586-587):该条件判断最早由 2013-07-20 commit c3b48690 引入,当时分支体是 account = account_from_name(account);2014-03-09 commit 9628a3f2("Removed account_from_name(). This is a big deal.")删除 account_from_name() 时把两处分支体替换成 pass,空分支保留至今。

create_simple_posting_with_cost 的成本日期原本写死为哨兵值 datetime.date(1, 1, 1),2025-01-23 commit 994c8a70(Fix #934: date(1,1,1) should be None)改成 entry.date:593)。副作用是这个函数从此不能再传 entry=None:第 593 行先取 entry.date 就会抛 AttributeError,而第 595 行 if entry is not None: 的防护已经无用;同一提交把 convert_test.pyinterpolate_test.py 里原先传 None 的调用都改成传一个真实交易。create_simple_posting 不碰 entry.date,仍支持 None

9. compare.py:稳定哈希

# Note: this routine is slow and would stand to be implemented in C.
hashobj = hashlib.md5()
for attr_name, attr_value in zip(objtuple._fields, objtuple):
    if attr_name in ignore:
        continue
    if isinstance(attr_value, (list, set, frozenset)):
        subhashes = []
        for element in attr_value:
            if isinstance(element, tuple):
                subhashes.append(stable_hash_namedtuple(element, ignore))
            else:
                md5 = hashlib.md5()
                md5.update(str(element).encode())
                subhashes.append(md5.hexdigest())
        for subhash in sorted(subhashes):
            hashobj.update(subhash.encode())
    else:
        hashobj.update(str(attr_value).encode())
return hashobj.hexdigest()

compare.py:47-65。三条规则:字段名在 ignore 里就跳过;容器字段(list/set/frozenset)逐元素求子哈希、排序后再喂进去;其余一切走 str()。第 47 行的注释"这段慢、应该用 C 实现"从 2014-09-01 commit c3e96441 起就挂在那里。

排序子哈希是为了让 tags/links 这类 frozenset 与迭代顺序无关,代价是 Transaction.postings 这个有序 list 的顺序也一并被抹掉。ignore 递归传给子元组,所以 exclude_meta=TruePosting.meta 也被跳过。

2014-11-27 commit d4bb1119 修了一个直接后果:isinstance 判断原本只写 (list, set)frozenset 落到 else 分支被 str() 掉,输出随集合迭代顺序漂移。2020-10-17 commit c193cd91(Fixed #566)修了另一个:subhashes 原本是 set,同一交易里两条完全相同的 posting 会被去重、重复未计入哈希,改成 list 后重复才计数;compare_test.py:66-90 (test_hash_entries_same_postings) 用一个含重复 posting 的交易与去掉重复的版本对比,要求哈希集合不等。

10. hash_entry 默认含 meta,比较时排除

IGNORED_FIELD_NAMES = {"meta", "diff_amount"}compare.py:24-25)。diff_amount 在列表里,是因为它不是账本输入的一部分:余额校验失败时由 ops/balance.py:181 事后填进 Balance 指令(2014-06-11 commit e066d649 修过忘记忽略它导致的假 diff)。

hash_entry(entry, exclude_meta=False):68-84)默认"包含 meta"。docstring 写明取舍:跨来源比较的单元测试要排除,因为 filename/lineno 必然不同;而"用哈希唯一标识一笔交易"时正需要它们(:73-77)。这个默认值由 2020-05-23 commit 9c1478c2(Fixed #281: Query "id" column does not identify transactions uniquely)翻转——此前无条件排除 meta,查询的 id 列区分不了同日同额的两笔交易。parser/context.py:90 走默认路径,plugins/noduplicates.py:20 与三个比较函数一律传 exclude_meta=True

compare_test.py:152-169 (test_hash_with_exclude_meta) 把两种模式一次锁死:同一文件里两条逐字相同的交易,exclude_meta=False 哈希不等(行号不同),exclude_meta=True 哈希相等。

11. 重复检测与 Price 的特例

if hash_ in entry_hash_dict:
    if isinstance(entry, Price):
        # Note: Allow duplicate Price entries, they should be common
        # because of the nature of stock markets ...
        num_legal_duplicates += 1
    else:
        other_entry = entry_hash_dict[hash_]
        errors.append(CompareError(entry.meta, "Duplicate entry: ...", entry))
entry_hash_dict[hash_] = entry

compare.py:111-127Price 是唯一被放行的重复:注释解释市场休市时数据源会重复返回上一交易日的价格(2014-08-01 commit 34f5d7c3)。这与 Price 自己的 docstring 形成对照——data.py:377-381 说同一天多条 price 指令"makes no sense",但不打算解决。

无论是否重复,第 127 行都会把 entry_hash_dict[hash_] 覆盖成后来那条。函数末尾有一个只在无错误时才跑的一致性断言(:129-134):len(entry_hash_dict) + num_legal_duplicates == len(entries)

compare_entries:138-170)、includes_entries:173-197)、excludes_entries:200-225)结构相同:两侧各算一遍哈希字典,任一侧有 CompareError 就把第一个包成 ValueError 抛出,再在 key 集合上做差集 / 子集 / 交集,结果用 data.sorted 排序后返回(2014-06-03 commit 30d44591)。parser/cmptest.py 把它们包装成测试断言。

12. 设计决策与理由

决策 理由 证据
12 种指令各一个 NamedTuple,无方法、无共同基类 哑数据结构;靠 isinstance 分派,ALL_DIRECTIVES 元组即可用于类型断言 data.py:80-81,451-465:616
字段用类语法加类型注解 注解在运行期可读,beanquery 用它推导列类型 data_test.py:438-449;commit 7ee06ff7
meta 是可变 dict 而非元组 元数据是任意键值,且插件需要在既有指令上追加;多数核心路径修改前显式 .copy(),但非强制约束 data.py:32ops/balance.py:181booking_full.py:238plugins/currency_accounts.py:68-75
空 tags/links 用共享 EMPTY_SET 而非 None 下游不必到处判空 data.py:46-49CHANGES:1621-1630
排序键第二项按类型给固定权重 同一天的语义次序(Open→Balance→其它→Document→Close)与文件书写顺序无关 data.py:695-705
排序键第三项是 lineno 同类型同日的先后由源文件决定,结果可复现 data.py:88-90,719
哈希用 str() 递归而非 hash() 内置 hash 对 str 加了每进程随机盐,跨进程不稳定 compare.py:59,64
容器字段的子哈希先排序 让 frozenset 字段与迭代顺序无关 compare.py:61;commit d4bb1119
子哈希用 list 而非 set 收集 重复元素必须计入,否则重复 posting 未计入哈希 compare.py:53;commit c193cd91
hash_entry 默认包含 meta 哈希被当作交易的唯一 id 使用,必须含文件名行号 compare.py:73-77;commit 9c1478c2
比较函数一律 exclude_meta=True 两侧来自不同文件,位置信息必然不同 compare.py:158-159,186-188,213-215
diff_amountmeta 一同忽略 它是校验阶段回填的派生字段,不是输入身份的一部分 compare.py:25;commit e066d649
只有 Price 允许重复 行情源在休市日重复返回同一价格 compare.py:112-117

13. 行为细节与边界

现象 后果 证据
_replace 不复制 meta 新旧指令共享同一字典,写入会同时改到旧对象 data.py:32,822-826
哈希对 Decimal 小数位敏感 Decimal("100")Decimal("100.00") 元组相等但 str() 不同,哈希不同 compare.py:64
字段哈希直接拼接,无分隔符 相邻两个字符串字段的边界不可分辨,理论上可构造碰撞 compare.py:64
非容器字段一律 str() None 与字符串 "None" 哈希相同 compare.py:64
postings 是 list 但子哈希被排序 posting 顺序不影响哈希,顺序相反的两笔交易被判为同一条 compare.py:61
hash_entries 后写覆盖先写 重复时字典里留下的是最后一条 compare.py:127
entry_sortkey 直接取 meta["lineno"] 缺该键的指令抛 KeyError data.py:719
iter_entry_dates 不校验输入有序 输入未排序时二分结果无意义 data.py:844-846
find_closest 跳过 lineno <= 0 插件生成的指令(行号 0)永远不会被返回 data.py:801
find_closest 是线性扫描 名字暗示的索引结构不存在,每次调用 O(n) data.py:799-805
create_simple_posting* 修改 entry.postings 在"指令不可变"的前提下留了一处原地写 data.py:560-561,595-596
create_simple_posting_with_cost 不再接受 entry=None :593 先取 entry.date:595 的 None 防护已失效 commit 994c8a70
sanity_check_typesassert python -O 下整个自检被剥离 data.py:616-643
sanity_check_types 只检查 Transaction 的 tags/links Note/Document 的这两个字段不受检 data.py:621-635
【文档漂移】Balance docstring 字段顺序 docstring 依次写 amountdiff_amounttolerance,定义顺序是 amounttolerancediff_amount;按位置构造会装错 data.py:190-195 vs :201-203
【文档漂移】Posting docstring 提到 entry 字段 "that's what the entry field should be set to",但该字段 2015 年已删除 data.py:209-211 vs :231-236
【文档漂移】Note docstring 不含 tags/links 2021-02-06 commit 6bca6e5b 加了字段没更 docstring data.py:296-303 vs :309-310
【文档漂移】Custom docstring 不含 date 属性表跳过了 date,字段里有 data.py:437-443 vs :445-448
【文档漂移】Transaction.flag 注解与运行期检查不一致 注解是 Optional[Flag](2025-07-04 commit 33996672),sanity_check_types 却要求 isinstance(entry.flag, str)(2024-12-24 commit afb42d6b);解析器产出 chr(flag) 恒为 str data.py:264 vs :622grammar.py:1142
【文档漂移】Transaction.narration docstring 说"never None" 注解是 Optional[str],自检也放行 None data.py:254-255 vs :266,624
【文档漂移】Open.currencies docstring 说可为 None 注解是 list[Currency],非 Optional;测试直接传字符串 "USD" data.py:101-103 vs :114data_test.py:101
【文档漂移】Note/Document 的 tags/links 说"None if empty" 2016 年起解析器只产出 EMPTY_SET data.py:415-416 vs grammar.py:1020-1023
【文档漂移】new_metadatakvlist 注解为 Meta | None 解析器实际传的是 KeyValue 具名元组的列表,靠 dict.update 接受二元序列才能工作 data.py:508,520grammar.py:65-71,644

14. 测试锁定了什么

覆盖方式:data_test.py 全部手工构造指令,不经解析器;compare_test.py 相反,一律用 loader.load_string 加载一段账本文本再比较。

测试 行号 锁定的行为
test_create_simple_posting(_with_cost) data_test.py:30-44 返回值就是被追加进 entry.postings[0] 的那个对象
test_sanity_check_types :46-66 合法交易通过;字符串 / dict / date / flag=1 / payee=1 / narration=1 / tags={} / links={} / postings=None 各自抛 AssertionError
test_posting_has_conversion / test_transaction_has_conversion :68-80 无 price 为假,_replace(price=…) 后为真
test_get_entry :82-86 指令与 TxnPosting 都能取回交易
test_entry_sortkey / test_sort :159-167 同日七条的类型序与行号序(见第 6 节)
test_posting_sortkey :169-197 TxnPosting 与裸指令混排结果同上
test_filter_txns :199-203 七条里筛出四条 Transaction
test_has_entry_account_component :205-230 组件必须整段匹配
test_find_closest :232-285 精确命中、向后就近、跨条切换、跨文件隔离、无解返回 None
test_remove_account_postings :287-307 指令条数不变,只有目标账户的 posting 被摘除
test_iter_entry_dates :309-418 九种区间:端点重合、端点缺失、单侧缺失、全越界
test_data_tuples_support_pickle :421-435 Transaction 可 pickle 往返
test_directive_typed_named_tuples :438-449 12 个指令类的注解都能被 get_type_hints 解出
test_hash_entries compare_test.py:38-47 同一文本加载 64 遍,哈希键集合必须完全一致
test_hash_entries_with_duplicates :49-64 1 条 price 与 5 条相同 price 都只得到 1 个哈希
test_hash_entries_same_postings :66-90 含重复 posting 的交易与去重版本哈希不同
test_compare_entries :92-126 相等、单侧多一条、双侧各缺一条,missing 的类型与条数
test_includes_entries / test_excludes_entries :128-150 子集判定与不相交判定
test_hash_with_exclude_meta :152-169 同文件两条相同交易:含 meta 不等、排除 meta 相等

没有覆盖的行为:CommodityPadEventQueryCustom 五种指令在 data_test.py 里从未被构造;data_test.py 没有直接单测 new_metadata(..., kvlist)(该参数通过解析器元数据测试间接覆盖,grammar.py:644 的所有指令构造路径都会传入 kvlistgrammar_test.py:1408-1417,1445-1466,1511-1528,1562-1594 直接断言合并后的键和值);sanity_check_typesallow_none_for_tags_and_links=True 分支与 Note/Document 路径;posting_sortkeyassert 分支;iter_entry_dates 输入未排序的情形;stable_hash_namedtuple 的自定义 ignoreCompareError.source/CompareError.entry 的具体内容(message 已由 noduplicates_test.py:12-16 覆盖);重复 Price 场景下 hash_entries 返回的 errors 是否为空没有显式断言(末尾一致性断言已被现有无错误调用执行,见 compare_test.py:38-47,但没有专门构造断言失败的路径);dtypesBeancountError 本身。

15. 演变史

日期 提交 / 记录 变化
2013-07-20 / 2014-03-09 c3b48690 / 9628a3f2 create_simple_posting*account 分支体从 account_from_name(account) 改为空 pass9628a3f2 删除 account_from_name()),空分支残迹见第 8 节
2014-08-01 34f5d7c3 hash_entries 放行重复 Price
2014-06-11 e066d649 diff_amount 加入忽略字段,修假 diff
2014-06-03 30d44591 比较函数返回的 missing 列表改为排序输出
2014-09-01 c3e96441 加上"这段慢、应该用 C 实现"的注释
2014-10-12 / 2015-05-09 819e81c3 / b58e942c 新增 data.sort(),随后改名 data.sorted()
2014-11-27 d4bb1119 哈希的容器判断补上 frozenset
2014-07-19 / 2014-12-25 16206c33 / 7a20367b 位置字段两次改名:FileLocationSourcemeta
2015-07-09 febfd3ab + 4c37f19a 删除 Posting.entry,同时从 IGNORED_FIELD_NAMES 移除 'entry'
2016-10-23 01acc5c9 Document 支持 tags 与 links
2016-12-04 70b08ba3CHANGES:1621-1637 tags/links 空值统一为 EMPTY_SET,并加选项 allow_deprecated_none_for_tags_and_links
2019-04-17 / 2019-05-01 249af500 / 4d975080 尝试让 txn 关键字产生 flag=None,随后回退(#295)
2020-05-23 9c1478c2CHANGES:283-285 hash_entry 默认改为包含 meta,修 #281
2020-10-17 c193cd91CHANGES:143-144 子哈希容器由 setlist,修 #566
2020-10-25 / 2020-11-01 7ee06ff7 / e8412d1b / 40d2e38a 指令改为带注解的 NamedTuple 类,注释移入 docstring
2021-02-06 6bca6e5b Note 支持 tags 与 links
2021-02-20 / 2022-06-22 0d46c7cd / 3e5b4ae0 Booking 新增 STRICT_WITH_SIZEHIFO
2024-05-21 / 2024-05-22 201e695e / af6a3224 Posting.unitsOptional[Amount];Transaction 的 tags/links 改 FrozenSet
2024-12-09 c765939e 澄清 posting_sortkey 接收 TxnPosting 或指令,新增参数类型注解及运行期 assert isinstance(entry, (TxnPosting,) + ALL_DIRECTIVES)(#887)
2024-12-24 / 2025-07-04 afb42d6b / 33996672 运行期 flag 检查收紧为 str;半年后注解放宽为 Flag | None
2025-01-23 994c8a70 create_simple_posting_with_cost 的哨兵日期 date(1,1,1) 改为 entry.date(#934)

16. 与其他模块的关系

17. 参考索引

beancount/core/data.py:1 模块 docstring;8-26 导入;29-32 类型别名;35-43 BeancountError;46-49 EMPTY_SET;52-77 Booking;80-90 公共字段注释;93-115 Open;118-130 Close;133-152 Commodity;155-174 Pad;177-203 Balance;206-236 Posting;239-269 Transaction;272-284 TxnPosting;287-310 Note;313-347 Event;350-369 Query;372-394 Price;397-424 Document;427-448 Custom;451-465 ALL_DIRECTIVES;467-481 Directive;484-498 dtypes;501-505 Entries/Directives/Options;508-521 new_metadata;524-562 create_simple_posting;565-597 create_simple_posting_with_cost;600 NoneType;603-643 sanity_check_types;646-657 posting_has_conversion;660-677 transaction_has_conversion;680-692 get_entry;695-705 SORT_ORDER;708-719 entry_sortkey;722-730 sorted;733-747 posting_sortkey;750-764 filter_txns;767-780 has_entry_account_component;783-806 find_closest;809-828 remove_account_postings;831-848 iter_entry_dates

beancount/core/compare.py:16-21 CompareError;24-25 IGNORED_FIELD_NAMES;28-65 stable_hash_namedtuple(47 慢注释、52-62 容器分支、61 排序、63-64 else);68-84 hash_entry;87-135 hash_entries(111-127 重复处理、112-117 Price 特例、129-134 断言);138-170 compare_entries;173-197 includes_entries;200-225 excludes_entries

测试data_test.py:13,17,30-44,46-66,68-80,82-86,88-138,140-157,159-197,199-230,232-285,287-307,309-418,421-435,438-449compare_test.py:10-34,38-47,49-64,66-90,92-126,128-150,152-169

其它beancount/core/flags.py:8-14beancount/utils/bisect_key.py:25-52beancount/parser/grammar.py:65-71,223,630-866,1009-1023,1142beancount/parser/printer.py:142,553beancount/parser/context.py:37,90beancount/parser/cmptest.py:160,202,235beancount/parser/booking_full.py:238-240beancount/parser/options.py:692-696beancount/ops/validation.py:340-342beancount/ops/balance.py:181beancount/ops/documents.py:65,176-181beancount/core/interpolate.py:257beancount/plugins/noduplicates.py:20beancount/scripts/doctor.py:166,175,182,318beancount/api.py:46,87-98CHANGES:143-144,283-285,1621-1637

commitc3b48690(2013-07-20)、9628a3f2(2014-03-09)、30d44591(2014-06-03)、e066d649(2014-06-11)、16206c33(2014-07-19)、34f5d7c3(2014-08-01)、c3e96441(2014-09-01)、819e81c3(2014-10-12)、d4bb1119(2014-11-27)、7a20367b(2014-12-25)、b58e942c(2015-05-09)、febfd3ab/4c37f19a(2015-07-09)、01acc5c9(2016-10-23)、70b08ba3(2016-12-04)、249af500(2019-04-17)、4d975080(2019-05-01)、9c1478c2(2020-05-23)、3ea6d9f6(2020-08-11)、c193cd91(2020-10-17)、7ee06ff7(2020-10-25)、e8412d1b/40d2e38a(2020-11-01)、6bca6e5b(2021-02-06)、0d46c7cd(2021-02-20)、3e5b4ae0(2022-06-22)、201e695e(2024-05-21)、af6a3224(2024-05-22)、c765939e(2024-12-09)、afb42d6b(2024-12-24)、994c8a70(2025-01-23)、33996672(2025-07-04)。