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
41 changes: 27 additions & 14 deletions graphify/manifest_ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,31 @@

_MAX_MANIFEST_BYTES = 2_000_000 # 2 MB cap — manifests are small; this rejects junk

_TOMLI_REQUIRED = (
"Package-manifest ingestion on Python < 3.11 needs tomli. "
"Install with: pip install 'tomli' "
"(or reinstall graphifyy, which declares tomli for python_version < '3.11')."
)


def _load_toml_module():
"""Return a tomllib-compatible module, or raise ImportError (#3283).

Returning ``None`` used to look identical to a virtual workspace root with
nothing to emit, so missing ``tomli`` on Python 3.10 silently dropped every
``Cargo.toml`` / ``pyproject.toml``. Fail loud the way ``cargo_introspect``
already does.
"""
try:
import tomllib as _toml # type: ignore[import-not-found]
return _toml
except ImportError:
try:
import tomli as _toml # type: ignore[import-not-found,no-redef]
return _toml
except ImportError as exc:
raise ImportError(_TOMLI_REQUIRED) from exc


def is_package_manifest_path(path: Path) -> bool:
"""True if ``path`` is a recognized package manifest (by filename)."""
Expand Down Expand Up @@ -182,13 +207,7 @@ def _pep508_name(spec: str) -> str:


def _parse_pyproject(text: str) -> dict | None:
try:
import tomllib as _toml
except ImportError:
try:
import tomli as _toml # type: ignore
except ImportError:
return None
_toml = _load_toml_module()
data = _toml.loads(text)
proj = data.get("project", {}) if isinstance(data.get("project"), dict) else {}
poetry = (data.get("tool", {}) or {}).get("poetry", {}) if isinstance(data.get("tool"), dict) else {}
Expand All @@ -207,13 +226,7 @@ def _parse_cargo(text: str) -> dict | None:
"""Cargo.toml: name/version from ``[package]``, runtime deps from
``[dependencies]`` plus every ``[target.<cfg>.dependencies]`` table (mirrors
``_parse_pyproject``'s runtime-only scope; dev-/build-dependencies excluded)."""
try:
import tomllib as _toml
except ImportError: # pragma: no cover — Python < 3.11 without tomli only
try:
import tomli as _toml # type: ignore
except ImportError:
return None
_toml = _load_toml_module()
data = _toml.loads(text)
pkg = data.get("package", {}) if isinstance(data.get("package"), dict) else {}
name = pkg.get("name")
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies = [
"networkx>=3.4",
"numpy>=1.21",
"rapidfuzz>=3.0",
"tomli>=2.0.1; python_version < '3.11'",
"tree-sitter>=0.23.0,<0.26",
"tree-sitter-python>=0.23,<0.26",
"tree-sitter-javascript>=0.23,<0.26",
Expand Down
64 changes: 64 additions & 0 deletions tests/test_manifest_tomli_required.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Regression tests for missing tomli on Python < 3.11 (#3283)."""
from __future__ import annotations

import builtins
import sys
from pathlib import Path

import pytest

from graphify.manifest_ingest import (
_TOMLI_REQUIRED,
_load_toml_module,
_parse_cargo,
_parse_pyproject,
extract_package_manifest,
)


def test_load_toml_module_raises_when_tomli_missing(monkeypatch):
real_import = builtins.__import__

def fake_import(name, *args, **kwargs):
if name in ("tomllib", "tomli"):
raise ImportError(f"blocked {name}")
return real_import(name, *args, **kwargs)

monkeypatch.setattr(builtins, "__import__", fake_import)
with pytest.raises(ImportError, match="tomli"):
_load_toml_module()


def test_parse_pyproject_surfaces_missing_parser(monkeypatch):
monkeypatch.setattr(
"graphify.manifest_ingest._load_toml_module",
lambda: (_ for _ in ()).throw(ImportError(_TOMLI_REQUIRED)),
)
with pytest.raises(ImportError, match="tomli"):
_parse_pyproject('[project]\nname = "x"\n')


def test_parse_cargo_surfaces_missing_parser(monkeypatch):
monkeypatch.setattr(
"graphify.manifest_ingest._load_toml_module",
lambda: (_ for _ in ()).throw(ImportError(_TOMLI_REQUIRED)),
)
with pytest.raises(ImportError, match="tomli"):
_parse_cargo('[package]\nname = "x"\nversion = "0.1.0"\n')


def test_extract_package_manifest_reports_missing_tomli(tmp_path, monkeypatch):
p = tmp_path / "pyproject.toml"
p.write_text('[project]\nname = "cool"\nversion = "0.1"\n', encoding="utf-8")
monkeypatch.setattr(
"graphify.manifest_ingest._load_toml_module",
lambda: (_ for _ in ()).throw(ImportError(_TOMLI_REQUIRED)),
)
result = extract_package_manifest(p)
assert result["nodes"] == []
assert "tomli" in result.get("error", "").lower()


@pytest.mark.skipif(sys.version_info >= (3, 11), reason="stdlib tomllib present")
def test_tomli_is_importable_on_py310():
import tomli # noqa: F401