Skip to content

Commit 26bd4f4

Browse files
authored
fix(python-driver): add null-guards in ANTLR parser and relax runtime version pin (#2372)
Fix two related issues in the Python driver's ANTLR4 parsing pipeline: - Add null-guards in ResultVisitor methods (visitAgValue, visitFloatLiteral, visitPair, visitObj, handleAnnotatedValue) to prevent AttributeError crashes when the ANTLR4 parse tree contains None child nodes. This occurs with vertices that have complex properties (large arrays, special characters, deeply nested structures). - Relax antlr4-python3-runtime version constraint from ==4.11.1 to >=4.11.1,<5.0 in both pyproject.toml and requirements.txt. The 4.11.1 pin is incompatible with Python >= 3.13. The ANTLR ATN serialized format is unchanged between 4.11 and 4.13, so the generated lexer/parser files are compatible. Validated with antlr4-python3-runtime==4.13.2 on Python 3.11-3.14. - Also replaces shadowing of builtin 'dict' in handleAnnotatedValue with 'd', and uses .get() for safer key access on parsed vertex/edge dicts. - Add tests for malformed/truncated agtype input handling. Verify that malformed and truncated agtype strings raise AGTypeError (or recover gracefully) rather than crashing with AttributeError. This tests the null-guards added to the ANTLR parser visitor. - visitFloatLiteral: raise AGTypeError on malformed child node instead of silently returning a fallback value - visitObj: add comment documenting that visitPair's validation makes the None-guard defensive-only - handleAnnotatedValue: add comment explaining partial-construction behavior on type-check failure - pyproject.toml: add comment explaining ANTLR4 version range rationale - Tests: assert AGTypeError (or graceful recovery) for malformed and truncated inputs, not just absence of AttributeError - handleAnnotatedValue: default properties to {} when missing from parsed dict, preventing __getitem__ crashes on access - Tests: replace weak assertNotIsInstance with structural type checks - Fix truncated test docstring to match actual assertion behavior - Use PostgreSQL "$user" placeholder in SET search_path. - Exercise real escapes and Unicode in special-characters vertex test (json.dumps). - Add Python 3.9 trove classifier to match requires-python and dependency comment. - Build vertex agtype with string concat to avoid invalid f-string braces. - Assert stored description matches parser behavior: JSON escapes remain literal, UTF-8 decodes normally (ensure_ascii=False on json.dumps). - Regenerate parser with ANTLR 4.13.2 to silence runtime-version-mismatch warning. The generated lexer/parser were hardcoded to check for ANTLR runtime 4.11.1, which triggered a noisy 'ANTLR runtime and generated code versions disagree' warning when installed against a newer runtime like 4.13.2. Regenerating from Agtype.g4 with the 4.13.2 tool aligns the generated checkVersion() call with the default-installed runtime in the allowed dependency range and eliminates the warning. - Bumps the declared floor of antlr4-python3-runtime to 4.13.2 so the default install path is warning-free. Made-with: Cursor
1 parent 908d7b2 commit 26bd4f4

8 files changed

Lines changed: 245 additions & 39 deletions

File tree

drivers/python/age/builder.py

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,16 @@ def visitAgValue(self, ctx:AgtypeParser.AgValueContext):
9292

9393
if annoCtx is not None:
9494
annoCtx.accept(self)
95-
anno = annoCtx.IDENT().getText()
95+
identNode = annoCtx.IDENT()
96+
if identNode is None:
97+
raise AGTypeError(ctx.getText(), "Missing type annotation identifier")
98+
anno = identNode.getText()
99+
if valueCtx is None:
100+
raise AGTypeError(ctx.getText(), "Missing value for annotated type")
96101
return self.handleAnnotatedValue(anno, valueCtx)
97102
else:
103+
if valueCtx is None:
104+
return None
98105
return valueCtx.accept(self)
99106

100107

@@ -109,9 +116,14 @@ def visitIntegerValue(self, ctx:AgtypeParser.IntegerValueContext):
109116

110117
# Visit a parse tree produced by AgtypeParser#floatLiteral.
111118
def visitFloatLiteral(self, ctx:AgtypeParser.FloatLiteralContext):
119+
text = ctx.getText()
112120
c = ctx.getChild(0)
121+
if c is None or not hasattr(c, 'symbol') or c.symbol is None:
122+
raise AGTypeError(
123+
str(text),
124+
"Malformed float literal: missing or invalid child node"
125+
)
113126
tp = c.symbol.type
114-
text = ctx.getText()
115127
if tp == AgtypeParser.RegularFloat:
116128
return float(text)
117129
elif tp == AgtypeParser.ExponentFloat:
@@ -150,15 +162,27 @@ def visitObj(self, ctx:AgtypeParser.ObjContext):
150162
namVal = self.visitPair(c)
151163
name = namVal[0]
152164
valCtx = namVal[1]
153-
val = valCtx.accept(self)
154-
obj[name] = val
165+
# visitPair() raises AGTypeError when the value node is
166+
# missing, so valCtx should never be None here. The
167+
# guard is kept as a defensive fallback only.
168+
if valCtx is not None:
169+
val = valCtx.accept(self)
170+
obj[name] = val
171+
else:
172+
obj[name] = None
155173
return obj
156174

157175

158176
# Visit a parse tree produced by AgtypeParser#pair.
159177
def visitPair(self, ctx:AgtypeParser.PairContext):
160178
self.visitChildren(ctx)
161-
return (ctx.STRING().getText().strip('"') , ctx.agValue())
179+
strNode = ctx.STRING()
180+
agValNode = ctx.agValue()
181+
if strNode is None:
182+
raise AGTypeError(ctx.getText(), "Missing key in object pair")
183+
if agValNode is None:
184+
raise AGTypeError(ctx.getText(), "Missing value in object pair")
185+
return (strNode.getText().strip('"') , agValNode)
162186

163187

164188
# Visit a parse tree produced by AgtypeParser#array.
@@ -171,38 +195,49 @@ def visitArray(self, ctx:AgtypeParser.ArrayContext):
171195
return li
172196

173197
def handleAnnotatedValue(self, anno:str, ctx:ParserRuleContext):
198+
# Each branch below constructs a model object (Vertex, Edge, Path)
199+
# and populates it from the parsed dict/list. If a type check
200+
# fails (e.g. the parsed value is not a dict), AGTypeError is
201+
# raised and the partially-constructed object is discarded — no
202+
# cleanup is needed because the caller propagates the exception.
174203
if anno == "numeric":
175204
return Decimal(ctx.getText())
176205
elif anno == "vertex":
177-
dict = ctx.accept(self)
178-
vid = dict["id"]
206+
d = ctx.accept(self)
207+
if not isinstance(d, dict):
208+
raise AGTypeError(str(ctx.getText()), "Expected dict for vertex, got " + type(d).__name__)
209+
vid = d.get("id")
179210
vertex = None
180-
if self.vertexCache != None and vid in self.vertexCache :
211+
if self.vertexCache is not None and vid in self.vertexCache:
181212
vertex = self.vertexCache[vid]
182213
else:
183214
vertex = Vertex()
184-
vertex.id = dict["id"]
185-
vertex.label = dict["label"]
186-
vertex.properties = dict["properties"]
215+
vertex.id = d.get("id")
216+
vertex.label = d.get("label")
217+
vertex.properties = d.get("properties") or {}
187218

188-
if self.vertexCache != None:
219+
if self.vertexCache is not None:
189220
self.vertexCache[vid] = vertex
190221

191222
return vertex
192223

193224
elif anno == "edge":
194225
edge = Edge()
195-
dict = ctx.accept(self)
196-
edge.id = dict["id"]
197-
edge.label = dict["label"]
198-
edge.end_id = dict["end_id"]
199-
edge.start_id = dict["start_id"]
200-
edge.properties = dict["properties"]
226+
d = ctx.accept(self)
227+
if not isinstance(d, dict):
228+
raise AGTypeError(str(ctx.getText()), "Expected dict for edge, got " + type(d).__name__)
229+
edge.id = d.get("id")
230+
edge.label = d.get("label")
231+
edge.end_id = d.get("end_id")
232+
edge.start_id = d.get("start_id")
233+
edge.properties = d.get("properties") or {}
201234

202235
return edge
203236

204237
elif anno == "path":
205238
arr = ctx.accept(self)
239+
if not isinstance(arr, list):
240+
raise AGTypeError(str(ctx.getText()), "Expected list for path, got " + type(arr).__name__)
206241
path = Path(arr)
207242

208243
return path

drivers/python/age/gen/AgtypeLexer.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
# KIND, either express or implied. See the License for the
1313
# specific language governing permissions and limitations
1414
# under the License.
15-
# Generated from ../Agtype.g4 by ANTLR 4.11.1
16-
15+
# Generated from ../Agtype.g4 by ANTLR 4.13.2
1716
from antlr4 import *
1817
from io import StringIO
1918
import sys
@@ -142,9 +141,7 @@ class AgtypeLexer(Lexer):
142141

143142
def __init__(self, input=None, output:TextIO = sys.stdout):
144143
super().__init__(input, output)
145-
self.checkVersion("4.11.1")
144+
self.checkVersion("4.13.2")
146145
self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache())
147146
self._actions = None
148147
self._predicates = None
149-
150-

drivers/python/age/gen/AgtypeListener.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
# KIND, either express or implied. See the License for the
1313
# specific language governing permissions and limitations
1414
# under the License.
15-
# Generated from ../Agtype.g4 by ANTLR 4.11.1
16-
15+
# Generated from ../Agtype.g4 by ANTLR 4.13.2
1716
from antlr4 import *
18-
if __name__ is not None and "." in __name__:
17+
if "." in __name__:
1918
from .AgtypeParser import AgtypeParser
2019
else:
2120
from AgtypeParser import AgtypeParser
@@ -159,4 +158,4 @@ def exitFloatLiteral(self, ctx:AgtypeParser.FloatLiteralContext):
159158

160159

161160

162-
del AgtypeParser
161+
del AgtypeParser

drivers/python/age/gen/AgtypeParser.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@
1212
# KIND, either express or implied. See the License for the
1313
# specific language governing permissions and limitations
1414
# under the License.
15-
# Generated from ../Agtype.g4 by ANTLR 4.11.1
15+
# Generated from ../Agtype.g4 by ANTLR 4.13.2
1616
# encoding: utf-8
17-
1817
from antlr4 import *
1918
from io import StringIO
2019
import sys
@@ -108,7 +107,7 @@ class AgtypeParser ( Parser ):
108107

109108
def __init__(self, input:TokenStream, output:TextIO = sys.stdout):
110109
super().__init__(input, output)
111-
self.checkVersion("4.11.1")
110+
self.checkVersion("4.13.2")
112111
self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache)
113112
self._predicates = None
114113

@@ -854,5 +853,3 @@ def floatLiteral(self):
854853
finally:
855854
self.exitRule()
856855
return localctx
857-
858-

drivers/python/age/gen/AgtypeVisitor.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
# KIND, either express or implied. See the License for the
1313
# specific language governing permissions and limitations
1414
# under the License.
15-
# Generated from ../Agtype.g4 by ANTLR 4.11.1
16-
15+
# Generated from ../Agtype.g4 by ANTLR 4.13.2
1716
from antlr4 import *
18-
if __name__ is not None and "." in __name__:
17+
if "." in __name__:
1918
from .AgtypeParser import AgtypeParser
2019
else:
2120
from AgtypeParser import AgtypeParser
@@ -100,4 +99,4 @@ def visitFloatLiteral(self, ctx:AgtypeParser.FloatLiteralContext):
10099

101100

102101

103-
del AgtypeParser
102+
del AgtypeParser

drivers/python/pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ authors = [
2929
{name = "Ikchan Kwon, Apache AGE", email = "dev-subscribe@age.apache.org"}
3030
]
3131
classifiers = [
32+
"Programming Language :: Python :: 3.9",
3233
"Programming Language :: Python :: 3.10",
3334
"Programming Language :: Python :: 3.11",
3435
"Programming Language :: Python :: 3.12",
@@ -37,7 +38,11 @@ classifiers = [
3738
]
3839
dependencies = [
3940
"psycopg",
40-
"antlr4-python3-runtime==4.11.1",
41+
# Parser is generated with ANTLR 4.13.2. Runtime is forward- and
42+
# backward-compatible within the 4.x series (checkVersion() emits a
43+
# warning on mismatch but parsing still works); tested on 4.11.1–4.13.2
44+
# with Python 3.9–3.14.
45+
"antlr4-python3-runtime>=4.13.2,<5.0",
4146
]
4247

4348
[project.urls]

drivers/python/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
psycopg
2-
antlr4-python3-runtime==4.11.1
2+
antlr4-python3-runtime>=4.11.1,<5.0
33
setuptools
44
networkx

0 commit comments

Comments
 (0)