feat: sap-cli skill v2.1.0 — self-contained distributable package
- assets/: sap-cli source (v2.1.0, 26 commands, 16 object types) - references/: tool constraints + error handling (self-contained) - scripts/setup.py: one-click install/config/verify - SKILL.md: full command reference + dual-platform install guide - VERSION: 2.1.0 Built from D:/Codespace/sap-cli via scripts/pack_skill.py
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""依赖排序模块。
|
||||
|
||||
使用 Kahn 算法对清单对象进行拓扑排序,
|
||||
按 depends_on 显式依赖和 TYPE_PRIORITY 类型优先级决定执行顺序。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
from sapcli.exceptions import CyclicDependencyError
|
||||
from sapcli.manifest import ManifestEntry
|
||||
|
||||
logger = logging.getLogger("sapcli.sorter")
|
||||
|
||||
# 类型默认优先级(数值越小越先执行)
|
||||
TYPE_PRIORITY: dict[str, int] = {
|
||||
"domain": 10,
|
||||
"dataelement": 20,
|
||||
"structure": 25,
|
||||
"table": 30,
|
||||
"tabletype": 40,
|
||||
"view": 42,
|
||||
"lockobject": 44,
|
||||
"searchhelp": 46,
|
||||
"messageclass": 48,
|
||||
"interface": 50,
|
||||
"class": 60,
|
||||
"functiongroup": 70,
|
||||
"function": 75,
|
||||
"include": 78,
|
||||
"cdsview": 79,
|
||||
"report": 80,
|
||||
}
|
||||
|
||||
# 未注册类型的默认优先级
|
||||
_DEFAULT_PRIORITY = 90
|
||||
|
||||
|
||||
def topological_sort(objects: list[ManifestEntry]) -> list[ManifestEntry]:
|
||||
"""对清单对象进行拓扑排序。
|
||||
|
||||
1. 按 depends_on 建有向边(被依赖 → 当前对象)
|
||||
2. 无入边的节点按 TYPE_PRIORITY 排序作为 tie-breaker
|
||||
3. 检测循环依赖 → 抛出 CyclicDependencyError
|
||||
|
||||
Returns:
|
||||
排序后的对象列表(依赖在前,被依赖在后...不,应该是依赖在前,被依赖的对象先执行)。
|
||||
"""
|
||||
if not objects:
|
||||
return []
|
||||
|
||||
name_map: dict[str, ManifestEntry] = {obj.name: obj for obj in objects}
|
||||
names = set(name_map.keys())
|
||||
|
||||
# ── 建图:计算入度 ──
|
||||
in_degree: dict[str, int] = {name: 0 for name in names}
|
||||
# adj[A] = [B, C, ...] 表示 A 完成后可以释放 B、C 的入度
|
||||
adj: dict[str, list[str]] = {name: [] for name in names}
|
||||
|
||||
for obj in objects:
|
||||
for dep in obj.depends_on:
|
||||
if dep not in names:
|
||||
# 依赖不在当前列表中,忽略(外部依赖)
|
||||
logger.debug("忽略外部依赖: %s → %s", obj.name, dep)
|
||||
continue
|
||||
# dep → obj(dep 先执行,obj 后执行)
|
||||
adj[dep].append(obj.name)
|
||||
in_degree[obj.name] += 1
|
||||
|
||||
# ── Kahn 算法 ──
|
||||
# 初始化:入度为 0 的节点
|
||||
ready: list[str] = [
|
||||
name for name, deg in in_degree.items() if deg == 0
|
||||
]
|
||||
# 按 TYPE_PRIORITY 排序
|
||||
ready.sort(key=lambda n: _priority(n_map=name_map, n=n))
|
||||
|
||||
queue: deque[str] = deque(ready)
|
||||
sorted_names: list[str] = []
|
||||
|
||||
while queue:
|
||||
# 取出优先级最高(数值最小)的节点
|
||||
# 由于 deque 不支持按优先级取,先排序再取
|
||||
# 实际实现:每次从 ready 列表中取第一个
|
||||
current = queue.popleft()
|
||||
sorted_names.append(current)
|
||||
|
||||
# 释放后续节点
|
||||
newly_ready: list[str] = []
|
||||
for neighbor in adj[current]:
|
||||
in_degree[neighbor] -= 1
|
||||
if in_degree[neighbor] == 0:
|
||||
newly_ready.append(neighbor)
|
||||
|
||||
# 新就绪的节点按优先级排序后加入队列
|
||||
newly_ready.sort(key=lambda n: _priority(n_map=name_map, n=n))
|
||||
queue.extend(newly_ready)
|
||||
|
||||
# ── 循环检测 ──
|
||||
if len(sorted_names) != len(names):
|
||||
remaining = names - set(sorted_names)
|
||||
# 尝试找出循环链
|
||||
cycle = _detect_cycle(remaining, adj, in_degree)
|
||||
raise CyclicDependencyError(cycle)
|
||||
|
||||
result = [name_map[n] for n in sorted_names]
|
||||
logger.info("拓扑排序完成: %d 个对象", len(result))
|
||||
return result
|
||||
|
||||
|
||||
def _priority(*, n_map: dict[str, ManifestEntry], n: str) -> int:
|
||||
"""获取节点优先级数值。"""
|
||||
obj_type = n_map[n].type if n in n_map else ""
|
||||
return TYPE_PRIORITY.get(obj_type, _DEFAULT_PRIORITY)
|
||||
|
||||
|
||||
def _detect_cycle(
|
||||
remaining: set[str],
|
||||
adj: dict[str, list[str]],
|
||||
in_degree: dict[str, int],
|
||||
) -> list[str]:
|
||||
"""尝试在剩余节点中检测循环,返回循环中的节点名列表。"""
|
||||
# 沿着入度 > 0 的边走,迟早会回到已访问的节点
|
||||
if not remaining:
|
||||
return []
|
||||
|
||||
visited: set[str] = set()
|
||||
path: list[str] = []
|
||||
start = next(iter(remaining))
|
||||
|
||||
current = start
|
||||
for _ in range(len(remaining) + 1):
|
||||
if current in visited:
|
||||
# 找到循环起点
|
||||
idx = path.index(current)
|
||||
return path[idx:]
|
||||
visited.add(current)
|
||||
path.append(current)
|
||||
|
||||
# 找下一个仍在 remaining 中的后继
|
||||
found_next = False
|
||||
for neighbor in adj.get(current, []):
|
||||
if neighbor in remaining:
|
||||
current = neighbor
|
||||
found_next = True
|
||||
break
|
||||
if not found_next:
|
||||
break
|
||||
|
||||
# 无法确定精确循环链,返回所有剩余节点
|
||||
return list(remaining)
|
||||
Reference in New Issue
Block a user