From ee659dd9ccb43fc9ed4747d372d53276a53a3bad Mon Sep 17 00:00:00 2001 From: cfrei Date: Mon, 8 Jun 2026 16:22:51 +0200 Subject: [PATCH] fix: guard label/text normalizers against None node labels Nodes can carry label=None (OpenAI-compatible LLM backends emit null labels during semantic extraction). Callers use dict.get("label", fallback), which returns None for an explicit null value (the fallback only applies when the key is absent). That None reaches helpers calling unicodedata.normalize(...), crashing the whole extract pipeline with: TypeError: normalize() argument 2 must be str, not None at whichever normalizer runs first (dedup -> build -> export): - dedup._norm - build._norm_label - export._strip_diacritics - serve._strip_diacritics Extraction is cached before the build step, so the crash recurs on every re-run until the cache is wiped, with no --no-dedup escape hatch. Coerce non-str input to "" at each chokepoint; a null label then normalizes to "" (already skipped by surrounding 'if key:' guards). Same class as #454. Fixes #1194 --- graphify/build.py | 4 +++- graphify/dedup.py | 4 +++- graphify/export.py | 4 +++- graphify/serve.py | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/graphify/build.py b/graphify/build.py index 1e040420e..5893ef2aa 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -311,8 +311,10 @@ def build( return build_from_json(combined, directed=directed, root=root) -def _norm_label(label: str) -> str: +def _norm_label(label: str | None) -> str: """Canonical dedup key — Unicode-aware, preserves CJK/word characters.""" + if not isinstance(label, str): + label = "" if label is None else str(label) label = unicodedata.normalize("NFKC", label) return re.sub(r"[\W_ ]+", " ", label.casefold(), flags=re.UNICODE).strip() diff --git a/graphify/dedup.py b/graphify/dedup.py index b2885fe5c..913975a26 100644 --- a/graphify/dedup.py +++ b/graphify/dedup.py @@ -15,8 +15,10 @@ # ── helpers ─────────────────────────────────────────────────────────────────── -def _norm(label: str) -> str: +def _norm(label: str | None) -> str: """Lowercase + collapse non-alphanumeric runs to space (Unicode-aware).""" + if not isinstance(label, str): + label = "" if label is None else str(label) label = unicodedata.normalize("NFKC", label) return re.sub(r"[\W_]+", " ", label.casefold(), flags=re.UNICODE).strip() diff --git a/graphify/export.py b/graphify/export.py index e9f7b5040..26fb20aca 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -102,8 +102,10 @@ def _obsidian_tag(name: str) -> str: return re.sub(r"[^a-zA-Z0-9_\-/]", "", name.replace(" ", "_")) -def _strip_diacritics(text: str) -> str: +def _strip_diacritics(text: str | None) -> str: import unicodedata + if not isinstance(text, str): + text = "" if text is None else str(text) nfkd = unicodedata.normalize("NFKD", text) return "".join(c for c in nfkd if not unicodedata.combining(c)) diff --git a/graphify/serve.py b/graphify/serve.py index a349f511b..6e05b6a98 100644 --- a/graphify/serve.py +++ b/graphify/serve.py @@ -51,8 +51,10 @@ def _communities_from_graph(G: nx.Graph) -> dict[int, list[str]]: return communities -def _strip_diacritics(text: str) -> str: +def _strip_diacritics(text: str | None) -> str: import unicodedata + if not isinstance(text, str): + text = "" if text is None else str(text) nfkd = unicodedata.normalize("NFKD", text) return "".join(c for c in nfkd if not unicodedata.combining(c))