フラットレコードの反復処理
bca.flatten_spaces(result) walks the nested FuncSpace tree in pre-order and yields one flat, scalar-only dict per node, ready for sqlite3.executemany, pandas.DataFrame.from_records, or any other tabular consumer. This page is for a caller loading metrics into a database or a DataFrame.
Metric keys use the same dotted convention as the CLI’s CSV writer (cyclomatic.modified.sum, halstead.volume, loc.lloc_average, and so on). Identity keys (path, name, kind, start_line, end_line, parent_name, depth) are added on every record.
executemany による SQLite への書き込み
以下の例は、1 つのファイルを分析し、フラット化された全キーの和集合を列とする sqlite テーブルへ、FuncSpace ごとに 1 行を挿入します。
"""Flatten a FuncSpace tree into scalar rows for sqlite / pandas.
Demonstrates ``bca.flatten_spaces`` + ``sqlite3.executemany``. The
pandas equivalent is shown in the book as a non-executed snippet so
this example stays dependency-free (sqlite ships with the stdlib).
Tied to the book's ``python/flat-records.md`` page.
"""
from __future__ import annotations
import sqlite3
from contextlib import closing
from pathlib import Path
import big_code_analysis as bca
def run(path: Path, db_path: Path) -> int:
"""Analyse ``path`` and insert one row per FuncSpace into ``db_path``.
Returns the number of rows inserted so the test can assert on it.
"""
result = bca.analyze(path)
if result is None:
msg = f"{path} was skipped (looks generated)"
raise SystemExit(msg)
# フラット化されたキーはドット区切りの小文字名
# (`halstead.unique_operators`、`halstead.total_operators` など)で、
# SQLite の大文字小文字を区別しない列比較の下でも一意です(かつての
# `N1`/`n1` の Halstead 衝突は #511 で解消済み)。そのため各キーは
# リネームなしでそれぞれ独自の列に収まります。
records = [dict(r) for r in bca.flatten_spaces(result)]
if not records:
return 0
columns = sorted({k for r in records for k in r})
cols_sql = ", ".join(f'"{c}"' for c in columns)
placeholders = ", ".join("?" for _ in columns)
rows = [tuple(r.get(c) for c in columns) for r in records]
# `closing(sqlite3.connect(...))` がドキュメント化されたイディオムです —
# 素の ``with sqlite3.connect(...)`` コンテキストマネージャーは
# トランザクションのコミット / ロールバックを行うだけで、接続を
# クローズしません。そのため長時間動くコンシューマーはファイル
# ディスクリプターをリークします(さらに Windows では db ファイルの
# 排他的書き込みロックを保持し続けます)。
with closing(sqlite3.connect(db_path)) as db, db:
db.execute(f"CREATE TABLE IF NOT EXISTS metrics ({cols_sql})")
db.executemany(
f"INSERT INTO metrics ({cols_sql}) VALUES ({placeholders})",
rows,
)
return len(rows)
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
sys.exit("usage: python flat_records.py <source-file> <out.db>")
inserted = run(Path(sys.argv[1]), Path(sys.argv[2]))
print(f"inserted {inserted} rows into {sys.argv[2]}")
The iterator is lazy and single-use: it walks the input once without materialising the whole list. A second iteration of the same iterator yields nothing; call list() once if you need to re-iterate.
Pandas
flatten_spaces is the natural input to pandas.DataFrame.from_records. pandas is not a dependency of the bindings; install it separately if you want the DataFrame view.
import big_code_analysis as bca
import pandas as pd
result = bca.analyze("src/lib.rs")
if result is not None:
df = pd.DataFrame.from_records(bca.flatten_spaces(result))
print(df.head())
# スペースの kind でグループ化し、関数・クラス・ファイルそれぞれの
# 循環的複雑度の平均を確認します。
by_kind = df.groupby("kind")["cyclomatic.sum"].mean()
識別列と CLI CSV の比較
The flat-record schema is mostly aligned with the CLI’s CSV writer, with a few intentional deltas:
- 識別列は、こちらでは
name/kindを使いますが、CSV ライターはspace_name/space_kindを使います。フラットレコードにはparent_name/depthも追加されますが、CSV ライターにはありません。 - The per-space
cognitive.value,cyclomatic.value,cyclomatic.modified.value,abc.valueandnargs.valueleaves (the numbersbca checkthresholds against) are present here, as in the JSON shape, but absent from the CSV header. tokens.*は JSON の形(tokens.tokens、tokens.average、tokens.min、tokens.max)にフラット化されます。CSV と異なるのは合計のリーフだけで、CSV ではtokens.sumと綴られます。average/min/maxのリーフは現在一致しています(#590)。CSV と完全に揃える必要がある場合は、コンシューマー側で合計のリーフをリネームしてください。
Anonymous spaces (Rust closures, JavaScript function expressions and arrows) keep their name == "<anonymous>" marker verbatim; flatten_spaces does not normalise.
注意点
parent_namealone cannot disambiguate same-named siblings nested under different parents (twoInnerclasses under two different outer classes both surface asparent_name == "Inner"for their own children). Pair it withdepthand source-order position, or rebuild the qualified name in your consumer, if you need a fully-qualified path.- Do not mutate the input
resultwhile iterating: the walker keeps references into it, so mutations to not-yet-yielded subtrees show up in later records. - 存在しないメトリクスのサブツリーはキーを生成しません(
Noneではなく欠落)。これはメトリクス選択の「Halstead 無効時」のエッジケースと一致します。 flatten_spacesraisesTypeErrorif the input is not a mapping. Filter out theNonereturns frombca.analyze(generated files underskip_generated=True) before passing, and likewise theNoneslotsanalyze_batchemits underskip_generated=False, which mark a file the read gate declined to parse.