Skip to content

feat(packaging): gate the leiden extra by interpreter so Leiden works on 3.13+ - #3310

Open
andrebrait wants to merge 1 commit into
Graphify-Labs:v8from
pfBlockerNG:feat/leiden-native
Open

feat(packaging): gate the leiden extra by interpreter so Leiden works on 3.13+#3310
andrebrait wants to merge 1 commit into
Graphify-Labs:v8from
pfBlockerNG:feat/leiden-native

Conversation

@andrebrait

@andrebrait andrebrait commented Sep 3, 2026

Copy link
Copy Markdown

Problem

_partition() calls graspologic_native.leiden() first, then the graspologic wrapper, then falls back to NetworkX Louvain (#3104). The leiden extra installs the graspologic distribution, whose metadata is requires_python = <3.13,>=3.9.

Below 3.13 that works — graspologic requires graspologic-native>=1.2.1,<2.0.0 itself, so the native binding _native_leiden() calls is already present. From 3.13 the marker excludes the interpreter, so uv tool install "graphifyy[leiden]" installs nothing, the native import fails, and clustering silently drops to Louvain. The extra is a no-op on a current interpreter, users lose Leiden's connectivity guarantee with no diagnostic, and the README's "Python < 3.13 only" note documents the limitation rather than a way around it.

Change

One extra, gated by interpreter:

leiden = [
    "graspologic; python_version < '3.13'",
    "graspologic-native>=1.2.1,<2.0.0; python_version >= '3.13'",
]

all mirrors the same pair. README row updated. uv.lock regenerated. Guard added to tests/test_backend_extras.py.

The arms are complementary and exhaustive: every supported interpreter gets a working backend, and an install below 3.13 resolves to exactly what it resolves to today. graspologic-native publishes cp39-abi3 wheels (Linux manylinux/musllinux x86_64+aarch64, macOS universal2, Windows amd64/arm64), so 3.13+ needs no compiler and skips graspologic's umap/pynndescent/numba install chain.

Why the 3.13+ arm has a ceiling

Not because graspologic sets one — because of how _native_leiden() calls the binding. It passes eight exact keyword arguments (edges, starting_communities, resolution, randomness, iterations, use_modularity, seed, trials) and unpacks a 2-tuple return, and the whole call sits inside except Exception: return None (cluster.py:73-91). A breaking major would therefore not raise — it would return None, fall through to a graspologic that is absent on 3.13+, and land on Louvain. That is exactly the silent degradation this change removes, so an uncapped arm could quietly re-introduce the bug on someone's next uv tool upgrade.

It also keeps the arms symmetric: below 3.13 the effective constraint is already <2.0.0 via graspologic, so without a ceiling a graspologic-native 2.0 release would leave <3.13 on 1.x while 3.13+ jumped — the arms diverging is the one thing the gate is meant to prevent. House style is the same shape for directly-consumed APIs (mcp>=1,<3, starlette>=1.3.1,<2, every tree-sitter grammar), while broad stable dependencies stay uncapped (networkx>=3.4, numpy>=1.21). The cost is the usual one — 3.13+ users wait for a graphify release after a 2.0 — and I'd take that over silent Louvain, but happy to drop the ceiling if you weigh it the other way.

Addressing the review on 7132b77

Both advisory findings are handled, and the first one prompted the redesign — thank you, it is a better change than what I first pushed.

"all extra now changes Leiden backend for existing users" — correct to flag, and the gate now makes it structurally impossible: below 3.13 all resolves to graspologic alone, unchanged. Worth recording that the original concern was also already moot, because graspologic depends on graspologic-native:

$ curl -s https://pypi.org/pypi/graspologic/json | jq -r '.info.requires_dist[]' | grep -i native
graspologic-native<2.0.0,>=1.2.1

so [leiden]/[all] on <3.13 already installed the native binding and _partition() already took _native_leiden(). The unmarked line did carry a real latent bug, though — no ceiling, so a future graspologic-native 2.x would have conflicted with graspologic's own <2.0.0 cap on <3.13. The gate plus the matching ceiling removes that.

"Test module imports Python 3.11-only tomllib" — a genuine break on the >=3.10 floor, thank you. The guard moved to tests/test_backend_extras.py, which is both the house home for extras assertions and already carries the tomllib/tomli fallback this repo uses (graphify/cargo_introspect.py:12-21), so no new import appears anywhere. tests/test_wheel_packaging.py is untouched.

Verification

$ uv run pytest -q tests/test_backend_extras.py tests/test_wheel_packaging.py
138 passed

$ uv lock --check
Resolved 203 packages

The guard fails against v8 metadata (expected exactly one graspologic-native arm), and every assertion is mutation-checked — each of these turns it red:

Mutation Result
v8 baseline, no 3.13+ arm 1 failed
arms overlap (3.13+ arm unmarked) 1 failed
gap left at 3.13 (boundary moved to 3.14) 2 failed
below-3.13 arm switched to the native package 2 failed
all drops the 3.13+ arm 1 failed

Behaviour on Python 3.13.5, real 23.7k-node / 50k-edge corpus: the native path produces 1115 communities against Louvain's 1131, with the largest communities shrinking from 619/466/422 nodes to 364/359/339 — Leiden's refinement pass doing what it should — and byte-identical output across PYTHONHASHSEED=0 and =9.

Note

ruff format --check already reports tests/test_backend_extras.py as needing reformatting on unmodified v8 (a docstring blank line and a quote style, both outside my hunks); I left that alone rather than mixing a whole-file reformat into this diff. ruff check is clean.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds a leiden-native extra pinning graspologic-native>=1.2.1 with no interpreter marker, giving Leiden community detection on Python ≥ 3.13 where the existing leiden extra can't install and _partition() would otherwise fall back to Louvain. Folds the new dependency into the all extra and documents it in the README install table. Adds test_leiden_native_extra_reaches_every_supported_python asserting the extra stays marker-free, contains only graspologic-native, leaves leiden unchanged, and that both remain subsets of all.

Worth a look

  • all extra now changes Leiden backend for existing userspyproject.toml:103 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Test module imports Python 3.11-only tomllibtests/test_wheel_packaging.py:12 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 39 functions depend on the 39 functions this change touches.

Health — grade A; no new coupling hotspots.

Verification — 39 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 39 function(s) in the blast radius were not formally verified this run

@andrebrait andrebrait changed the title feat(packaging): add a leiden-native extra so Leiden installs on Python 3.13+ feat(packaging): gate the leiden extra by interpreter so Leiden works on 3.13+ Sep 3, 2026

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

Extends the leiden extra to install graspologic-native directly on Python 3.13+ where graspologic's metadata no longer pulls in the binding, so Leiden community detection keeps working instead of silently falling back to NetworkX Louvain; the same second arm is added to the all extra. Adds tests asserting the two leiden arms stay complementary and exhaustive across supported interpreters and that all carries both.

No blocking issues surfaced. 6 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 39 functions depend on the 39 functions this change touches.

Health — grade A; no new coupling hotspots.

Verification — 39 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 39 function(s) in the blast radius were not formally verified this run

_partition() calls graspologic_native.leiden() first and falls back to the
graspologic wrapper, then to NetworkX Louvain (Graphify-Labs#3104). Below Python 3.13 the
graspologic distribution supplies that binding as a dependency of its own, so
the extra works. From 3.13 graspologic's metadata excludes the interpreter, so
`uv tool install "graphifyy[leiden]"` installs nothing at all, the native
import fails, and clustering silently drops to Louvain -- users lose Leiden's
connectivity guarantee with no diagnostic, and the README documents the
limitation rather than a way around it.

graspologic-native publishes cp39-abi3 wheels for Linux, macOS and Windows, so
naming the binding directly on 3.13+ restores Leiden with no compiler and
without graspologic's umap/pynndescent/numba install chain.

The two arms are complementary and exhaustive, so `[leiden]` installs a
working backend on every supported interpreter and existing installs below
3.13 resolve to exactly what they resolve to today. The ceiling on the 3.13+
arm is the one graspologic itself sets on the same package.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

Adds a native Leiden backend for Python 3.13+ by having the leiden extra (and all) depend on graspologic-native>=1.2.1,<2.0.0 on that interpreter while keeping graspologic below 3.13, so [leiden] installs a working backend everywhere instead of silently degrading to NetworkX Louvain where graspologic's metadata drops out. Guards this with tests asserting the two arms are complementary, exhaustive, and both carried by all. Updates the README extras table and refreshes uv.lock accordingly.

No blocking issues surfaced. 6 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 39 functions depend on the 39 functions this change touches.

Health — grade A; no new coupling hotspots.

Verification — 39 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 39 function(s) in the blast radius were not formally verified this run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant