Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ Codex users also need `multi_agent = true` under `[features]` in `~/.codex/confi
| `neo4j` | Neo4j push support | `uv tool install "graphifyy[neo4j]"` |
| `falkordb` | FalkorDB push support | `uv tool install "graphifyy[falkordb]"` |
| `svg` | SVG graph export | `uv tool install "graphifyy[svg]"` |
| `leiden` | Leiden community detection (Python < 3.13 only) | `uv tool install "graphifyy[leiden]"` |
| `leiden` | Leiden community detection (Python < 3.13: graspologic; 3.13+: the native abi3 wheel) | `uv tool install "graphifyy[leiden]"` |
| `ollama` | Ollama local inference | `uv tool install "graphifyy[ollama]"` |
| `openai` | OpenAI / OpenAI-compatible APIs | `uv tool install "graphifyy[openai]"` |
| `gemini` | Google Gemini API | `uv tool install "graphifyy[gemini]"` |
Expand Down
16 changes: 14 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,19 @@ falkordb = ["falkordb"]
pdf = ["pypdf>=6.12.0", "markdownify"]
watch = ["watchdog"]
svg = ["matplotlib", "numpy>=2.0; python_version >= '3.13'"]
leiden = ["graspologic; python_version < '3.13'"]
# _partition() calls graspologic_native.leiden() directly (#3104). Below 3.13 the
# graspologic distribution supplies that binding as a dependency of its own; from 3.13
# graspologic's metadata excludes the interpreter, so name the binding directly there
# and Leiden keeps working instead of silently falling back to NetworkX Louvain. The
# arms are exhaustive, so `[leiden]` installs a working backend on every supported
# interpreter. Ceiling: _native_leiden() passes eight exact keyword arguments and
# swallows every exception, so a breaking major would return None and degrade to
# Louvain silently -- the bug this gate exists to fix. It also keeps the arms
# symmetric, since graspologic caps the same package below 2.0 on the other arm.
leiden = [
"graspologic; python_version < '3.13'",
"graspologic-native>=1.2.1,<2.0.0; python_version >= '3.13'",
]
office = ["python-docx", "openpyxl"]
google = ["openpyxl"]
# --postgres reconstructs DDL and feeds it to extract_sql(), which needs the
Expand Down Expand Up @@ -98,7 +110,7 @@ commonlisp = ["tree-sitter-commonlisp"]
# every platform, no C toolchain; optional because Robot Framework corpora are
# QA-automation specific.
robot = ["robotframework>=4.0"]
all = ["mcp>=1,<3", "starlette>=1.3.1,<2", "neo4j", "falkordb", "pypdf>=6.12.0", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9", "matplotlib", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal", "tree-sitter-ocaml", "tree-sitter-commonlisp", "robotframework>=4.0"]
all = ["mcp>=1,<3", "starlette>=1.3.1,<2", "neo4j", "falkordb", "pypdf>=6.12.0", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "graspologic-native>=1.2.1,<2.0.0; python_version >= '3.13'", "python-docx", "openpyxl", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9", "matplotlib", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal", "tree-sitter-ocaml", "tree-sitter-commonlisp", "robotframework>=4.0"]

[project.scripts]
graphify = "graphify.__main__:main"
Expand Down
25 changes: 25 additions & 0 deletions tests/test_backend_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,28 @@ def test_backend_pkg_hint_points_at_uv_tool_and_extra():
assert "uv tool install" in msg
assert 'graphifyy[anthropic]' in msg
assert "pip install anthropic" in msg # pip/venv fallback still mentioned


def _leiden_arms(extras: dict) -> dict[str, str]:
"""Map each `leiden` requirement to its interpreter marker."""
return {dep.split(";")[0].strip(): dep.partition(";")[2].strip() for dep in extras["leiden"]}


def test_leiden_extra_installs_a_working_backend_on_every_supported_python():
"""`_partition()` calls graspologic_native.leiden() first and falls back to
NetworkX Louvain when the binding is absent. graspologic's own metadata stops at
3.13, so below that it supplies the binding and from 3.13 the extra must name it
directly -- otherwise `[leiden]` installs nothing on a current interpreter and
clustering silently loses Leiden. The two arms must stay complementary and
exhaustive, or some interpreter is left with no backend."""
arms = _leiden_arms(_extras())
assert arms.get("graspologic") == "python_version < '3.13'", f"below-3.13 arm: {arms}"
native = [pkg for pkg in arms if pkg.startswith("graspologic-native")]
assert len(native) == 1, f"expected exactly one graspologic-native arm, got {arms}"
assert arms[native[0]] == "python_version >= '3.13'", f"3.13+ arm: {arms}"
assert len(arms) == 2, f"a third arm leaves the gate ambiguous: {arms}"


def test_all_extra_gates_leiden_the_same_way():
extras = _extras()
assert set(extras["leiden"]) <= set(extras["all"]), "[all] must carry both leiden arms"
Loading