refactor: extract shared dep parser to lib/parse-deps.py (Closes #20)
Single source of truth for dependency parsing, replacing three copies: - dev-poll.sh get_deps() now calls parse-deps.py - supervisor P3b/P3c import parse_deps() via importlib Supports stdin, argument, and --json modes for different callers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
acab6c95c8
commit
6cf580c010
3 changed files with 78 additions and 52 deletions
|
|
@ -115,18 +115,8 @@ dep_is_merged() {
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
get_deps() {
|
get_deps() {
|
||||||
local issue_body="$1"
|
local issue_body="$1"
|
||||||
# Extract #NNN references from "Depends on" / "Blocked by" sections
|
# Shared parser: lib/parse-deps.py (single source of truth)
|
||||||
# Capture the header line AND subsequent lines until next ## section
|
echo "$issue_body" | python3 "${FACTORY_ROOT}/lib/parse-deps.py"
|
||||||
{
|
|
||||||
echo "$issue_body" | awk '
|
|
||||||
BEGIN { IGNORECASE=1 }
|
|
||||||
/^##? *(Depends on|Blocked by|Dependencies)/ { capture=1; next }
|
|
||||||
capture && /^##? / { capture=0 }
|
|
||||||
capture { print }
|
|
||||||
' | grep -oP '#\K[0-9]+' || true
|
|
||||||
# Also check inline deps on same line as keyword
|
|
||||||
echo "$issue_body" | grep -iE '(depends on|blocked by)' | grep -oP '#\K[0-9]+' || true
|
|
||||||
} | sort -un
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
59
lib/parse-deps.py
Executable file
59
lib/parse-deps.py
Executable file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract dependency issue numbers from an issue body.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
echo "$ISSUE_BODY" | python3 lib/parse-deps.py
|
||||||
|
python3 lib/parse-deps.py "$ISSUE_BODY"
|
||||||
|
python3 lib/parse-deps.py --json < issues.json
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
stdin/arg: reads a single issue body, prints one dep number per line
|
||||||
|
--json: reads a JSON array of issues from stdin, prints JSON
|
||||||
|
dep graph: {"issue_num": [dep1, dep2], ...}
|
||||||
|
|
||||||
|
Matches the same logic as dev-poll.sh get_deps():
|
||||||
|
- Sections: ## Dependencies / ## Depends on / ## Blocked by
|
||||||
|
- Inline: "depends on #NNN" / "blocked by #NNN" anywhere
|
||||||
|
- Ignores: ## Related (safe for sibling cross-references)
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def parse_deps(body):
|
||||||
|
"""Return sorted list of unique dependency issue numbers from an issue body."""
|
||||||
|
deps = set()
|
||||||
|
in_section = False
|
||||||
|
for line in (body or "").split("\n"):
|
||||||
|
if re.match(r"^##?\s*(Depends on|Blocked by|Dependencies)", line, re.IGNORECASE):
|
||||||
|
in_section = True
|
||||||
|
continue
|
||||||
|
if in_section and re.match(r"^##?\s", line):
|
||||||
|
in_section = False
|
||||||
|
if in_section:
|
||||||
|
deps.update(int(m) for m in re.findall(r"#(\d+)", line))
|
||||||
|
if re.search(r"(depends on|blocked by)", line, re.IGNORECASE):
|
||||||
|
deps.update(int(m) for m in re.findall(r"#(\d+)", line))
|
||||||
|
return sorted(deps)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "--json":
|
||||||
|
issues = json.load(sys.stdin)
|
||||||
|
graph = {}
|
||||||
|
for issue in issues:
|
||||||
|
num = issue["number"]
|
||||||
|
deps = parse_deps(issue.get("body", ""))
|
||||||
|
deps = [d for d in deps if d != num]
|
||||||
|
if deps:
|
||||||
|
graph[num] = deps
|
||||||
|
json.dump(graph, sys.stdout)
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
body = sys.argv[1]
|
||||||
|
else:
|
||||||
|
body = sys.stdin.read()
|
||||||
|
for dep in parse_deps(body):
|
||||||
|
print(dep)
|
||||||
|
|
@ -297,33 +297,22 @@ status "P3: checking for circular dependencies"
|
||||||
BACKLOG_FOR_DEPS=$(codeberg_api GET "/issues?state=open&labels=backlog&type=issues&limit=50" 2>/dev/null || true)
|
BACKLOG_FOR_DEPS=$(codeberg_api GET "/issues?state=open&labels=backlog&type=issues&limit=50" 2>/dev/null || true)
|
||||||
if [ -n "$BACKLOG_FOR_DEPS" ] && [ "$BACKLOG_FOR_DEPS" != "null" ] && [ "$(echo "$BACKLOG_FOR_DEPS" | jq 'length' 2>/dev/null || echo 0)" -gt 0 ]; then
|
if [ -n "$BACKLOG_FOR_DEPS" ] && [ "$BACKLOG_FOR_DEPS" != "null" ] && [ "$(echo "$BACKLOG_FOR_DEPS" | jq 'length' 2>/dev/null || echo 0)" -gt 0 ]; then
|
||||||
|
|
||||||
|
PARSE_DEPS="${FACTORY_ROOT}/lib/parse-deps.py"
|
||||||
|
|
||||||
CYCLES=$(echo "$BACKLOG_FOR_DEPS" | python3 -c '
|
CYCLES=$(echo "$BACKLOG_FOR_DEPS" | python3 -c '
|
||||||
import sys, json, re
|
import sys, json, importlib.util
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("parse_deps", sys.argv[1])
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
|
||||||
issues = json.load(sys.stdin)
|
issues = json.load(sys.stdin)
|
||||||
|
|
||||||
def parse_deps(body):
|
|
||||||
deps = set()
|
|
||||||
in_section = False
|
|
||||||
for line in (body or "").split("\n"):
|
|
||||||
if re.match(r"^##?\s*(Depends on|Blocked by|Dependencies)", line, re.IGNORECASE):
|
|
||||||
in_section = True
|
|
||||||
continue
|
|
||||||
if in_section and re.match(r"^##?\s", line):
|
|
||||||
in_section = False
|
|
||||||
if in_section:
|
|
||||||
deps.update(int(m) for m in re.findall(r"#(\d+)", line))
|
|
||||||
if re.search(r"(depends on|blocked by)", line, re.IGNORECASE):
|
|
||||||
deps.update(int(m) for m in re.findall(r"#(\d+)", line))
|
|
||||||
return deps
|
|
||||||
|
|
||||||
graph = {}
|
graph = {}
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
num = issue["number"]
|
num = issue["number"]
|
||||||
deps = parse_deps(issue.get("body", ""))
|
deps = [d for d in mod.parse_deps(issue.get("body", "")) if d != num]
|
||||||
deps.discard(num)
|
|
||||||
if deps:
|
if deps:
|
||||||
graph[num] = deps
|
graph[num] = set(deps)
|
||||||
|
|
||||||
WHITE, GRAY, BLACK = 0, 1, 2
|
WHITE, GRAY, BLACK = 0, 1, 2
|
||||||
color = {n: WHITE for n in graph}
|
color = {n: WHITE for n in graph}
|
||||||
|
|
@ -352,7 +341,7 @@ for cycle in cycles:
|
||||||
if key not in seen:
|
if key not in seen:
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
print(" -> ".join(f"#{n}" for n in cycle))
|
print(" -> ".join(f"#{n}" for n in cycle))
|
||||||
' 2>/dev/null || true)
|
' "$PARSE_DEPS" 2>/dev/null || true)
|
||||||
|
|
||||||
if [ -n "$CYCLES" ]; then
|
if [ -n "$CYCLES" ]; then
|
||||||
while IFS= read -r cycle; do
|
while IFS= read -r cycle; do
|
||||||
|
|
@ -367,36 +356,24 @@ for cycle in cycles:
|
||||||
status "P3: checking for stale dependencies"
|
status "P3: checking for stale dependencies"
|
||||||
|
|
||||||
STALE_DEPS=$(echo "$BACKLOG_FOR_DEPS" | CODEBERG_TOKEN="$CODEBERG_TOKEN" CODEBERG_API="$CODEBERG_API" python3 -c '
|
STALE_DEPS=$(echo "$BACKLOG_FOR_DEPS" | CODEBERG_TOKEN="$CODEBERG_TOKEN" CODEBERG_API="$CODEBERG_API" python3 -c '
|
||||||
import sys, json, re, os
|
import sys, json, os, importlib.util
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("parse_deps", sys.argv[1])
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
|
||||||
issues = json.load(sys.stdin)
|
issues = json.load(sys.stdin)
|
||||||
token = os.environ.get("CODEBERG_TOKEN", "")
|
token = os.environ.get("CODEBERG_TOKEN", "")
|
||||||
api = os.environ.get("CODEBERG_API", "")
|
api = os.environ.get("CODEBERG_API", "")
|
||||||
issue_map = {i["number"]: i for i in issues}
|
issue_map = {i["number"]: i for i in issues}
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
def parse_deps(body):
|
|
||||||
deps = set()
|
|
||||||
in_section = False
|
|
||||||
for line in (body or "").split("\n"):
|
|
||||||
if re.match(r"^##?\s*(Depends on|Blocked by|Dependencies)", line, re.IGNORECASE):
|
|
||||||
in_section = True
|
|
||||||
continue
|
|
||||||
if in_section and re.match(r"^##?\s", line):
|
|
||||||
in_section = False
|
|
||||||
if in_section:
|
|
||||||
deps.update(int(m) for m in re.findall(r"#(\d+)", line))
|
|
||||||
if re.search(r"(depends on|blocked by)", line, re.IGNORECASE):
|
|
||||||
deps.update(int(m) for m in re.findall(r"#(\d+)", line))
|
|
||||||
return deps
|
|
||||||
|
|
||||||
checked = {}
|
checked = {}
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
num = issue["number"]
|
num = issue["number"]
|
||||||
deps = parse_deps(issue.get("body", ""))
|
deps = [d for d in mod.parse_deps(issue.get("body", "")) if d != num]
|
||||||
deps.discard(num)
|
|
||||||
for dep in deps:
|
for dep in deps:
|
||||||
if dep in checked:
|
if dep in checked:
|
||||||
dep_data = checked[dep]
|
dep_data = checked[dep]
|
||||||
|
|
@ -423,7 +400,7 @@ for issue in issues:
|
||||||
print(f"#{num} blocked by #{dep} \"{dep_title}\" (open {age_days} days)")
|
print(f"#{num} blocked by #{dep} \"{dep_title}\" (open {age_days} days)")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
' 2>/dev/null || true)
|
' "$PARSE_DEPS" 2>/dev/null || true)
|
||||||
|
|
||||||
if [ -n "$STALE_DEPS" ]; then
|
if [ -n "$STALE_DEPS" ]; then
|
||||||
while IFS= read -r stale; do
|
while IFS= read -r stale; do
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue