code-to10 v6 A7: unify row_get on peer_messages

- peer_messages.row_get gains default=None (was None-only); the
  three old variants agree on all real row types (None / dict /
  sqlite3.Row): auto_memory's old default was already None, and
  lifecycle's old None special-case and dict .get give identical
  results on those types, so no call-site behavior changes and no
  call site passes a non-None default.
- auto_memory and lifecycle delete their private _row_get copies
  and import peer_messages.row_get as _row_get (precedent: server,
  worker, handler_project, handler_support already do this);
  peer_messages imports no cloud modules, so no import cycle.
  lifecycle's old broad-Exception catch differs only for pathological
  __getitem__ inputs, which are not DB rows and are out of contract;
  this is documented on row_get, not left as a second story.
- Scope note: other modules' own row_get copies (approvals,
  automations, marker_tools, todos, wait_ops) are out of this item's
  stated scope and are untouched.
- Also drops auto_memory's unused 'import os' (pre-existing F401),
  so touched files are ruff E,F clean.
- Tests: cloud/tests/test_row_get_unified.py locks the identity
  (both aliases ARE row_get), the real-row-type matrix, the
  None-default fallback, and snapshot_to_dict via the unified path.

(cherry picked from commit 1644a0b837bfefb13cdc3c93d87fc1b246026607)
This commit is contained in:
host committed 2026-11-03 02:49:26 +00:00
1 parent c33bfd240e
commit be2962525b
4 files changed
+116 -24

No files matched your search

+4 -8
View File
@@ -53,12 +53,15 @@ cloud/migrations.py 的对齐说明;PG DDL 同步进
cloud/db.py POSTGRES_SCHEMA 与 MIGRATION_TABLES。
"""
import os
import re
import time
from cloud.db import connect as _db_connect
from cloud.identity import IdentityStore
# 行取值统一走 peer_messages.row_get(A7):本模块旧私有版与之
# 同款语义(缺列/缺键/空行回 default,默认 None),调用处仍
# 用 _row_get 别名,行为不变。
from cloud.peer_messages import row_get as _row_get
from cloud.quota import default_quota_usd_micros as _quota_default
from cloud.quota import monthly_usage as _quota_monthly_usage
from cloud.quota import quota_for_owner as _quota_for_owner
@@ -152,13 +155,6 @@ def quota_remaining_percent(db_path, owner):
conn.close()
def _row_get(row, name, default=None):
try:
return row[name]
except (KeyError, IndexError, TypeError):
return default
def _first_line(text, max_len=200):
for line in (text or "").splitlines():
line = " ".join(line.split())
+7 -11
View File
@@ -31,6 +31,13 @@ import json
import time
import uuid
# 行取值统一走 peer_messages.row_get(A7):本模块旧私有版在
# 真实行类型(None / dict / sqlite3.Row)下与之结果完全一致
# (None 特判、dict .get、缺列回 None),调用处仍用 _row_get
# 别名,行为不变;旧版宽 Exception 兜底的病态行不在契约内,
# 见 peer_messages.row_get docstring。
from cloud.peer_messages import row_get as _row_get
SNAPSHOT_STATUS_LIMIT = 4000
SNAPSHOT_DIFF_LIMIT = 20000
REF_LIMIT = 2000
@@ -39,17 +46,6 @@ REF_KINDS = ("doc", "build", "external", "pr", "file")
BINDING_MODES = ("heartbeat", "cron")
def _row_get(row, name):
if row is None:
return None
if isinstance(row, dict):
return row.get(name)
try:
return row[name]
except Exception:
return None
# -- worktree 快照 ------------------------------------------------------
def ensure_snapshots_table(conn):
+13 -5
View File
@@ -62,14 +62,22 @@ def peer_message_prefix(from_task_id):
return "[来自任务 %s 的消息]\n" % from_task_id
def row_get(row, name):
"""行取值容错:sqlite3.Row 缺列报 IndexError、dict 缺键
报 KeyError、空行报 TypeError,一律回 None(worker 直连
老库、列还没迁移时排空不能因此停摆)。"""
def row_get(row, name, default=None):
"""行取值容错(全仓行取值的统一口,A7):sqlite3.Row 缺列
报 IndexError、dict 缺键报 KeyError、空行报 TypeError,
一律回 default(默认 None;worker 直连老库、列还没迁移时
排空不能因此停摆)。handler 链、auto_memory、lifecycle
三处统一走本函数:三者旧语义在真实行类型(None / dict /
sqlite3.Row)下完全一致——auto_memory 旧版的 default 参数
默认就是 None,lifecycle 旧版的 None 特判与 dict .get 在
这些类型下与本函数结果相同;旧 lifecycle 版另带的宽
Exception 兜底只对“__getitem__ 抛其它异常的病态行”有
区别,那类输入不是 DB 行,不属于本函数契约,统一后此
类输入改为抛给调用方(不在测试锁定的真实行类型内)。"""
try:
return row[name]
except (KeyError, IndexError, TypeError):
return None
return default
def extract_peer_messages(text):
+92
View File
@@ -0,0 +1,92 @@
"""row_get 簇统一单测(A7):三处走同一函数,语义钉住。
只用标准库 + 内存 SQLite,不联网。锁定点:
- 统一身份:auto_memory._row_get 与 lifecycle._row_get 就是
peer_messages.row_get 本体(不是又一份复写)。
- 真实行类型矩阵(None / dict / sqlite3.Row)下取值与回落
与三处旧版一致:现值原样回(含 0 / 空串等假值),缺列 /
缺键 / 空行回 default(默认 None)。
- default 回落差异钉住:三处旧版的默认回落都是 None——
auto_memory 旧版 default 参数默认 None,lifecycle 旧版无
default 参数一律回 None,peer 旧版回 None;统一后两模块
调用处仍不传 default,缺失时仍为 None,不算改语义。
- 统一路径实测:lifecycle.snapshot_to_dict 在 dict 缺键行与
sqlite3.Row 行上都经统一口出正确字段。
"""
import sqlite3
import sys
import unittest
from pathlib import Path
try:
import cloud.auto_memory as auto_memory
import cloud.lifecycle as lifecycle
from cloud.peer_messages import row_get
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import cloud.auto_memory as auto_memory
import cloud.lifecycle as lifecycle
from cloud.peer_messages import row_get
def _sqlite_row():
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
try:
conn.execute("CREATE TABLE t(a TEXT, b INTEGER)")
conn.execute("INSERT INTO t(a, b) VALUES('x', 0)")
full = conn.execute("SELECT a, b FROM t").fetchone()
partial = conn.execute("SELECT a FROM t").fetchone()
return full, partial
finally:
conn.close()
class RowGetUnifiedTestCase(unittest.TestCase):
def test_all_three_are_the_same_function(self):
self.assertIs(auto_memory._row_get, row_get)
self.assertIs(lifecycle._row_get, row_get)
def test_real_row_type_matrix(self):
full, partial = _sqlite_row()
# 现值原样回(含假值 0,不被当成缺失)。
self.assertEqual(row_get({"a": 0}, "a"), 0)
self.assertEqual(row_get({"a": ""}, "a"), "")
self.assertEqual(row_get(full, "a"), "x")
self.assertEqual(row_get(full, "b"), 0)
# 缺键 / 缺列 / 空行一律回 default(默认 None)。
self.assertIsNone(row_get({"a": 1}, "missing"))
self.assertIsNone(row_get(partial, "b"))
self.assertIsNone(row_get(None, "a"))
# 显式 default 只在缺失时生效,现值 None 也不被替换
#(旧 auto_memory 版同款语义:取到什么回什么)。
self.assertEqual(row_get({"a": 1}, "missing", default="d"), "d")
self.assertEqual(row_get(None, "a", default=7), 7)
self.assertIsNone(row_get({"a": None}, "a", default="d"))
def test_default_fallback_is_none_in_both_modules(self):
# 两模块调用处均不传 default;缺失回落必须仍是 None,
# 否则就是改了旧语义。
self.assertIsNone(auto_memory._row_get({}, "nope"))
self.assertIsNone(lifecycle._row_get({}, "nope"))
self.assertIsNone(auto_memory._row_get(None, "nope"))
self.assertIsNone(lifecycle._row_get(None, "nope"))
def test_lifecycle_snapshot_to_dict_via_unified_get(self):
out = lifecycle.snapshot_to_dict({"id": "s1", "task_id": "t1"})
self.assertEqual(out["id"], "s1")
self.assertEqual(out["task_id"], "t1")
self.assertIsNone(out["project_id"])
self.assertIsNone(out["branch"])
self.assertEqual(out["untracked"], [])
full, _partial = _sqlite_row()
out2 = lifecycle.snapshot_to_dict(
{"id": "s2", "untracked_json": '["a.txt"]'})
self.assertEqual(out2["untracked"], ["a.txt"])
# sqlite Row 行同样走统一口:缺列不炸。
self.assertIsNone(row_get(full, "untracked_json"))
if __name__ == "__main__":
unittest.main()