Skip to content

Commit 56aac4e

Browse files
committed
fix(schema): pydantic v1 compatibility for the 3.10 dependency line
The release compat job (Python 3.10, where ray==2.0.0 pins pydantic v1) caught two v2-only idioms that landed after v1.1.1 — the tag-gated job is the first thing that exercises that half of the matrix: - BodyNode.arguments forward-references PyCallArgument (#120); v1 resolves string annotations only when told to, so the module now calls update_forward_refs on v1 (v2 rebuilds automatically and its shim rejects localns, hence the guard). - merge_edges copied edges with model_copy(); routed through a new model_copy() compat helper alongside the existing model_dump_json one, and the CLI's raw artifacts.model_dump(mode="json") now goes through a model_dump() helper that maps mode=json onto a v1 json-round-trip. Verified on Python 3.10 + pydantic 1.10.26 locally: all 60 tests from the failing families pass; the only local residue is ray 2.0.0's x86_64 grpc wheel refusing Apple Silicon in subprocess tests, which does not affect the ubuntu runners. Python 3.12 + pydantic v2: 291 passed solo (a concurrent-run '2 failed' reproduced as machine-load flakiness, not code).
1 parent 341fea1 commit 56aac4e

4 files changed

Lines changed: 33 additions & 4 deletions

File tree

codeanalyzer/__main__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def _pin_hash_seed() -> None:
3838

3939
from codeanalyzer.core import Codeanalyzer
4040
from codeanalyzer.utils import _set_log_level, logger
41-
from codeanalyzer.schema import model_dump_json, strip_internal_only
41+
from codeanalyzer.schema import model_dump, model_dump_json, strip_internal_only
4242
from codeanalyzer.options import AnalysisOptions, EmitTarget
4343

4444

@@ -368,7 +368,7 @@ def main(
368368
print(
369369
json.dumps(
370370
strip_internal_only(
371-
artifacts.model_dump(mode="json", exclude_none=True)
371+
model_dump(artifacts, mode="json", exclude_none=True)
372372
)
373373
)
374374
)
@@ -384,7 +384,7 @@ def _write_output(artifacts, output_dir: Path):
384384
# Strip internal-only fields here rather than with a field-level Pydantic
385385
# `exclude`: the analysis cache shares the serializer and must keep them.
386386
json_str = json.dumps(
387-
strip_internal_only(artifacts.model_dump(mode="json", exclude_none=True))
387+
strip_internal_only(model_dump(artifacts, mode="json", exclude_none=True))
388388
)
389389
with output_file.open("w") as f:
390390
f.write(json_str)

codeanalyzer/schema/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,27 @@ def model_dump_json(model, **kwargs):
104104
v1_kwargs['separators'] = kwargs['separators']
105105
return model.json(**v1_kwargs)
106106

107+
def model_dump(model, **kwargs):
108+
"""Compatibility helper for dict serialization (v2 model_dump / v1 dict).
109+
110+
``mode="json"`` (v2) maps to a json round-trip on v1 so both versions
111+
yield JSON-safe primitives.
112+
"""
113+
if PYDANTIC_V2:
114+
return model.model_dump(**kwargs)
115+
import json as _json
116+
mode = kwargs.pop("mode", None)
117+
v1_kwargs = {k: v for k, v in kwargs.items() if k in ("exclude_none", "exclude")}
118+
if mode == "json":
119+
return _json.loads(model.json(**v1_kwargs))
120+
return model.dict(**v1_kwargs)
121+
122+
123+
def model_copy(model):
124+
"""Compatibility helper for copying a model (v2 model_copy / v1 copy)."""
125+
return model.model_copy() if PYDANTIC_V2 else model.copy()
126+
127+
107128
def model_validate_json(model_class, json_data):
108129
"""Compatibility helper for JSON deserialization."""
109130
if PYDANTIC_V2:

codeanalyzer/schema/py_schema.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,13 @@ class PyCallArgument(BaseModel):
308308
inferred_type: Optional[str] = None
309309

310310

311+
# BodyNode.arguments forward-references PyCallArgument (defined later);
312+
# pydantic v1 resolves string annotations only when told to, while v2
313+
# rebuilds automatically (and its update_forward_refs shim rejects localns).
314+
if not hasattr(BodyNode, "model_rebuild"): # pydantic v1
315+
BodyNode.update_forward_refs(PyCallArgument=PyCallArgument)
316+
317+
311318
@builder
312319
class PyCallsite(BaseModel):
313320
"""Represents a Python call site (function or method invocation) with contextual metadata."""

codeanalyzer/semantic_analysis/call_graph.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import networkx as nx
3030

3131
from codeanalyzer.semantic_analysis.defuse_linker import _module_qual
32+
from codeanalyzer.schema import model_copy
3233
from codeanalyzer.schema.py_schema import (
3334
PyApplication,
3435
PyCallable,
@@ -295,5 +296,5 @@ def merge_edges(*edge_lists: list) -> list:
295296
cur.weight += e.weight
296297
cur.prov = sorted(set(cur.prov) | set(e.prov))
297298
else:
298-
by_key[k] = e.model_copy()
299+
by_key[k] = model_copy(e)
299300
return list(by_key.values())

0 commit comments

Comments
 (0)