parser.parse_file
只负责一个文件的文本:产出的指令里还留着待推算的数字,pad、balance
这类要看到全局余额才能兑现的指令还是原样,include 只是记在
options 里的一串文件名。loader.py
是把这些补齐的唯一入口,对外只做一件事——给一个顶层文件名或一段字符串,拿回
(entries, errors, options_map)。它决定四件别处决定不了的事:阶段顺序(解析
→ booking → 插件 →
校验)、内置插件相对用户插件的前后位置、多份文件的选项如何并成一份、能否复用上次的加载结果。
| 名称 | 位置 | 作用 |
|---|---|---|
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.pad、beancount.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_currency、dcontext、pythonpath |
_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 |
装配缓存策略,模块导入时按环境变量执行一次 |
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_recursive 里
assert path.isabs(source)(beancount/loader.py:432)的前提。加密文件走单独分支且明确不进缓存。非加密分支调的是模块级名字
_load_file,由 initialize 绑定(第 4
节),这层间接使缓存可开关、可替换。两个分支打印错误的位置不同:加密分支由它转交的
load_string 调 _log_errors。
load_string 与 load_encrypted_fileload_string(beancount/loader.py:341-373)可选先
textwrap.dedent(供 load_doc 用缩进的
docstring),再调 _load([(string, False)], ...),元组第二位
False
表示这是内容而非文件名。load_encrypted_file(beancount/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,与缓存无关。
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_filename(beancount/loader.py:180-195)算出:模板是绝对路径就直接用,否则拼到顶层文件目录下,{filename}
填 basename,默认落在账本旁成为
.apples.beancount.picklecache。compute_input_hash(beancount/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)。
pickle_cache_function(beancount/loader.py:198-277)的包装按序做:读缓存
→ 反序列化 → needs_refresh 判定 → 命中即
return result。三处容错都不抛异常:反序列化
except Exception 全兜(注释说损坏或过期的 pickle
会以各种异常类型冒出来)后 logging.error
重算(beancount/loader.py:231-241),删旧缓存失败与写缓存失败各只
warning(beancount/loader.py:250-259、beancount/loader.py:266-274)。写盘还有门槛:耗时
>= PICKLE_CACHE_THRESHOLD(1.0
秒)才落盘(beancount/loader.py:264-273)。delete_cache_function(beancount/loader.py:280-301,缓存关闭时启用)同样调
os.remove,但无 try
包裹(beancount/loader.py:295-296):删除失败直接抛出,与
pickle_cache_function 对 os.remove
的容错(beancount/loader.py:252-258)不同。
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_function(beancount/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)。
_parse_recursive:include 展开与选项合并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) 配末尾
append(beancount/loader.py:518)是 FIFO
队列,include 广度优先展开。"是否顶层"用
options_map is None 判断,只有最先处理的源的 options
被采纳。
文件源在这里被断言为绝对路径(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:419、beancount/loader.py:459),字符串源用进程工作目录(beancount/loader.py:427)。commit
793d8be9 之前用 chdir() 包住
glob.glob,改显式拼路径为线程安全。
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)。
顶层文件的 options_map 被整份采用,其余收进
other_options_map(beancount/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_map(beancount/loader.py:532-560)只聚合三样:operating_currency
去重合并、dcontext 用 update_from
把各文件的显示精度并进顶层那一份(beancount/loader.py:548-551)、由开了
insert_pythonpath 的文件所在目录生成
pythonpath
列表(beancount/loader.py:553-558)。pythonpath
不是用户可写选项,是这里合成的中间值。
_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_eliminated(beancount/parser/booking.py:26-54),返回
(entries, errors) 二元组;run_transformations
执行全部插件,同样返回二元组;validation.validate
运行标准校验加调用方传入的 extra_validations
并收集错误,只返回错误列表(beancount/ops/validation.py:410-436),bean-check
传 HARDCORE_VALIDATIONS。四段接口不统一,_load
逐段拆包、显式汇总,末尾才组装成三元组返回(beancount/loader.py:634);booking
调用本就在插件之前,commit 0e46c762 把
booking.interpolate 换成 booking.book(),并入
inventory-booking
validation。标准校验按只读用途编写,但接口和调用方都不强制校验函数保持
entries 不变——loader
注释提到可比较前后哈希确认,"目前没做"(beancount/loader.py:629-631)。sys.path
改动包在 try/finally,只在插件阶段生效(commit
bb87d7e5 从 grammar 层移来);末行把输入指纹写进 options
供缓存判定。
run_transformations:插件顺序与异常隔离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 插件 →
pad → balance。raw
模式内置插件全不跑,选项文档说其用途是让用户完全掌控顺序(beancount/parser/options.py:616-621)。分前后两组的理由在两端各自职责:documents(注册于
beancount/ops/documents.py:17,35-36)生成
Document 指令,放前面让用户插件看得到产物;pad
与 balance(注册于
beancount/ops/pad.py:18,29-34、beancount/ops/balance.py:17,48-60)依据全部交易算余额、补齐、校验,任何插件增改交易都会改变结果,所以放最后,且
pad 先于 balance。pad 原本在
PLUGINS_PRE,commit 60f1c40f(#742)移到
PLUGINS_POST:插件生成的交易此前不被 pad 计入,与"padding
让随后的 balance 断言成立"的语义不符,同一提交在
beancount/ops/pad_test.py
补了回归测试。PLUGINS_AUTO 默认空,只有
bean-check --auto 临时 extend 进
DEFAULT_PLUGINS_AUTO 并在 finally
还原(beancount/scripts/check.py:53-85)。
导入阶段只吞 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 312a6e88 为 beancount.plugins.auto
这类元插件加的,combine_plugins(beancount/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)。
| 决策 | 理由 | 证据 |
|---|---|---|
加载入口最终返回
(entries, errors, options_map),常规错误累积不中断 |
一处语法或插件错误不该让整本账加载失败;错误带 metadata 供
printer 定位;内部各阶段接口不必统一,由 _load
汇总 |
beancount/loader.py:598-636,164-177 |
插件异常转成错误而不抛出,导入只吞 ImportError |
坏插件不该阻断加载;插件模块自身的初始化异常是真 bug,让它冒出来 | beancount/loader.py:683-687,717-736;beancount/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-338;CHANGES:2829-2832 |
缓存层做成可替换的模块级 _load_file |
一次装配、全进程生效,脚本能覆盖、测试能替换 | beancount/loader.py:81-85,811-844;beancount/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 |
| 现象 | 后果 | 证据 |
|---|---|---|
非法 plugin_processing_mode 分支写的是
assert "字符串" |
断言恒真,之后 plugins_iter 未绑定触发
UnboundLocalError;经由
_load/load_string/load_file
的正常解析路径,非法值会被选项转换器挡下(只接受
raw/default),此分支不可达;但
run_transformations
是可直接调用的公开函数,options_map
也能被调用方修改后传入,此时分支仍可达 |
beancount/loader.py:639-673;beancount/parser/options.py:24-36;beancount/parser/grammar_test.py:724-731 |
执行插件的 except Exception 里判
SystemExit |
SystemExit 继承 BaseException,不会被
except Exception 捕获,这段 re-raise
是死代码;sys.exit() 穿透的行为与注释一致 |
beancount/loader.py:721-724;beancount/loader_test.py:96-102 |
被 include 的加密文件不进 filenames_seen |
不出现在
options_map["include"],改它不使缓存失效,也不参与重复检测 |
beancount/loader.py:417-423,456,525 |
| 字符串源的 include 相对进程 cwd 解析 | 同一段字符串在不同工作目录下结果不同 | beancount/loader.py:424-428;beancount/loader_test.py:377-404(该测试名为
test_load_string_with_relative_include,但第 394
行实际调用的是 loader.load_file,并未覆盖
load_string 这一行为) |
| 重复 include 报错但不回滚 | 先解析的那份指令保留 | beancount/loader.py:435-445;beancount/loader_test.py:345-375 |
| 缺失文件与空 glob 是两条不同消息 | does not exist /
does not match any files,测试用一个正则同时容纳 |
beancount/loader.py:447-455,502-509;beancount/loader_test.py:254 |
options_map["include"] 被覆写 |
加载后它是本次读过的全部绝对路径排序列表,不是用户写的 include 声明 | beancount/loader.py:525;beancount/parser/options.py:241-246 |
aggregate_options_map 先 copy.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-522;beancount/parser/parser.py:224-227;beancount/loader_test.py:142-147,234-238 |
validation.validate 里
validation_tests = VALIDATIONS 后
+= extra_validations |
+= 对列表原地扩展,validation_tests
与模块级 VALIDATIONS(即
BASIC_VALIDATIONS)是同一对象;带
extra_validations
调用一次就把它们永久追加进全局列表,重复调用持续累积 |
beancount/ops/validation.py:390-407,423-425 |
--auto 靠修改模块级 PLUGINS_AUTO 生效 |
进程内全局状态,bean-check 在 finally
还原,注释说是为同进程跑测试套件 |
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 |
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=True、insert_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.remove 抛
OSError 时不崩溃 |
两个 ..._override_filename_pattern_... /
..._disable |
653-711 | 环境变量与参数两种方式覆盖模板;use_cache=False
时不留缓存文件 |
TestOptionsAggregation |
715-737 | 三个文件的 operating_currency 合并成一个集合 |
覆盖方式两点:缓存类的
setUp(beancount/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_plugins、dcontext
聚合、内置插件相对顺序(回归测试在
beancount/ops/pad_test.py)、插件配置参数传递、缓存损坏分支。
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 | 2d68cf72、661bfbea;CHANGES:2965-2975 |
实现 pickle 缓存,初期由 BEANCOUNT_LOAD_CACHE
开启,称大文件快约 15 倍;随后默认开启并加 1 秒写入门槛 |
| 2015-12-01 | 51226691 |
排序提前到 booking 之前(#84) |
| 2015-12-06 至 12-21 | 794a7b54、4f87d1a3、eba4a664、22340365、2f4ff797、6c3434d7、8fb6af0d |
include 改成全量文件列表并据此让 include
变更触发失效;缓存改用文件名+mtime+size 哈希;跨 include 聚合
options(#91);缓存命中时也输出错误(#92);加载加密文件;文件名支持变量与
~ 展开 |
| 2016 年 | f7f0d5ce、4e1aaccd、845e9caa、be478072 |
移动过的文件不再抛异常(#94,01-09);损坏或过期 pickle
的处理(03-05);只读文件系统删缓存失败不报错(04-17);BEANCOUNT_LOAD_CACHE_FILENAME(04-18) |
| 2017-05-24 | 312a6e88 |
__plugins__ 支持函数对象,元插件
auto/pedantic 成为可能 |
| 2018-03 / 08 | c16864f4、44dfdd3d、957001b8 |
include 支持通配符(#241、PR59,当时用 chdir 包住
glob);支持 include 加密文件(#325) |
| 2020-05-25 / 06-02 | 5fa98017、d9a70473;CHANGES:266-277 |
bean-check 加入
--no-cache/--cache-filename;随后
--no-cache 改为顺带删除既有缓存 |
| 2020-12-14 | 628acbfa |
插件异常处理重写(#588):导入只吞
ImportError,执行期逐个回调捕获并输出完整
traceback;此前是一个 except (ImportError, TypeError)
包住整段 |
| 2021 年 | c46eaacd、bb87d7e5、cb3526a1 |
跨文件聚合
DisplayContext(05-15);PYTHONPATH 插入从
grammar 层移到 loader 层(09-26);bean-check --auto 与
PLUGINS_AUTO 插入位(12-05) |
| 2024-01-06 | 60f1c40f |
beancount.ops.pad 从 PLUGINS_PRE 移到
PLUGINS_POST(#742) |
| 2024-11-28 | 793d8be9 |
用显式路径拼接替换 chdir,glob 展开变为线程安全 |
| 2024-12-04 | 0265c999 |
修 win32 测试失败之余,把写缓存的判定从 > 改成
>=,耗时恰好等于门槛也落盘 |
| 2024-12-21 / 12-23(合入序) | 85950542、955876a9 |
LoadError 先由 namedtuple 改为
NamedTuple 类(entry: Any
无默认值),955876a9 再把 entry 改标注为
None 并给默认值 |
parser.parse_file/parse_string
给出单文件的指令、错误与 options;booking.book
补齐仓位与推算数字(07 篇 interpolate
是其中一环);validation.validate
做只读检查;options.OPTIONS_DEFAULTS
兜底;encryption
判定与解密;printer.print_errors
输出错误;misc_utils.log_time 分阶段计时。dcontext(06 篇),loader 用 update_from
并进顶层那一份,渲染精度因而是全账本合并的结果(beancount/loader.py:550)。__plugins__
的元素可以是函数名字符串,也可以是函数对象(beancount/loader.py:705-711);签名
(entries, options_map[, config]) -> (entries, errors)(beancount/parser/options.py:273-297,该处
docstring 仍只提"函数名",未随 commit 312a6e88
加入的函数对象支持同步更新);内置三个插件用同一契约。bean-check 是仓库内唯一暴露
--no-cache/--cache-filename/--auto
命令行选项的脚本(beancount/scripts/check.py:53-67);缓存另可经
loader.initialize() 与两个环境变量控制(见 4.3 节),不止
bean-check 一途。bean-doctor 多处调
load_file;load_doc 被全仓库测试当作"docstring
即账本"的装饰器。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-54;beancount/ops/validation.py:410-436;beancount/ops/documents.py:17;beancount/ops/pad.py:18;beancount/ops/balance.py:17;beancount/plugins/auto.py:17;beancount/scripts/check.py:53-85;beancount/api.py:77-79;beancount/parser/grammar_test.py:711-731;CHANGES:266-277,2829-2832,2965-2975。
commit:0e46c762(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)。