フラットレコードの反復処理
bca.flatten_spaces(result) は、ネストした FuncSpace ツリーを行きがけ順(pre-order)に走査し、ノードごとにスカラー値のみのフラットな dict を 1 つずつ生成します — sqlite3.executemany や pandas.DataFrame.from_records をはじめ、あらゆる表形式のコンシューマーにそのまま渡せます。
メトリクスのキーは、CLI の CSV ライターと同じドット区切りの規約(cyclomatic.modified.sum、halstead.volume、loc.lloc_average など)を使います。識別キー(path、name、kind、start_line、end_line、parent_name、depth)はすべてのレコードに付加されます。
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]}")
このイテレーターは 遅延評価かつ単回使用 です。リスト全体を実体化せずに、入力を 1 回だけ走査します。同じイテレーターを 2 回目に反復しても何も返しません — 再反復が必要な場合は一度 list() を呼んでください。
Pandas
flatten_spaces は pandas.DataFrame.from_records への自然な入力です。Pandas はバインディングの依存関係ではないため、DataFrame ビューが必要な場合は別途インストールしてください。
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 の比較
フラットレコードのスキーマは CLI の CSV ライターとほぼ揃っていますが、意図的な差分がいくつかあります。
- 識別列は、こちらでは
name/kindを使いますが、CSV ライターはspace_name/space_kindを使います。フラットレコードにはparent_name/depthも追加されますが、CSV ライターにはありません。 tokens.*は JSON の形(tokens.tokens、tokens.average、tokens.min、tokens.max)にフラット化されます。CSV と異なるのは合計のリーフだけで、CSV ではtokens.sumと綴られます。average/min/maxのリーフは現在一致しています(#590)。CSV と完全に揃える必要がある場合は、コンシューマー側で合計のリーフをリネームしてください。
匿名スペース(Rust のクロージャ、JavaScript の関数式やアロー関数)は name == "<anonymous>" マーカーをそのまま保持します — flatten_spaces は正規化を行いません。
注意点
parent_nameだけでは、異なる親の下にネストした同名の兄弟を区別できません(例えば、異なる外側クラスの下にある 2 つのInnerクラスは、どちらも自身の子に対してparent_name == "Inner"として現れます)。完全修飾パスが必要な場合は、depthとソース順の位置を組み合わせるか、コンシューマー側で修飾名を再構築してください。- 反復中に入力の
resultを変更しないでください。ウォーカーは入力への参照を保持しているため、まだ生成されていないサブツリーへの変更は後続のレコードに反映されてしまいます。 - 存在しないメトリクスのサブツリーはキーを生成しません(
Noneではなく欠落)。これはメトリクス選択の「Halstead 無効時」のエッジケースと一致します。 flatten_spacesは、入力がマッピングでない場合にTypeErrorを送出します。呼び出し側は、渡す前にbca.analyzeのNone戻り値(例えばskip_generated=Trueの下での生成ファイル)をフィルタリングする必要があります。