Skip to content

Commit 09185c0

Browse files
committed
Fix NumPy compatibility for Python 3.12+ and enhance Pydantic compatibility
- CRITICAL: Fix NumPy build failure on Python 3.12+ (resolves #19) * Split NumPy constraints to handle Python 3.12+ requiring NumPy 1.26.0+ * Resolves 'ModuleNotFoundError: No module named distutils' errors - Fix Pydantic v1/v2 compatibility issues throughout codebase * Add runtime version detection and compatibility helpers * Update JSON serialization to use compatible methods - Bump version to 0.1.11
1 parent 9cc33d0 commit 09185c0

File tree

6 files changed

+192
-57
lines changed

6 files changed

+192
-57
lines changed

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.11] - 2025-07-21
9+
10+
### Fixed
11+
- **CRITICAL**: Fixed NumPy build failure on Python 3.12+ (addresses [#19](https://github.com/codellm-devkit/codeanalyzer-python/issues/19))
12+
- Updated NumPy dependency constraints to handle Python 3.12+ compatibility
13+
- Split NumPy version constraints into three tiers:
14+
- `numpy>=1.21.0,<1.24.0` for Python < 3.11
15+
- `numpy>=1.24.0,<2.0.0` for Python 3.11.x
16+
- `numpy>=1.26.0,<2.0.0` for Python 3.12+ (requires NumPy 1.26+ which supports Python 3.12)
17+
- Resolves `ModuleNotFoundError: No module named 'distutils'` errors on Python 3.12+
18+
- Ensures compatibility with Python 3.12 which removed `distutils` from the standard library
19+
- Fixed Pydantic v1/v2 compatibility issues in JSON serialization throughout codebase
20+
- Added comprehensive Pydantic version detection and compatibility layer
21+
- Introduced `model_dump_json()` and `model_validate_json()` helper functions for cross-version compatibility
22+
- Fixed `PyApplication.parse_raw()` deprecated method usage (replaced with `model_validate_json()`)
23+
- Updated CLI output methods to use compatible serialization functions
24+
- Resolved forward reference updates only for Pydantic v1 (v2 handles these automatically)
25+
26+
### Changed
27+
- Enhanced Pydantic compatibility infrastructure in schema module
28+
- Added runtime Pydantic version detection using `importlib.metadata`
29+
- Created compatibility abstraction layer for JSON serialization/deserialization
30+
- Improved forward reference resolution logic to work with both Pydantic v1 and v2
31+
- Updated all JSON serialization calls to use new compatibility functions
32+
- Better error handling for missing Pydantic dependency
33+
34+
### Technical Details
35+
- Added `packaging` dependency for robust version comparison
36+
- Enhanced schema module with runtime version detection and compatibility helpers
37+
- Updated core analysis caching system to use compatible Pydantic JSON methods
38+
- Improved CLI output formatting with cross-version Pydantic support
39+
840
## [0.1.10] - 2025-07-20
941

1042
### Added

codeanalyzer/__main__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from codeanalyzer.core import Codeanalyzer
77
from codeanalyzer.utils import _set_log_level, logger
88
from codeanalyzer.config import OutputFormat
9+
from codeanalyzer.schema import model_dump_json
910

1011
def main(
1112
input: Annotated[
@@ -102,7 +103,7 @@ def main(
102103
# Handle output based on format
103104
if output is None:
104105
# Output to stdout (only for JSON)
105-
print(artifacts.json(separators=(",", ":")))
106+
print(model_dump_json(artifacts, separators=(",", ":")))
106107
else:
107108
# Output to file
108109
output.mkdir(parents=True, exist_ok=True)
@@ -113,8 +114,8 @@ def _write_output(artifacts, output_dir: Path, format: OutputFormat):
113114
"""Write artifacts to file in the specified format."""
114115
if format == OutputFormat.JSON:
115116
output_file = output_dir / "analysis.json"
116-
# Use Pydantic's json() with separators for compact output
117-
json_str = artifacts.json(indent=None)
117+
# Use Pydantic's model_dump_json() for compact output
118+
json_str = model_dump_json(artifacts, indent=None)
118119
with output_file.open("w") as f:
119120
f.write(json_str)
120121
logger.info(f"Analysis saved to {output_file}")

codeanalyzer/core.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import ray
1010
from codeanalyzer.utils import logger
11-
from codeanalyzer.schema import PyApplication, PyModule
11+
from codeanalyzer.schema import PyApplication, PyModule, model_dump_json, model_validate_json
1212
from codeanalyzer.semantic_analysis.codeql import CodeQLLoader
1313
from codeanalyzer.semantic_analysis.codeql.codeql_exceptions import CodeQLExceptions
1414
from codeanalyzer.syntactic_analysis.exceptions import SymbolTableBuilderRayError
@@ -408,7 +408,7 @@ def _load_pyapplication_from_cache(self, cache_file: Path) -> PyApplication:
408408
"""
409409
with cache_file.open('r') as f:
410410
data = f.read()
411-
return PyApplication.parse_raw(data)
411+
return model_validate_json(PyApplication, data)
412412

413413
def _save_analysis_cache(self, app: PyApplication, cache_file: Path) -> None:
414414
"""Save analysis to cache file.
@@ -421,8 +421,8 @@ def _save_analysis_cache(self, app: PyApplication, cache_file: Path) -> None:
421421
cache_file.parent.mkdir(parents=True, exist_ok=True)
422422

423423
with cache_file.open('w') as f:
424-
f.write(app.json(indent=2))
425-
424+
f.write(model_dump_json(app, indent=2))
425+
426426
logger.info(f"Analysis cached to {cache_file}")
427427

428428
def _file_unchanged(self, file_path: Path, cached_module: PyModule) -> bool:

codeanalyzer/schema/__init__.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from importlib.metadata import version, PackageNotFoundError
2+
from packaging.version import parse as parse_version
3+
14
from .py_schema import (
25
PyApplication,
36
PyCallable,
@@ -22,12 +25,48 @@
2225
"PyCallableParameter",
2326
]
2427

25-
# Resolve forward references
26-
PyCallable.update_forward_refs(PyClass=PyClass)
27-
PyClass.update_forward_refs(PyCallable=PyCallable)
28-
PyModule.update_forward_refs(PyCallable=PyCallable, PyClass=PyClass)
29-
PyApplication.update_forward_refs(
30-
PyCallable=PyCallable,
31-
PyClass=PyClass,
32-
PyModule=PyModule
33-
)
28+
try:
29+
pydantic_version = version("pydantic")
30+
except PackageNotFoundError:
31+
pydantic_version = "0.0.0" # fallback or raise if appropriate
32+
33+
PYDANTIC_V2 = parse_version(pydantic_version) >= parse_version("2.0.0")
34+
35+
if not PYDANTIC_V2:
36+
# Safe to pass localns
37+
PyCallable.update_forward_refs(PyClass=PyClass)
38+
PyClass.update_forward_refs(PyCallable=PyCallable)
39+
PyModule.update_forward_refs(PyCallable=PyCallable, PyClass=PyClass)
40+
PyApplication.update_forward_refs(
41+
PyCallable=PyCallable,
42+
PyClass=PyClass,
43+
PyModule=PyModule
44+
)
45+
46+
# Compatibility helpers for Pydantic v1/v2
47+
def model_dump_json(model, **kwargs):
48+
"""Compatibility helper for JSON serialization."""
49+
if PYDANTIC_V2:
50+
return model.model_dump_json(**kwargs)
51+
else:
52+
# Map Pydantic v2 parameters to v1 equivalents
53+
v1_kwargs = {}
54+
if 'indent' in kwargs:
55+
v1_kwargs['indent'] = kwargs['indent']
56+
if 'separators' in kwargs:
57+
# In v1, separators is passed to dumps_kwargs
58+
v1_kwargs['separators'] = kwargs['separators']
59+
return model.json(**v1_kwargs)
60+
61+
def model_validate_json(model_class, json_data):
62+
"""Compatibility helper for JSON deserialization."""
63+
if PYDANTIC_V2:
64+
return model_class.model_validate_json(json_data)
65+
else:
66+
return model_class.parse_raw(json_data)
67+
68+
__all__.extend([
69+
"PYDANTIC_V2",
70+
"model_dump_json",
71+
"model_validate_json"
72+
])

pyproject.toml

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,60 @@
11
[project]
22
name = "codeanalyzer-python"
3-
version = "0.1.10"
3+
version = "0.1.11"
44
description = "Static Analysis on Python source code using Jedi, CodeQL and Treesitter."
55
readme = "README.md"
66
authors = [
7-
{ name = "Rahul Krishna", email = "[email protected]" }
7+
{ name = "Rahul Krishna", email = "[email protected]" }
88
]
99
requires-python = ">=3.9"
1010

1111
dependencies = [
12-
"jedi>=0.18.0,<0.20.0",
13-
"msgpack>=1.0.0,<1.0.7",
14-
"networkx>=2.6.0,<3.2.0",
15-
"pandas>=1.3.0,<2.0.0",
16-
"numpy>=1.21.0,<1.24.0",
17-
"pydantic>=1.8.0,<2.0.0",
18-
"requests>=2.20.0,<3.0.0",
19-
"rich>=12.6.0,<14.0.0",
20-
"typer>=0.9.0,<1.0.0",
21-
"ray>=2.0.0,<3.0.0",
22-
"typing-extensions>=4.0.0"
12+
# jedi
13+
"jedi>=0.18.0,<0.20.0; python_version < '3.11'",
14+
"jedi<=0.19.2; python_version >= '3.11'",
15+
# msgpack
16+
"msgpack>=1.0.0,<1.0.7; python_version < '3.11'",
17+
"msgpack>=1.0.7,<2.0.0; python_version >= '3.11'",
18+
# networkx
19+
"networkx>=2.6.0,<3.2.0; python_version < '3.11'",
20+
"networkx>=3.0.0,<4.0.0; python_version >= '3.11'",
21+
# pandas
22+
"pandas>=1.3.0,<2.0.0; python_version < '3.11'",
23+
"pandas>=2.0.0,<3.0.0; python_version >= '3.11'",
24+
# numpy
25+
"numpy>=1.21.0,<1.24.0; python_version < '3.11'",
26+
"numpy>=1.24.0,<2.0.0; python_version >= '3.11' and python_version < '3.12'",
27+
"numpy>=1.26.0,<2.0.0; python_version >= '3.12'",
28+
# pydantic
29+
"pydantic>=1.8.0,<2.0.0; python_version < '3.11'",
30+
"pydantic>=2.0.0,<3.0.0; python_version >= '3.11'",
31+
# requests
32+
"requests>=2.20.0,<3.0.0; python_version >= '3.11'",
33+
# rich
34+
"rich>=12.6.0,<14.0.0; python_version < '3.11'",
35+
"rich>=14.0.0,<15.0.0; python_version >= '3.11'",
36+
# typer
37+
"typer>=0.9.0,<1.0.0; python_version < '3.11'",
38+
"typer>=0.9.0,<2.0.0; python_version >= '3.11'",
39+
# typing-extensions
40+
"typing-extensions>=4.0.0,<5.0.0; python_version < '3.11'",
41+
"typing-extensions>=4.5.0,<6.0.0; python_version >= '3.11'",
42+
# ray
43+
"ray==2.0.0; python_version < '3.11'",
44+
"ray>=2.10.0,<3.0.0; python_version >= '3.11'",
45+
"packaging>=25.0",
2346
]
2447

2548
[dependency-groups]
2649
test = [
27-
"pytest>=7.0.0,<8.0.0",
28-
"pytest-asyncio>=0.14.0,<0.15.0",
29-
"pytest-cov>=2.10.0,<3.0.0",
30-
"pytest-pspec>=0.0.3"
50+
"pytest>=7.0.0,<8.0.0",
51+
"pytest-asyncio>=0.14.0,<0.15.0",
52+
"pytest-cov>=2.10.0,<3.0.0",
53+
"pytest-pspec>=0.0.3"
3154
]
3255
dev = [
33-
"ipdb>=0.13.0,<0.14.0",
34-
"pre-commit>=2.9.0,<3.0.0"
56+
"ipdb>=0.13.0,<0.14.0",
57+
"pre-commit>=2.9.0,<3.0.0"
3558
]
3659

3760
[project.scripts]
@@ -47,45 +70,45 @@ include = ["codeanalyzer/py.typed"]
4770

4871
[tool.hatch.build.targets.sdist]
4972
include = [
50-
"codeanalyzer",
51-
"codeanalyzer/py.typed",
52-
"README.md",
53-
"LICENSE",
54-
"NOTICE"
73+
"codeanalyzer",
74+
"codeanalyzer/py.typed",
75+
"README.md",
76+
"LICENSE",
77+
"NOTICE"
5578
]
5679

5780
[tool.pytest.ini_options]
5881
addopts = [
59-
"-p", "coverage",
60-
"--cov=codeanalyzer",
61-
"--cov-report=html",
62-
"--cov-report=term-missing",
63-
"--cov-fail-under=40",
64-
"--ignore=test/fixtures"
82+
"-p", "coverage",
83+
"--cov=codeanalyzer",
84+
"--cov-report=html",
85+
"--cov-report=term-missing",
86+
"--cov-fail-under=40",
87+
"--ignore=test/fixtures"
6588
]
6689
testpaths = ["test"]
6790

6891
[tool.coverage.run]
6992
source = ["codeanalyzer"]
7093
branch = true
7194
omit = [
72-
"*/tests/*",
73-
"*/test_*",
74-
"*/__pycache__/*",
75-
"*/venv/*",
76-
"*/.venv/*",
77-
"codeanalyzer/semantic_analysis/*"
95+
"*/tests/*",
96+
"*/test_*",
97+
"*/__pycache__/*",
98+
"*/venv/*",
99+
"*/.venv/*",
100+
"codeanalyzer/semantic_analysis/*"
78101
]
79102

80103
[tool.coverage.report]
81104
precision = 2
82105
show_missing = true
83106
exclude_lines = [
84-
"pragma: no cover",
85-
"def __repr__",
86-
"raise AssertionError",
87-
"raise NotImplementedError",
88-
"if __name__ == .__main__.:"
107+
"pragma: no cover",
108+
"def __repr__",
109+
"raise AssertionError",
110+
"raise NotImplementedError",
111+
"if __name__ == .__main__.:"
89112
]
90113

91114
[tool.coverage.html]

test_numpy_constraints.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python3
2+
"""Test script to verify NumPy dependency constraints work correctly."""
3+
4+
import sys
5+
from packaging.version import parse as parse_version
6+
7+
def test_numpy_constraints():
8+
"""Test that NumPy constraints are correct for different Python versions."""
9+
python_version = parse_version(f"{sys.version_info.major}.{sys.version_info.minor}")
10+
print(f"Testing on Python {python_version}")
11+
12+
try:
13+
import numpy
14+
numpy_version = parse_version(numpy.__version__)
15+
print(f"NumPy version: {numpy_version}")
16+
17+
# Test constraints based on Python version
18+
if python_version < parse_version("3.11"):
19+
if not (parse_version("1.21.0") <= numpy_version < parse_version("1.24.0")):
20+
print(f"ERROR: NumPy {numpy_version} not in expected range 1.21.0-1.24.0 for Python < 3.11")
21+
return False
22+
elif python_version >= parse_version("3.11") and python_version < parse_version("3.12"):
23+
if not (parse_version("1.24.0") <= numpy_version < parse_version("2.0.0")):
24+
print(f"ERROR: NumPy {numpy_version} not in expected range 1.24.0-2.0.0 for Python 3.11.x")
25+
return False
26+
elif python_version >= parse_version("3.12"):
27+
if not (parse_version("1.26.0") <= numpy_version < parse_version("2.0.0")):
28+
print(f"ERROR: NumPy {numpy_version} not in expected range 1.26.0-2.0.0 for Python 3.12+")
29+
return False
30+
31+
print("✅ NumPy constraints are satisfied")
32+
return True
33+
34+
except ImportError as e:
35+
print(f"ERROR: Failed to import NumPy: {e}")
36+
return False
37+
38+
if __name__ == "__main__":
39+
success = test_numpy_constraints()
40+
sys.exit(0 if success else 1)

0 commit comments

Comments
 (0)