From e397acb438ba2e74ff7db7900577d53a798bc7ad Mon Sep 17 00:00:00 2001 From: Bastian Germann Date: Tue, 24 Feb 2026 23:40:24 +0100 Subject: [PATCH] Support Python 3.14 AST: handle ast.Constant and use ast_utils helpers --- compyle/ast_utils.py | 24 +++++++++++++++++++++++ compyle/cython_generator.py | 9 +++++---- compyle/jit.py | 19 ++++++++++++++---- compyle/template.py | 11 +++++++++-- compyle/translator.py | 39 +++++++++++++++++++++++++++++-------- 5 files changed, 84 insertions(+), 18 deletions(-) diff --git a/compyle/ast_utils.py b/compyle/ast_utils.py index e622142..b27aad0 100644 --- a/compyle/ast_utils.py +++ b/compyle/ast_utils.py @@ -174,3 +174,27 @@ def has_return(code): """Returns True of the node has a return statement. """ return has_node(code, ast.Return) + + +def is_str_node(node): + """Return True if the AST node represents a string literal. + + Works on both older ASTs (ast.Str) and newer ones (ast.Constant). + """ + if isinstance(node, ast.Constant): + return isinstance(node.value, str) + Str = getattr(ast, 'Str', None) + return Str is not None and isinstance(node, Str) + + +def get_str_value(node): + """Return the string value for a string AST node. + + Returns None if the node is not a string node. + """ + if isinstance(node, ast.Constant): + return node.value if isinstance(node.value, str) else None + Str = getattr(ast, 'Str', None) + if Str is not None and isinstance(node, Str): + return node.s + return None diff --git a/compyle/cython_generator.py b/compyle/cython_generator.py index a0e7e51..d84c23c 100644 --- a/compyle/cython_generator.py +++ b/compyle/cython_generator.py @@ -21,7 +21,7 @@ from .types import KnownType, Undefined, get_declare_info from .config import get_config -from .ast_utils import get_assigned, has_return +from .ast_utils import get_assigned, has_return, get_str_value, is_str_node from .utils import getsourcelines logger = logging.getLogger(__name__) @@ -247,11 +247,12 @@ def parse_declare(code): if call.func.id != 'declare': raise CodeGenerationError('Unknown declare statement: %s' % code) arg0 = call.args[0] - if not isinstance(arg0, ast.Str): - err = 'Type should be a string, given :%r' % arg0.s + typestr = get_str_value(arg0) + if typestr is None: + err = 'Type should be a string, given :%r' % getattr(arg0, 's', getattr(arg0, 'value', arg0)) raise CodeGenerationError(err) - return get_declare_info(arg0.s) + return get_declare_info(typestr) class CythonGenerator(object): diff --git a/compyle/jit.py b/compyle/jit.py index 080fd42..21292a8 100644 --- a/compyle/jit.py +++ b/compyle/jit.py @@ -15,6 +15,7 @@ from .extern import Extern from .utils import getsourcelines from .profile import profile +from .ast_utils import get_str_value from . import array from . import parallel @@ -198,15 +199,18 @@ def warn(self, message, node): warnings.warn(msg) def visit_declare(self, node): - if not isinstance(node.args[0], ast.Str): + arg0 = node.args[0] + type_str = get_str_value(arg0) + if type_str is None: self.error("Argument to declare should be a string.", node) - type_str = node.args[0].s return self.get_declare_type(type_str) def visit_cast(self, node): - if not isinstance(node.args[1], ast.Str): + arg1 = node.args[1] + typestr = get_str_value(arg1) + if typestr is None: self.error("Cast type should be a string.", node) - return node.args[1].s + return typestr def visit_address(self, node): base_type = self.visit(node.args[0]) @@ -294,6 +298,13 @@ def visit_BinOp(self, node): def visit_Num(self, node): return get_ctype_from_arg(node.n) + def visit_Constant(self, node): + val = node.value + if isinstance(val, (int, float)): + return get_ctype_from_arg(val) + # For other constants (e.g., strings/None/bool), we return None + return None + def visit_UnaryOp(self, node): return self.visit(node.operand) diff --git a/compyle/template.py b/compyle/template.py index 55e6a78..d9ff609 100644 --- a/compyle/template.py +++ b/compyle/template.py @@ -4,6 +4,7 @@ from .types import kwtype_to_annotation import mako.template +from .ast_utils import get_str_value getfullargspec = inspect.getfullargspec @@ -45,8 +46,14 @@ def _get_code(self): args += extra_args arg_string = ', '.join(args) body = m.body[0].body - template = body[-1].value.s - docstring = body[0].value.s if len(body) == 2 else '' + # Extract template and docstring in an AST-version-agnostic way + last_val = body[-1].value + template = get_str_value(last_val) or '' + + docstring = '' + if len(body) == 2: + first_val = body[0].value + docstring = get_str_value(first_val) or '' name = self.name sig = 'def {name}({args}):\n """{docs}\n """'.format( name=name, args=arg_string, docs=docstring diff --git a/compyle/translator.py b/compyle/translator.py index 7a10a92..6d46fa9 100644 --- a/compyle/translator.py +++ b/compyle/translator.py @@ -26,6 +26,7 @@ CodeGenerationError, KnownType, Undefined, all_numeric ) from .utils import getsource +from .ast_utils import is_str_node, get_str_value PY_VER = sys.version_info.major @@ -234,11 +235,11 @@ def _indent_block(self, code): return '\n'.join(pad + x for x in lines) def _remove_docstring(self, body): - if body and isinstance(body[0], ast.Expr) and \ - isinstance(body[0].value, ast.Str): - return body[1:] - else: - return body + if body and isinstance(body[0], ast.Expr): + val = body[0].value + if is_str_node(val): + return body[1:] + return body def _get_local_info(self, obj): return None @@ -351,9 +352,11 @@ def visit_Assign(self, node): left, right = node.targets[0], node.value if isinstance(right, ast.Call) and \ isinstance(right.func, ast.Name) and right.func.id == 'declare': - if not isinstance(right.args[0], ast.Str): + arg0 = right.args[0] + s = get_str_value(arg0) + if s is None: self.error("Argument to declare should be a string.", node) - type = right.args[0].s + type = s if isinstance(left, ast.Name): self._known.add(left.id) return self._get_variable_declaration(type, [self.visit(left)]) @@ -395,7 +398,11 @@ def visit_Call(self, node): elif 'atomic' in node.func.id: return self.render_atomic(node.func.id, node.args[0]) elif node.func.id == 'cast': - return '(%s) (%s)' % (node.args[1].s, self.visit(node.args[0])) + arg1 = node.args[1] + typestr = get_str_value(arg1) + if typestr is None: + self.error("Argument to cast should be a string.", node) + return '(%s) (%s)' % (typestr, self.visit(node.args[0])) else: return '{func}({args})'.format( func=node.func.id, @@ -691,6 +698,22 @@ def visit_NotEq(self, node): def visit_Num(self, node): return literal_to_float(node.n, self._use_double) + def visit_Constant(self, node): + val = node.value + # Handle booleans explicitly first + if isinstance(val, bool): + return self._replacements[val] + # Numbers: int/float + if isinstance(val, (int, float)): + return literal_to_float(val, self._use_double) + # Strings + if isinstance(val, str): + return r'"%s"' % val + # None and other constants + if val in self._replacements: + return self._replacements[val] + return repr(val) + def visit_Or(self, node): return '||'