diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 548b40f22..b6f5e373f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.12"] + python-version: ["3.10", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 diff --git a/graphify/cluster.py b/graphify/cluster.py index 34952a44a..ad1af7b32 100644 --- a/graphify/cluster.py +++ b/graphify/cluster.py @@ -90,7 +90,16 @@ def _native_leiden(stable: nx.Graph, resolution: float) -> dict[str, int] | None except Exception: return None - return {id_to_node[node_id]: community for node_id, community in native_partitions.items()} + partition = { + id_to_node[node_id]: community + for node_id, community in native_partitions.items() + } + next_community = max(partition.values(), default=-1) + 1 + for node in stable.nodes(): + if node not in partition: + partition[node] = next_community + next_community += 1 + return partition def _partition(G: nx.Graph, resolution: float = 1.0) -> dict[str, int]: diff --git a/graphify/serve.py b/graphify/serve.py index 595f434ab..9676776ba 100644 --- a/graphify/serve.py +++ b/graphify/serve.py @@ -5,6 +5,7 @@ import math import os import re import sys +import warnings from array import array from collections import OrderedDict from pathlib import Path @@ -17,7 +18,9 @@ from graphify.build import edge_data, edge_datas from graphify.paths import default_graph_json as _default_graph_json try: - import jieba as _jieba # type: ignore[import-untyped] + with warnings.catch_warnings(): + warnings.simplefilter("ignore", SyntaxWarning) + import jieba as _jieba # type: ignore[import-untyped] except ImportError: _jieba = None diff --git a/pyproject.toml b/pyproject.toml index 1c80dbe94..901c9c7af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,10 @@ 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'"] +leiden = [ + "graspologic; python_version < '3.13'", + "graspologic-native>=1.3.1,<2; python_version >= '3.13'", +] office = ["python-docx", "openpyxl"] google = ["openpyxl"] # --postgres reconstructs DDL and feeds it to extract_sql(), which needs the @@ -77,7 +80,10 @@ bedrock = ["boto3"] anthropic = ["anthropic"] gemini = ["openai", "tiktoken"] openai = ["openai", "tiktoken"] -chinese = ["jieba"] +chinese = [ + "jieba; python_version < '3.14'", + "jieba-py>=0.46.12,<1; python_version >= '3.14'", +] sql = ["tree-sitter-sql"] # extract_pascal() uses tree-sitter-pascal for AST-quality extraction (more # accurate calls/inherits edges) and falls back to a regex extractor when it is @@ -99,7 +105,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", "psycopg[binary]", "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.3.1,<2; python_version >= '3.13'", "python-docx", "openpyxl", "psycopg[binary]", "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; python_version < '3.14'", "jieba-py>=0.46.12,<1; python_version >= '3.14'", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal", "tree-sitter-ocaml", "tree-sitter-commonlisp", "robotframework>=4.0"] [project.scripts] graphify = "graphify.__main__:main" diff --git a/tests/test_backend_extras.py b/tests/test_backend_extras.py index 4967ec917..84ed07ada 100644 --- a/tests/test_backend_extras.py +++ b/tests/test_backend_extras.py @@ -34,6 +34,13 @@ def test_anthropic_in_all_extra(): assert any("anthropic" in dep for dep in extras["all"]), "[all] must include anthropic" +def test_postgres_driver_in_all_extra(): + extras = _extras() + assert any(dep.startswith("psycopg") for dep in extras["all"]), ( + "[all] must include the PostgreSQL driver" + ) + + def test_backend_pkg_hint_points_at_uv_tool_and_extra(): msg = _backend_pkg_hint("anthropic", "anthropic") assert "uv tool install" in msg diff --git a/tests/test_build_merge_hyperedges_and_prune.py b/tests/test_build_merge_hyperedges_and_prune.py index 2e341c3b8..46b92c7c6 100644 --- a/tests/test_build_merge_hyperedges_and_prune.py +++ b/tests/test_build_merge_hyperedges_and_prune.py @@ -267,7 +267,7 @@ def test_prune_reextracted_absolute_node_not_deleted(tmp_path): def test_graphify_root_marker_with_a_utf8_bom_still_resolves(tmp_path): - """A marker written by Windows PowerShell 5.1 carries a UTF-8 BOM (#3028). + r"""A marker written by Windows PowerShell 5.1 carries a UTF-8 BOM (#3028). `Out-File -Encoding utf8` on 5.1 always prepends EF BB BF — there is no BOM-less utf8 there — and `str.strip()` does not remove U+FEFF, so the BOM diff --git a/tests/test_cluster.py b/tests/test_cluster.py index fa4d0a265..a07801417 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -137,6 +137,32 @@ def test_native_leiden_matches_graspologic_wrapper(monkeypatch): ) +def test_native_leiden_returns_complete_partition(): + """The native-only dependency path used on Python 3.13+ must run Leiden, + not silently fall through to NetworkX Louvain.""" + import pytest + if sys.version_info < (3, 13): + pytest.skip("graspologic-native is required directly on Python 3.13+") + import graspologic_native # noqa: F401 + import graphify.cluster as cl + + G = nx.Graph() + G.add_node("isolated") + for a, b in [("a1", "a2"), ("a1", "a3"), ("a2", "a3"), + ("b1", "b2"), ("b1", "b3"), ("b2", "b3"), ("a1", "b1")]: + G.add_edge(a, b) + + partition = cl._native_leiden(G, 1.0) + + assert partition is not None + assert set(partition) == set(G) + assert _grouping(partition) == { + frozenset({"a1", "a2", "a3"}), + frozenset({"b1", "b2", "b3"}), + frozenset({"isolated"}), + } + + def test_native_leiden_returns_none_when_binding_absent(monkeypatch): """When graspologic_native cannot be imported, _native_leiden must return None so _partition falls through to the wrapper / Louvain, not crash.""" diff --git a/tests/test_serve.py b/tests/test_serve.py index 3d4a943b0..15c207c39 100644 --- a/tests/test_serve.py +++ b/tests/test_serve.py @@ -1,5 +1,7 @@ """Tests for serve.py - MCP graph query helpers (no mcp package required).""" import json +import subprocess +import sys import unicodedata import pytest @@ -1126,6 +1128,20 @@ def test_query_graph_text_context_filter_aliases_resolve(): # --- Chinese segmentation --- +def test_serve_import_is_clean_under_syntax_warnings(tmp_path): + """Optional tokenizers must remain importable under Python's strict warning mode.""" + result = subprocess.run( + [ + sys.executable, + "-X", f"pycache_prefix={tmp_path / 'pycache'}", + "-W", "error::SyntaxWarning", + "-c", "import graphify.serve as serve; assert serve._jieba is not None", + ], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + def test_query_terms_chinese_segments_with_cached_jieba(monkeypatch): """Chinese text should use the cached jieba module and keep the original term.""" import graphify.serve as serve_mod diff --git a/uv.lock b/uv.lock index 42f4b9a68..fa7b5e331 100644 --- a/uv.lock +++ b/uv.lock @@ -1090,7 +1090,7 @@ wheels = [ [[package]] name = "graphifyy" -version = "0.9.57" +version = "0.9.59" source = { editable = "." } dependencies = [ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1134,7 +1134,9 @@ all = [ { name = "falkordb" }, { name = "faster-whisper", marker = "python_full_version >= '3.11'" }, { name = "graspologic", marker = "python_full_version < '3.13'" }, - { name = "jieba" }, + { name = "graspologic-native", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "jieba", marker = "python_full_version < '3.14'" }, + { name = "jieba-py", marker = "python_full_version >= '3.14'" }, { name = "markdownify" }, { name = "matplotlib" }, { name = "mcp" }, @@ -1164,7 +1166,8 @@ bedrock = [ { name = "boto3" }, ] chinese = [ - { name = "jieba" }, + { name = "jieba", marker = "python_full_version < '3.14'" }, + { name = "jieba-py", marker = "python_full_version >= '3.14'" }, ] commonlisp = [ { name = "tree-sitter-commonlisp" }, @@ -1188,6 +1191,7 @@ kimi = [ ] leiden = [ { name = "graspologic", marker = "python_full_version < '3.13'" }, + { name = "graspologic-native", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] mcp = [ { name = "mcp" }, @@ -1275,8 +1279,12 @@ requires-dist = [ { name = "faster-whisper", marker = "python_full_version >= '3.11' and extra == 'video'" }, { name = "graspologic", marker = "python_full_version < '3.13' and extra == 'all'" }, { name = "graspologic", marker = "python_full_version < '3.13' and extra == 'leiden'" }, - { name = "jieba", marker = "extra == 'all'" }, - { name = "jieba", marker = "extra == 'chinese'" }, + { name = "graspologic-native", marker = "python_full_version >= '3.13' and extra == 'all'", specifier = ">=1.3.1,<2" }, + { name = "graspologic-native", marker = "python_full_version >= '3.13' and extra == 'leiden'", specifier = ">=1.3.1,<2" }, + { name = "jieba", marker = "python_full_version < '3.14' and extra == 'all'" }, + { name = "jieba", marker = "python_full_version < '3.14' and extra == 'chinese'" }, + { name = "jieba-py", marker = "python_full_version >= '3.14' and extra == 'all'", specifier = ">=0.46.12,<1" }, + { name = "jieba-py", marker = "python_full_version >= '3.14' and extra == 'chinese'", specifier = ">=0.46.12,<1" }, { name = "markdownify", marker = "extra == 'all'" }, { name = "markdownify", marker = "extra == 'pdf'" }, { name = "matplotlib", marker = "extra == 'all'" }, @@ -1389,7 +1397,7 @@ dependencies = [ { name = "beartype", marker = "python_full_version < '3.13'" }, { name = "future", marker = "python_full_version < '3.13'" }, { name = "gensim", marker = "python_full_version < '3.13'" }, - { name = "graspologic-native", marker = "python_full_version < '3.13'" }, + { name = "graspologic-native", version = "1.2.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "hyppo", marker = "python_full_version < '3.13'" }, { name = "joblib", marker = "python_full_version < '3.13'" }, { name = "matplotlib", marker = "python_full_version < '3.13'" }, @@ -1415,6 +1423,12 @@ wheels = [ name = "graspologic-native" version = "1.2.5" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11'", +] sdist = { url = "https://files.pythonhosted.org/packages/25/2d/62b30d89533643ccf4778a18eb023f291b8877b5d85de3342f07b2d363a7/graspologic_native-1.2.5.tar.gz", hash = "sha256:27ea7e01fa44466c0b4cdd678d4561e5d3dc0cb400015683b7ae1386031257a0", size = 2512729, upload-time = "2025-04-02T19:34:22.961Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ae/86/10748f4c474b0c8f6060dd379bb0c4da5d42779244bb13a58656ffb44a03/graspologic_native-1.2.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bf05f2e162ae2a2a8d6e8cfccbe3586d1faa0b808159ff950478348df557c61e", size = 648437, upload-time = "2025-04-02T19:34:16.29Z" }, @@ -1423,6 +1437,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/51/21097af79f3d68626539ab829bdbf6cc42933f020e161972927d916e394c/graspologic_native-1.2.5-cp38-abi3-win_amd64.whl", hash = "sha256:c3ef2172d774083d7e2c8e77daccd218571ddeebeb2c1703cebb1a2cc4c56e07", size = 210438, upload-time = "2025-04-02T19:34:21.139Z" }, ] +[[package]] +name = "graspologic-native" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/25/607bcadd51954aa98892e44d1536560abb06a81ff48288982bd8303983ee/graspologic_native-1.3.1.tar.gz", hash = "sha256:fa6be9c292508b8a876d45ac98ee550534d9e33fcf7129a6df8a95573101be4c", size = 2655991, upload-time = "2026-06-14T01:01:51.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/f7/c3642c95d7c3f60ade4a7b1d583bb76e9410e9ffe4185701ac87b533ccf2/graspologic_native-1.3.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:72200aa468481d78d5b26735030c98f4a9e3388930c93043e1c01ef9fb2a0a62", size = 730640, upload-time = "2026-06-14T01:01:41.578Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d6/79475dec15daa002c066494283baac911b65d7956ed6a22c5f685c20987e/graspologic_native-1.3.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4319b6d7f9e01d52986fbc13ffcf50e262351e9c78b0a3453c9130cdd1779c", size = 403084, upload-time = "2026-06-14T01:01:43.12Z" }, + { url = "https://files.pythonhosted.org/packages/8d/73/5bcb4928f973996465cbd5dc1b33f9cbfbf1227d08fb3e0b8e82bb485a95/graspologic_native-1.3.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f622699db442f55fe754e44abef2586ce389a9e47e049208e4b324edbef9131", size = 417429, upload-time = "2026-06-14T01:01:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b7/0c2774a0f566bbb58f23ab450e4a3b89221b79e0c7aff5736c887c16d78b/graspologic_native-1.3.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7fc1fd3fb6ee65c940053774841f6c74d3b88a8ab7afb92d5ac8cc09241394ff", size = 471428, upload-time = "2026-06-14T01:01:45.664Z" }, + { url = "https://files.pythonhosted.org/packages/5f/25/887e991b4466c6ca07d6670f94b1382df87b302c0c996bc4e0ddd05ddc43/graspologic_native-1.3.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:279d922d977cd4a2cb70590a73fe589bb527e1171448d040b5e2b47e6aa58432", size = 619869, upload-time = "2026-06-14T01:01:47.045Z" }, + { url = "https://files.pythonhosted.org/packages/64/61/f324b76d007eb765792acba95a39f9c0e749d2338049bc3a67b5adc75f65/graspologic_native-1.3.1-cp39-abi3-win_amd64.whl", hash = "sha256:85dfb19cedd7b9ca627b3d1f283d3270af0a3c2e8378c9e61fe1f56d09a2a1a0", size = 260864, upload-time = "2026-06-14T01:01:48.634Z" }, + { url = "https://files.pythonhosted.org/packages/37/f6/c6fda035cb637e3fe205cc58c555ea68a091d519f7527b7d1351869cb0cd/graspologic_native-1.3.1-cp39-abi3-win_arm64.whl", hash = "sha256:8a631a522b597ff5cf1907efc9b03eb1bc268d8702cf0777efb85769dff29dab", size = 246883, upload-time = "2026-06-14T01:01:49.92Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1602,6 +1643,15 @@ version = "0.42.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } +[[package]] +name = "jieba-py" +version = "0.46.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/03/f940a9f8fc2441ef1175369c01726add656dc55fea9edf1421733467050d/jieba_py-0.46.12.tar.gz", hash = "sha256:c74a4ecf7e579c00915feeefd40e54450c1616ddfa18497cb0d42563f7e4eb97", size = 5310676, upload-time = "2026-04-20T23:27:27.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/26/b7bbcedc1bfa49fe44d71233819deb95026f13ed10936a175c6b2f8a0f05/jieba_py-0.46.12-py3-none-any.whl", hash = "sha256:a4f616b571cbb2f3668608973bcd882a04e2faa94b95338acbbddf3cbe111930", size = 5403721, upload-time = "2026-04-20T23:27:18.386Z" }, +] + [[package]] name = "jiter" version = "0.14.0" @@ -4065,12 +4115,19 @@ name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [