diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d0e635c..cb6599165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) -## 0.9.60 (unreleased) +## 0.9.61 (unreleased) + +- Fix: `graphify.serve` now imports cleanly on Python 3.12 and 3.13. The `chinese` extra pins `jieba-py` from 3.12 onward (0.9.60 mistakenly kept the old `jieba` until 3.14, and its invalid regex escapes are a hard error on 3.12+), and the jieba import now suppresses the tokenizer's `SyntaxWarning` regardless of message or line so it never escalates under `-W error`. +- Fix: the git hook's rebuild-root guard now rejects a symlink-loop or dangling `.graphify_root` on Python 3.13, whose `Path.resolve()` no longer raises on a loop — the saved root must resolve to a real directory inside the repo before it is adopted. + +## 0.9.60 (2026-09-12) - Fix: atomic writes now fall back correctly on Windows `WinError 17` (cannot move to a different drive), not just `PermissionError` — a shared `os_replace_with_fallback` copies through a temp in the target directory and restores the original if the swap fails, keeping install/export/cache writes crash-safe (#3508, thanks @ayushcodes10). - Fix: files that could not be classified into any language or type are now surfaced (a count and top extensions in the console and GRAPH_REPORT) instead of vanishing from a "successful" run; noise and ignored paths are unaffected (#3511, thanks @ayushcodes10). diff --git a/graphify/hooks.py b/graphify/hooks.py index ac26f160e..3576f81c1 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -192,7 +192,10 @@ try: try: _cwd = Path.cwd().resolve() _resolved = _candidate.resolve() - _in_repo = _resolved == _cwd or _cwd in _resolved.parents + # Python 3.13 no longer raises on a symlink loop (resolve returns + # the path unresolved), so require a real directory: a loop or a + # dangling target is not a dir and correctly falls back. + _in_repo = (_resolved == _cwd or _cwd in _resolved.parents) and _resolved.is_dir() except (OSError, RuntimeError): _in_repo = False if _in_repo: @@ -264,7 +267,10 @@ try: try: _cwd = Path.cwd().resolve() _resolved = _candidate.resolve() - _in_repo = _resolved == _cwd or _cwd in _resolved.parents + # Python 3.13 no longer raises on a symlink loop (resolve returns + # the path unresolved), so require a real directory: a loop or a + # dangling target is not a dir and correctly falls back. + _in_repo = (_resolved == _cwd or _cwd in _resolved.parents) and _resolved.is_dir() except (OSError, RuntimeError): _in_repo = False if _in_repo: diff --git a/graphify/serve.py b/graphify/serve.py index 3869d7b1d..9301a4a32 100644 --- a/graphify/serve.py +++ b/graphify/serve.py @@ -19,14 +19,15 @@ from graphify.paths import default_graph_json as _default_graph_json try: with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message=r'^"\\\." is an invalid escape sequence\.', - category=SyntaxWarning, - lineno=7, - ) + # jieba/jieba-py carry invalid regex escapes that emit a SyntaxWarning on + # compile; the message and line moved across Python versions (3.12 also + # reworded it), so ignore the category wholesale here rather than pinning + # a message/lineno. Under `-W error::SyntaxWarning` this keeps the import + # from escalating to a fatal SyntaxError. + warnings.simplefilter("ignore", SyntaxWarning) import jieba as _jieba # type: ignore[import-untyped] -except ImportError: +except (ImportError, SyntaxError): + # A stray/broken jieba must never take down the whole server import. _jieba = None diff --git a/pyproject.toml b/pyproject.toml index e29ab2019..45776c68e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.9.60" +version = "0.9.61" description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = "Apache-2.0" @@ -81,8 +81,8 @@ anthropic = ["anthropic"] gemini = ["openai", "tiktoken"] openai = ["openai", "tiktoken"] chinese = [ - "jieba; python_version < '3.14'", - "jieba-py>=0.46.12,<1; python_version >= '3.14'", + "jieba; python_version < '3.12'", + "jieba-py>=0.46.12,<1; python_version >= '3.12'", ] sql = ["tree-sitter-sql"] # extract_pascal() uses tree-sitter-pascal for AST-quality extraction (more @@ -105,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.16.1", "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.7.4", "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"] +all = ["mcp>=1,<3", "starlette>=1.3.1,<2", "neo4j", "falkordb", "pypdf>=6.16.1", "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.7.4", "matplotlib", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba; python_version < '3.12'", "jieba-py>=0.46.12,<1; python_version >= '3.12'", "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 41cb5fac8..e67fbe9c4 100644 --- a/tests/test_backend_extras.py +++ b/tests/test_backend_extras.py @@ -46,7 +46,7 @@ def test_version_gated_optional_backends_are_packaged(): for extra in ("leiden", "all"): assert "graspologic-native>=1.3.1,<2; python_version >= '3.13'" in extras[extra] for extra in ("chinese", "all"): - assert "jieba-py>=0.46.12,<1; python_version >= '3.14'" in extras[extra] + assert "jieba-py>=0.46.12,<1; python_version >= '3.12'" in extras[extra] def test_backend_pkg_hint_points_at_uv_tool_and_extra(): diff --git a/uv.lock b/uv.lock index 8e75dab7b..fe746b504 100644 --- a/uv.lock +++ b/uv.lock @@ -6,11 +6,14 @@ resolution-markers = [ "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.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' 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.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' 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'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -548,11 +551,14 @@ resolution-markers = [ "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.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' 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.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' 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'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' 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'" }, @@ -1090,7 +1096,7 @@ wheels = [ [[package]] name = "graphifyy" -version = "0.9.59" +version = "0.9.61" source = { editable = "." } dependencies = [ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1135,8 +1141,8 @@ all = [ { name = "faster-whisper", marker = "python_full_version >= '3.11'" }, { 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'" }, - { name = "jieba", marker = "python_full_version < '3.14'" }, - { name = "jieba-py", marker = "python_full_version >= '3.14'" }, + { name = "jieba", marker = "python_full_version < '3.12'" }, + { name = "jieba-py", marker = "python_full_version >= '3.12'" }, { name = "markdownify" }, { name = "matplotlib" }, { name = "mcp" }, @@ -1166,8 +1172,8 @@ bedrock = [ { name = "boto3" }, ] chinese = [ - { name = "jieba", marker = "python_full_version < '3.14'" }, - { name = "jieba-py", marker = "python_full_version >= '3.14'" }, + { name = "jieba", marker = "python_full_version < '3.12'" }, + { name = "jieba-py", marker = "python_full_version >= '3.12'" }, ] commonlisp = [ { name = "tree-sitter-commonlisp" }, @@ -1281,10 +1287,10 @@ requires-dist = [ { name = "graspologic", marker = "python_full_version < '3.13' and extra == 'leiden'" }, { 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 = "jieba", marker = "python_full_version < '3.12' and extra == 'all'" }, + { name = "jieba", marker = "python_full_version < '3.12' and extra == 'chinese'" }, + { name = "jieba-py", marker = "python_full_version >= '3.12' and extra == 'all'", specifier = ">=0.46.12,<1" }, + { name = "jieba-py", marker = "python_full_version >= '3.12' and extra == 'chinese'", specifier = ">=0.46.12,<1" }, { name = "markdownify", marker = "extra == 'all'" }, { name = "markdownify", marker = "extra == 'pdf'" }, { name = "matplotlib", marker = "extra == 'all'" }, @@ -1424,9 +1430,12 @@ 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.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' 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" } @@ -2326,11 +2335,14 @@ resolution-markers = [ "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.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' 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.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' 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'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ @@ -2393,9 +2405,12 @@ name = "numpy" version = "1.26.4" 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.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } @@ -2669,9 +2684,12 @@ name = "pandas" version = "3.0.3" 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.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' 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'" }, @@ -4001,9 +4019,12 @@ name = "scikit-learn" version = "1.8.0" 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.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "joblib", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, @@ -4119,11 +4140,14 @@ resolution-markers = [ "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.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' 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.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' 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'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' 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'" },