data.py 定义账本被解析成什么:12 种指令各是一个
NamedTuple,加上
Posting、TxnPosting
两个附属结构,再加上作用于指令流的一组工具函数——排序键、日期窗口迭代、按文件位置定位、类型自检。整个
beancount 管线(解析 → 插件 → 校验 → 报表)传递的就是
list[Directive] 加一个 options 字典,data.py
是这条流水线的公共数据模型。
compare.py
回答另一个问题:两条指令什么时候算"同一条"。元组自带的 ==
会把 meta 里的 filename/lineno
一并算入,来自两个文件的等价指令永远不相等。它的办法是给每条指令算一个递归的
MD5 稳定哈希,再用哈希集合做集合运算。
| 名称 | 位置 | 作用 |
|---|---|---|
Account/Currency/Flag/Meta |
data.py:29-32 |
类型别名,均为 str 或 dict[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,加附属的
Posting、TxnPosting |
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 |
相等 / 包含 / 不相交 |
# 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 个指令类连同
Posting、TxnPosting 都用
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.currencies、Transaction.postings、Custom.values
等列表),仍可被原地修改,见第 4、8
节。不可变换来的是:指令可被多个索引结构同时持有而无需防拷贝,可
pickle(data_test.py:421-435);但指令自身通常不可哈希——meta
是 dict,NamedTuple 的默认哈希会递归到它并抛出
TypeError,因此 compare.py(第 9
节)另行计算字符串哈希作为字典键。
这不是一开始就有的形态。2020-10-25 commit 7ee06ff7
删除了 new_directive()
工厂函数——此前每个指令类都由该工厂调用
NamedTuple(clsname, fields) 生成,改成直接声明的
class X(NamedTuple);2020-11-01 commit
e8412d1b 对 Posting/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_namedtuple 的
ignore 参数 docstring 至今写着 "For instance, circular
references to objects"(compare.py:41-42)。
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-521。Meta = dict[str, Any](:32),所以每条指令里都嵌着一个可变字典。data.py:83-90
的公共字段注释指出 filename 与 lineno
是"always present on all
directives"的两个特殊键:sanity_check_types
把它们的存在写成断言(:618-619),entry_sortkey
拿 lineno
当次级排序键(:719),printer.py:142 用
META_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-240
先 meta = 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。
# 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 70b08ba3
把 Transaction 和 Document 的 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/links 是
frozenset[str](:267-268,2024-05-22 commit
af6a3224 收紧),Note 与 Document
的仍写作
Optional[frozenset[str]](:309-310,423-424)。
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
永远最后。其余类型(Transaction、Price、Pad、Note、Event、Query、Commodity、Custom)都取默认
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。
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_key(beancount/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:37、scripts/doctor.py:318
都是"用户给了文件:行号,找出对应指令"。
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_conversion 由
transaction_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.py、interpolate_test.py 里原先传
None
的调用都改成传一个真实交易。create_simple_posting 不碰
entry.date,仍支持 None。
# 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=True 时
Posting.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 的交易与去掉重复的版本对比,要求哈希集合不等。
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 哈希相等。
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-127。Price
是唯一被放行的重复:注释解释市场休市时数据源会重复返回上一交易日的价格(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 种指令各一个 NamedTuple,无方法、无共同基类 | 哑数据结构;靠 isinstance
分派,ALL_DIRECTIVES 元组即可用于类型断言 |
data.py:80-81,451-465;:616 |
| 字段用类语法加类型注解 | 注解在运行期可读,beanquery 用它推导列类型 | data_test.py:438-449;commit 7ee06ff7 |
meta 是可变 dict 而非元组 |
元数据是任意键值,且插件需要在既有指令上追加;多数核心路径修改前显式
.copy(),但非强制约束 |
data.py:32;ops/balance.py:181;booking_full.py:238;plugins/currency_accounts.py:68-75 |
空 tags/links 用共享 EMPTY_SET 而非
None |
下游不必到处判空 | data.py:46-49;CHANGES: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_amount 与 meta 一同忽略 |
它是校验阶段回填的派生字段,不是输入身份的一部分 | compare.py:25;commit e066d649 |
只有 Price 允许重复 |
行情源在休市日重复返回同一价格 | compare.py:112-117 |
| 现象 | 后果 | 证据 |
|---|---|---|
_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_types 用 assert |
python -O 下整个自检被剥离 |
data.py:616-643 |
sanity_check_types 只检查 Transaction 的
tags/links |
Note/Document 的这两个字段不受检 |
data.py:621-635 |
【文档漂移】Balance docstring 字段顺序 |
docstring 依次写
amount、diff_amount、tolerance,定义顺序是
amount、tolerance、diff_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
:622;grammar.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
:114;data_test.py:101 |
【文档漂移】Note/Document 的 tags/links
说"None if empty" |
2016 年起解析器只产出 EMPTY_SET |
data.py:415-416 vs
grammar.py:1020-1023 |
【文档漂移】new_metadata 的 kvlist 注解为
Meta | None |
解析器实际传的是 KeyValue 具名元组的列表,靠
dict.update 接受二元序列才能工作 |
data.py:508,520;grammar.py:65-71,644 |
覆盖方式: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 相等 |
没有覆盖的行为:Commodity、Pad、Event、Query、Custom
五种指令在 data_test.py
里从未被构造;data_test.py 没有直接单测
new_metadata(..., kvlist)(该参数通过解析器元数据测试间接覆盖,grammar.py:644
的所有指令构造路径都会传入
kvlist,grammar_test.py:1408-1417,1445-1466,1511-1528,1562-1594
直接断言合并后的键和值);sanity_check_types 的
allow_none_for_tags_and_links=True 分支与
Note/Document
路径;posting_sortkey 的 assert
分支;iter_entry_dates
输入未排序的情形;stable_hash_namedtuple 的自定义
ignore;CompareError.source/CompareError.entry
的具体内容(message 已由
noduplicates_test.py:12-16 覆盖);重复 Price 场景下
hash_entries 返回的 errors
是否为空没有显式断言(末尾一致性断言已被现有无错误调用执行,见
compare_test.py:38-47,但没有专门构造断言失败的路径);dtypes
与 BeancountError 本身。
| 日期 | 提交 / 记录 | 变化 |
|---|---|---|
| 2013-07-20 / 2014-03-09 | c3b48690 / 9628a3f2 |
create_simple_posting* 的 account 分支体从
account_from_name(account) 改为空
pass(9628a3f2 删除
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 |
位置字段两次改名:FileLocation→Source→meta |
| 2015-07-09 | febfd3ab + 4c37f19a |
删除 Posting.entry,同时从
IGNORED_FIELD_NAMES 移除 'entry' |
| 2016-10-23 | 01acc5c9 |
Document 支持 tags 与 links |
| 2016-12-04 | 70b08ba3(CHANGES: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 | 9c1478c2(CHANGES:283-285) |
hash_entry 默认改为包含 meta,修 #281 |
| 2020-10-17 | c193cd91(CHANGES:143-144) |
子哈希容器由 set 改 list,修 #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_SIZE、HIFO |
| 2024-05-21 / 2024-05-22 | 201e695e / af6a3224 |
Posting.units 改
Optional[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) |
account.has_component(供
has_entry_account_component)、amount.Amount(Balance.amount、Price.amount、Posting.units/price)、number.D(两个
posting
构造器)、position.Cost/CostSpec(Posting.cost)、utils.bisect_key(iter_entry_dates)。compare.py
的项目内依赖只有 core.data。parser/grammar.py
是唯一的账本级构造入口,Builder
各方法一一对应指令(:630-866,1025-1142),并负责
new_metadata 与 tags/links
归一化;ops/documents.py:176-181 从目录树生成
Document,ops/pad.py、ops/summarize.py
生成带 flags.py 特殊 flag 的交易。loader.py:602,740、parser/grammar.py:223、core/prices.py:43、ops/documents.py:65、ops/summarize.py:513、plugins/auto_accounts.py:40
用
entry_sortkey;core/realization.py:363、ops/pad.py:72
用 posting_sortkey。ops/validation.py:340-342
把 sanity_check_types 的断言失败包成
ValidationError,选项从 options map 读入。parser/cmptest.py:160,202,235
把三个集合函数包成测试断言;plugins/noduplicates.py:20
报重复;parser/context.py:90 与
scripts/doctor.py:166,175,182 输出指令 id
并做打印-重解析的往返检查。beancount/api.py:46,87-98 把
dtypes 及各指令类型重新导出到顶层名字空间。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-449;compare_test.py:10-34,38-47,49-64,66-90,92-126,128-150,152-169。
其它:beancount/core/flags.py:8-14;beancount/utils/bisect_key.py:25-52;beancount/parser/grammar.py:65-71,223,630-866,1009-1023,1142;beancount/parser/printer.py:142,553;beancount/parser/context.py:37,90;beancount/parser/cmptest.py:160,202,235;beancount/parser/booking_full.py:238-240;beancount/parser/options.py:692-696;beancount/ops/validation.py:340-342;beancount/ops/balance.py:181;beancount/ops/documents.py:65,176-181;beancount/core/interpolate.py:257;beancount/plugins/noduplicates.py:20;beancount/scripts/doctor.py:166,175,182,318;beancount/api.py:46,87-98;CHANGES:143-144,283-285,1621-1637。
commit:c3b48690(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)。