Release 0.2.0: object tree, refs/modules options, docs
Add query object (--clear), modules -n, composite refs labels, FillChecking/TypeSet parsing, VERSION/CHANGELOG and --version. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+13
-2
@@ -1,3 +1,14 @@
|
||||
"""Индексация метаданных конфигураций 1С (выгрузки EDT/XML) для быстрого поиска."""
|
||||
"""Индексация метаданных и модулей конфигураций 1С (выгрузки EDT/XML)."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__status__ = "in development"
|
||||
|
||||
|
||||
def read_version_file() -> str:
|
||||
"""Версия из файла VERSION рядом со скриптами (источник истины для релиза)."""
|
||||
path = Path(__file__).resolve().parents[1] / "VERSION"
|
||||
if path.is_file():
|
||||
return path.read_text(encoding="utf-8").strip() or __version__
|
||||
return __version__
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
"""Поиск объекта/реквизита и вывод дерева структуры метаданных."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .parse_meta import FieldRec, ObjectRec, parse_metadata_xml
|
||||
|
||||
|
||||
FILL_LABEL = {
|
||||
"ShowError": "обяз.",
|
||||
"DontCheck": "необяз.",
|
||||
}
|
||||
|
||||
|
||||
def _norm(s: str) -> str:
|
||||
return " ".join((s or "").casefold().split())
|
||||
|
||||
|
||||
def _type_label(raw: str) -> str:
|
||||
if raw.startswith("cfg:"):
|
||||
return raw[4:]
|
||||
if raw.startswith("xs:"):
|
||||
return raw[3:]
|
||||
return raw
|
||||
|
||||
|
||||
def _fill_label(fill: str) -> str:
|
||||
if not fill:
|
||||
return "?"
|
||||
return FILL_LABEL.get(fill, fill)
|
||||
|
||||
|
||||
def _field_matches(f: dict[str, Any], q: str, qn: str) -> bool:
|
||||
"""Только точное совпадение имени или синонима реквизита."""
|
||||
name_cf = _norm(f.get("name") or "")
|
||||
syn_cf = _norm(f.get("synonym") or "")
|
||||
# синоним может быть «Клиент | en:Client» — сравниваем по частям
|
||||
syn_parts = [_norm(p) for p in (f.get("synonym") or "").split("|")]
|
||||
return name_cf == qn or syn_cf == qn or qn in syn_parts
|
||||
|
||||
|
||||
def find_structure_matches(
|
||||
conn: sqlite3.Connection,
|
||||
query: str,
|
||||
*,
|
||||
configs: list[str] | None = None,
|
||||
object_type: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Найти объекты по имени/синониму объекта или реквизита.
|
||||
|
||||
Каждый элемент:
|
||||
config, full_name, path, json, match_kind: object|field,
|
||||
matched_fields: list[field dict keys],
|
||||
score: int (меньше — лучше)
|
||||
"""
|
||||
q = query.strip()
|
||||
qn = _norm(q)
|
||||
if not qn:
|
||||
return []
|
||||
|
||||
sql_obj = """
|
||||
SELECT id, config, full_name, name, synonym, path, json, object_type
|
||||
FROM objects
|
||||
WHERE (
|
||||
name = ? OR full_name = ? OR full_name LIKE ?
|
||||
OR synonym LIKE ? OR name LIKE ?
|
||||
)
|
||||
"""
|
||||
params: list[Any] = [q, q, f"%.{q}", f"%{q}%", f"%{q}%"]
|
||||
if configs:
|
||||
sql_obj += f" AND config IN ({','.join('?' * len(configs))})"
|
||||
params.extend(configs)
|
||||
if object_type:
|
||||
sql_obj += " AND object_type = ?"
|
||||
params.append(object_type)
|
||||
sql_obj += " ORDER BY config, full_name LIMIT ?"
|
||||
params.append(max(limit * 5, 50))
|
||||
|
||||
by_key: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
|
||||
def _ensure(row) -> dict[str, Any]:
|
||||
key = (row["config"], row["full_name"])
|
||||
if key not in by_key:
|
||||
oid = row["object_id"] if "object_id" in row.keys() else row["id"]
|
||||
by_key[key] = {
|
||||
"config": row["config"],
|
||||
"full_name": row["full_name"],
|
||||
"path": row["path"],
|
||||
"json": row["json"],
|
||||
"object_type": row["object_type"],
|
||||
"match_kind": "none",
|
||||
"matched_fields": [],
|
||||
"object_id": oid,
|
||||
"score": 100,
|
||||
}
|
||||
return by_key[key]
|
||||
|
||||
for row in conn.execute(sql_obj, params):
|
||||
entry = _ensure(row)
|
||||
name_cf = _norm(row["name"])
|
||||
full_cf = _norm(row["full_name"])
|
||||
syn_cf = _norm(row["synonym"] or "")
|
||||
if name_cf == qn or full_cf == qn or full_cf.endswith("." + qn):
|
||||
entry["match_kind"] = "object"
|
||||
entry["score"] = min(entry["score"], 0)
|
||||
elif syn_cf == qn:
|
||||
entry["match_kind"] = "object"
|
||||
entry["score"] = min(entry["score"], 2)
|
||||
elif qn in name_cf or qn in full_cf:
|
||||
if entry["match_kind"] == "none":
|
||||
entry["match_kind"] = "object"
|
||||
entry["score"] = min(entry["score"], 20)
|
||||
elif qn in syn_cf:
|
||||
if entry["match_kind"] == "none":
|
||||
entry["match_kind"] = "object"
|
||||
entry["score"] = min(entry["score"], 25)
|
||||
|
||||
sql_f = """
|
||||
SELECT o.id AS object_id, o.config, o.full_name, o.path, o.json, o.object_type,
|
||||
o.name AS object_name, o.synonym AS object_synonym,
|
||||
f.kind, f.name, f.synonym, f.types, f.tabular_section, f.refs_json
|
||||
FROM fields f
|
||||
JOIN objects o ON o.id = f.object_id
|
||||
WHERE (
|
||||
f.name = ? OR f.synonym LIKE ? OR f.name LIKE ?
|
||||
)
|
||||
"""
|
||||
params_f: list[Any] = [q, f"%{q}%", f"%{q}%"]
|
||||
if configs:
|
||||
sql_f += f" AND o.config IN ({','.join('?' * len(configs))})"
|
||||
params_f.extend(configs)
|
||||
if object_type:
|
||||
sql_f += " AND o.object_type = ?"
|
||||
params_f.append(object_type)
|
||||
sql_f += " ORDER BY o.config, o.full_name LIMIT ?"
|
||||
params_f.append(max(limit * 15, 100))
|
||||
|
||||
for row in conn.execute(sql_f, params_f):
|
||||
fdict = {
|
||||
"kind": row["kind"],
|
||||
"name": row["name"],
|
||||
"synonym": row["synonym"] or "",
|
||||
"types": [t.strip() for t in (row["types"] or "").split(",") if t.strip()],
|
||||
"tabular_section": row["tabular_section"] or "",
|
||||
"refs": json.loads(row["refs_json"] or "[]"),
|
||||
"fill_checking": "",
|
||||
}
|
||||
if not _field_matches(fdict, q, qn):
|
||||
continue
|
||||
entry = _ensure(row)
|
||||
fname_cf = _norm(fdict["name"])
|
||||
fsyn_cf = _norm(fdict["synonym"])
|
||||
if fname_cf == qn:
|
||||
field_score = 5
|
||||
elif fsyn_cf == qn:
|
||||
field_score = 6
|
||||
else:
|
||||
field_score = 15
|
||||
# если объект уже найден точно — оставляем object, но добавляем поля
|
||||
if entry["score"] > field_score or entry["match_kind"] in {"none", "field"}:
|
||||
if entry["match_kind"] != "object" or entry["score"] > 0:
|
||||
entry["match_kind"] = "field"
|
||||
entry["score"] = min(entry["score"], field_score)
|
||||
elif entry["match_kind"] == "object" and entry["score"] == 0:
|
||||
# точный объект + ещё реквизит с тем же именем (редко) — остаётся object
|
||||
pass
|
||||
else:
|
||||
entry["score"] = min(entry["score"], field_score)
|
||||
|
||||
sig = (fdict["tabular_section"], fdict["name"], fdict["kind"])
|
||||
existing = {
|
||||
(x.get("tabular_section"), x.get("name"), x.get("kind"))
|
||||
for x in entry["matched_fields"]
|
||||
}
|
||||
if sig not in existing:
|
||||
entry["matched_fields"].append(fdict)
|
||||
|
||||
# убрать слабые «просто LIKE по имени объекта», если есть точные хиты
|
||||
items = [v for v in by_key.values() if v["match_kind"] != "none"]
|
||||
has_exact = any(v["score"] <= 6 for v in items)
|
||||
if has_exact:
|
||||
items = [v for v in items if v["score"] <= 15]
|
||||
|
||||
items.sort(key=lambda x: (x["score"], x["config"], x["full_name"]))
|
||||
return items[:limit]
|
||||
|
||||
|
||||
def load_object_structure(match: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Загрузить структуру: предпочесть свежий разбор XML (FillChecking)."""
|
||||
path_s = match.get("path") or ""
|
||||
path = Path(path_s) if path_s else None
|
||||
config = match["config"]
|
||||
otype = match.get("object_type") or match["full_name"].split(".", 1)[0]
|
||||
|
||||
if path and path.is_file():
|
||||
rec = parse_metadata_xml(path, config=config, object_type=otype)
|
||||
if rec:
|
||||
return rec.to_dict()
|
||||
|
||||
data = json.loads(match["json"])
|
||||
# старые индексы без fill_checking
|
||||
for f in data.get("fields") or []:
|
||||
f.setdefault("fill_checking", "")
|
||||
return data
|
||||
|
||||
|
||||
def format_types(types: list[str], refs: list[dict[str, Any]] | None = None) -> str:
|
||||
if not types:
|
||||
return "—"
|
||||
labels = [_type_label(t) for t in types]
|
||||
if len(labels) == 1:
|
||||
return labels[0]
|
||||
return " | ".join(labels)
|
||||
|
||||
|
||||
def format_field_line(
|
||||
f: dict[str, Any],
|
||||
*,
|
||||
indent: str = "",
|
||||
last: bool = True,
|
||||
under_ts: bool = False,
|
||||
) -> str:
|
||||
branch = "└── " if last else "├── "
|
||||
name = f.get("name") or ""
|
||||
kind = f.get("kind") or "Attribute"
|
||||
syn = f.get("synonym") or ""
|
||||
syn_s = f" — {syn}" if syn else ""
|
||||
if kind == "TabularSection":
|
||||
return f"{indent}{branch}`{kind}.{name}`{syn_s}"
|
||||
fill = _fill_label(f.get("fill_checking") or "")
|
||||
types_s = format_types(f.get("types") or [], f.get("refs"))
|
||||
if kind == "Column" and not under_ts and f.get("tabular_section"):
|
||||
name = f"{f['tabular_section']}.{name}"
|
||||
return f"{indent}{branch}`{kind}.{name}`{syn_s} [{fill}] : {types_s}"
|
||||
|
||||
|
||||
def format_object_tree(
|
||||
obj: dict[str, Any],
|
||||
*,
|
||||
clear: bool = False,
|
||||
matched_fields: list[dict[str, Any]] | None = None,
|
||||
match_kind: str = "object",
|
||||
) -> str:
|
||||
"""Дерево реквизитов объекта. clear — только совпадения."""
|
||||
lines: list[str] = []
|
||||
syn = obj.get("synonym") or ""
|
||||
syn_s = f" — {syn}" if syn else ""
|
||||
cfg = obj.get("config") or obj.get("_config") or ""
|
||||
header = f"`{obj.get('full_name') or obj.get('object_type') + '.' + obj.get('name')}`{syn_s}"
|
||||
if cfg:
|
||||
header = f"[{cfg}] {header}"
|
||||
lines.append(header)
|
||||
|
||||
fields: list[dict[str, Any]] = list(obj.get("fields") or [])
|
||||
|
||||
if clear:
|
||||
if match_kind == "field" and matched_fields:
|
||||
want: set[tuple[str, str, str]] = set()
|
||||
need_ts: set[str] = set()
|
||||
for mf in matched_fields:
|
||||
want.add(
|
||||
(
|
||||
mf.get("kind") or "",
|
||||
mf.get("tabular_section") or "",
|
||||
mf.get("name") or "",
|
||||
)
|
||||
)
|
||||
if mf.get("tabular_section") and mf.get("kind") == "Column":
|
||||
need_ts.add(mf["tabular_section"])
|
||||
selected: list[dict[str, Any]] = []
|
||||
for f in fields:
|
||||
key = (
|
||||
f.get("kind") or "",
|
||||
f.get("tabular_section") or "",
|
||||
f.get("name") or "",
|
||||
)
|
||||
if key in want:
|
||||
selected.append(f)
|
||||
elif f.get("kind") == "TabularSection" and f.get("name") in need_ts:
|
||||
selected.append(f)
|
||||
if not selected:
|
||||
lines.append(" (совпавшие реквизиты не найдены в структуре)")
|
||||
return "\n".join(lines)
|
||||
lines.extend(_render_field_tree(selected, only_keys=want, need_ts=need_ts))
|
||||
return "\n".join(lines)
|
||||
return "\n".join(lines)
|
||||
|
||||
if not fields:
|
||||
lines.append(" (нет реквизитов в индексе)")
|
||||
return "\n".join(lines)
|
||||
|
||||
lines.extend(_render_field_tree(fields))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _render_field_tree(
|
||||
fields: list[dict[str, Any]],
|
||||
*,
|
||||
only_keys: set[tuple[str, str, str]] | None = None,
|
||||
need_ts: set[str] | None = None,
|
||||
) -> list[str]:
|
||||
"""Рендер: сначала шапка объекта уже напечатана; здесь реквизиты и ТЧ."""
|
||||
lines: list[str] = []
|
||||
# группировка: header fields, then TS with columns
|
||||
headers = [
|
||||
f
|
||||
for f in fields
|
||||
if f.get("kind") not in {"TabularSection", "Column"}
|
||||
and not f.get("tabular_section")
|
||||
]
|
||||
tab_sections = [f for f in fields if f.get("kind") == "TabularSection"]
|
||||
columns_by_ts: dict[str, list[dict[str, Any]]] = {}
|
||||
for f in fields:
|
||||
if f.get("kind") == "Column" and f.get("tabular_section"):
|
||||
columns_by_ts.setdefault(f["tabular_section"], []).append(f)
|
||||
|
||||
if only_keys is not None:
|
||||
headers = [
|
||||
f
|
||||
for f in headers
|
||||
if (f.get("kind") or "", f.get("tabular_section") or "", f.get("name") or "")
|
||||
in only_keys
|
||||
]
|
||||
tab_sections = [
|
||||
f
|
||||
for f in tab_sections
|
||||
if f.get("name") in (need_ts or set())
|
||||
or (f.get("kind") or "", "", f.get("name") or "") in only_keys
|
||||
]
|
||||
|
||||
# плоский список узлов верхнего уровня для корректных └──/├──
|
||||
top: list[tuple[str, Any]] = [("h", f) for f in headers] + [("ts", f) for f in tab_sections]
|
||||
for i, (kind, node) in enumerate(top):
|
||||
last = i == len(top) - 1
|
||||
if kind == "h":
|
||||
lines.append(format_field_line(node, indent="", last=last))
|
||||
continue
|
||||
# tabular section
|
||||
lines.append(format_field_line(node, indent="", last=last))
|
||||
ts_name = node.get("name") or ""
|
||||
cols = columns_by_ts.get(ts_name, [])
|
||||
if only_keys is not None:
|
||||
cols = [
|
||||
c
|
||||
for c in cols
|
||||
if (c.get("kind") or "", c.get("tabular_section") or "", c.get("name") or "")
|
||||
in only_keys
|
||||
]
|
||||
child_indent = " " if last else "│ "
|
||||
for j, col in enumerate(cols):
|
||||
lines.append(
|
||||
format_field_line(
|
||||
col, indent=child_indent, last=(j == len(cols) - 1), under_ts=True
|
||||
)
|
||||
)
|
||||
return lines
|
||||
@@ -85,6 +85,10 @@ def collect_types(props: ET.Element) -> list[str]:
|
||||
raw = (t.text or "").strip()
|
||||
if raw:
|
||||
types.append(raw)
|
||||
for t in typ.findall(f".//{V8}TypeSet"):
|
||||
raw = (t.text or "").strip()
|
||||
if raw:
|
||||
types.append(raw)
|
||||
return types
|
||||
|
||||
|
||||
@@ -113,6 +117,7 @@ class FieldRec:
|
||||
types: list[str] = field(default_factory=list)
|
||||
refs: list[dict[str, str]] = field(default_factory=list)
|
||||
tabular_section: str = ""
|
||||
fill_checking: str = "" # ShowError | DontCheck | …
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -173,6 +178,7 @@ def _parse_field_props(props: ET.Element, kind: str, tabular: str = "") -> Field
|
||||
types=types,
|
||||
refs=parse_ref_targets(types),
|
||||
tabular_section=tabular,
|
||||
fill_checking=child_text(props, "FillChecking"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+92
-23
@@ -123,14 +123,18 @@ def search_modules(
|
||||
*,
|
||||
configs: list[str] | None = None,
|
||||
limit: int = 30,
|
||||
names_only: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
fts = _fts_query(query)
|
||||
sql = """
|
||||
snip_col = "'' AS snip" if names_only else "snippet(modules_fts, 4, '[', ']', '…', 16) AS snip"
|
||||
sql = f"""
|
||||
SELECT m.config, m.rel_path, m.owner, m.module_kind, m.symbols, m.line_count,
|
||||
snippet(modules_fts, 4, '[', ']', '…', 16) AS snip,
|
||||
o.synonym AS owner_synonym,
|
||||
{snip_col},
|
||||
bm25(modules_fts, 2.0, 5.0, 2.0, 8.0, 1.0) AS score
|
||||
FROM modules_fts
|
||||
JOIN modules m ON m.id = modules_fts.module_id
|
||||
LEFT JOIN objects o ON o.config = m.config AND o.full_name = m.owner
|
||||
WHERE modules_fts MATCH ?
|
||||
"""
|
||||
params: list[Any] = [fts]
|
||||
@@ -144,13 +148,15 @@ def search_modules(
|
||||
except sqlite3.OperationalError:
|
||||
like = f"%{query}%"
|
||||
sql2 = """
|
||||
SELECT config, rel_path, owner, module_kind, symbols, line_count, '' AS snip, 0 AS score
|
||||
FROM modules
|
||||
WHERE rel_path LIKE ? OR owner LIKE ? OR symbols LIKE ?
|
||||
SELECT m.config, m.rel_path, m.owner, m.module_kind, m.symbols, m.line_count,
|
||||
o.synonym AS owner_synonym, '' AS snip, 0 AS score
|
||||
FROM modules m
|
||||
LEFT JOIN objects o ON o.config = m.config AND o.full_name = m.owner
|
||||
WHERE m.rel_path LIKE ? OR m.owner LIKE ? OR m.symbols LIKE ?
|
||||
"""
|
||||
params2: list[Any] = [like, like, like]
|
||||
if configs:
|
||||
sql2 += f" AND config IN ({','.join('?' * len(configs))})"
|
||||
sql2 += f" AND m.config IN ({','.join('?' * len(configs))})"
|
||||
params2.extend(configs)
|
||||
sql2 += " LIMIT ?"
|
||||
params2.append(limit)
|
||||
@@ -163,7 +169,13 @@ def find_refs_to(
|
||||
*,
|
||||
configs: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
) -> list[dict[str, Any]]:
|
||||
) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
"""Обратные ссылки на объект.
|
||||
|
||||
Возвращает (строки, totals_by_config).
|
||||
При поиске по нескольким конфигурациям ``limit`` — максимум **на каждую**
|
||||
конфигурацию (чтобы crm3-26 не вытеснял crm3-dev из-за ORDER BY + LIMIT).
|
||||
"""
|
||||
kind = ""
|
||||
name = target
|
||||
if "." in target:
|
||||
@@ -183,25 +195,82 @@ def find_refs_to(
|
||||
}:
|
||||
kind_ref = kind + "Ref"
|
||||
|
||||
where = "r.to_name = ?"
|
||||
params: list[Any] = [name]
|
||||
if kind_ref:
|
||||
sql = """
|
||||
SELECT config, from_full_name, from_field, to_kind, to_name
|
||||
FROM refs
|
||||
WHERE to_name = ? AND (to_kind = ? OR to_kind = ?)
|
||||
"""
|
||||
params: list[Any] = [name, kind_ref, kind]
|
||||
else:
|
||||
sql = """
|
||||
SELECT config, from_full_name, from_field, to_kind, to_name
|
||||
FROM refs WHERE to_name = ?
|
||||
"""
|
||||
params = [name]
|
||||
where += " AND (r.to_kind = ? OR r.to_kind = ?)"
|
||||
params.extend([kind_ref, kind])
|
||||
if configs:
|
||||
sql += f" AND config IN ({','.join('?' * len(configs))})"
|
||||
where += f" AND r.config IN ({','.join('?' * len(configs))})"
|
||||
params.extend(configs)
|
||||
sql += " ORDER BY config, from_full_name LIMIT ?"
|
||||
params.append(limit)
|
||||
return [dict(r) for r in conn.execute(sql, params)]
|
||||
|
||||
# Итоги по конфигурациям (без LIMIT)
|
||||
totals: dict[str, int] = {
|
||||
r["config"]: int(r["n"])
|
||||
for r in conn.execute(
|
||||
f"SELECT r.config, COUNT(*) AS n FROM refs r WHERE {where} GROUP BY r.config",
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
# limit на каждую конфигурацию — иначе алфавитный ORDER BY отрезает остальные
|
||||
sql = f"""
|
||||
WITH matched AS (
|
||||
SELECT
|
||||
r.config,
|
||||
r.from_full_name,
|
||||
r.from_field,
|
||||
r.to_kind,
|
||||
r.to_name,
|
||||
f.types AS field_types,
|
||||
f.refs_json AS field_refs_json,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY r.config
|
||||
ORDER BY r.from_full_name, r.from_field
|
||||
) AS rn
|
||||
FROM refs r
|
||||
LEFT JOIN objects o
|
||||
ON o.config = r.config AND o.full_name = r.from_full_name
|
||||
LEFT JOIN fields f
|
||||
ON f.object_id = o.id
|
||||
AND (
|
||||
(COALESCE(f.tabular_section, '') = '' AND f.name = r.from_field)
|
||||
OR (f.tabular_section || '.' || f.name = r.from_field)
|
||||
)
|
||||
WHERE {where}
|
||||
)
|
||||
SELECT *
|
||||
FROM matched
|
||||
WHERE rn <= ?
|
||||
ORDER BY config, from_full_name, from_field
|
||||
"""
|
||||
rows = [dict(r) for r in conn.execute(sql, [*params, limit])]
|
||||
|
||||
for row in rows:
|
||||
types_s = row.get("field_types") or ""
|
||||
refs_raw = row.get("field_refs_json") or "[]"
|
||||
try:
|
||||
refs_list = json.loads(refs_raw) if refs_raw else []
|
||||
except json.JSONDecodeError:
|
||||
refs_list = []
|
||||
type_parts = [t.strip() for t in types_s.split(",") if t.strip()]
|
||||
# составной: несколько cfg:* или несколько ссылок в типе
|
||||
composite = len(refs_list) > 1 or len(type_parts) > 1
|
||||
row["is_composite"] = composite
|
||||
others: list[str] = []
|
||||
target_full = f"{row['to_kind']}.{row['to_name']}"
|
||||
for ref in refs_list:
|
||||
label = f"{ref.get('kind')}.{ref.get('name')}"
|
||||
if label != target_full and label not in others:
|
||||
others.append(label)
|
||||
for t in type_parts:
|
||||
# cfg:DocumentRef.X → DocumentRef.X
|
||||
short = t[4:] if t.startswith("cfg:") else t
|
||||
if short != target_full and short not in others and "Ref." in short:
|
||||
others.append(short)
|
||||
row["other_types"] = others
|
||||
|
||||
return rows, totals
|
||||
|
||||
|
||||
def get_object(
|
||||
|
||||
Reference in New Issue
Block a user