From b44fac42b1c9c0d947407ba4f673fbcec82136ba Mon Sep 17 00:00:00 2001 From: Florian Schulze Date: Thu, 19 Dec 2024 09:15:40 +0100 Subject: [PATCH] Fix ast deprecation warnings up to Python 3.13. --- CHANGES.rst | 3 + docs/library.rst | 2 +- src/chameleon/astutil.py | 2 +- src/chameleon/codegen.py | 51 +++++++++----- src/chameleon/compiler.py | 125 +++++++++++++++++++---------------- src/chameleon/tales.py | 8 +-- src/chameleon/zpt/program.py | 14 ++-- tox.ini | 3 +- 8 files changed, 124 insertions(+), 84 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7ea933b6..44eb2224 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,9 @@ Changes In next release ... +- Fix ``ast`` deprecation warnings up to Python 3.13. + (`#430 `_) + - Fix ``load_module`` deprecation warnings for Python >= 3.10. 4.5.4 (2024-04-08) diff --git a/docs/library.rst b/docs/library.rst index 230f0ce4..4099187a 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -72,7 +72,7 @@ You can write such a compiler as a closure: def uppercase_expression(string): def compiler(target, engine): uppercased = self.string.uppercase() - value = ast.Str(uppercased) + value = ast.Constant(uppercased) return [ast.Assign(targets=[target], value=value)] return compiler diff --git a/src/chameleon/astutil.py b/src/chameleon/astutil.py index 67dfbc1f..8c1a9554 100644 --- a/src/chameleon/astutil.py +++ b/src/chameleon/astutil.py @@ -45,7 +45,7 @@ def subscript( ) -> ast.Subscript: return ast.Subscript( value=value, - slice=ast.Index(value=ast.Str(s=name)), + slice=ast.Constant(name), ctx=ctx, ) diff --git a/src/chameleon/codegen.py b/src/chameleon/codegen.py index da881297..f9e4c958 100644 --- a/src/chameleon/codegen.py +++ b/src/chameleon/codegen.py @@ -2,6 +2,7 @@ import builtins import re +import sys import textwrap import types from ast import AST @@ -14,6 +15,8 @@ from ast import Module from ast import NodeTransformer from ast import alias +from ast import copy_location +from ast import fix_missing_locations from ast import unparse from typing import TYPE_CHECKING from typing import Any @@ -57,18 +60,34 @@ def wrapper(*vargs, **kwargs): symbols.update(kwargs) class Transformer(NodeTransformer): - def visit_FunctionDef(self, node) -> AST: - name = symbols.get(node.name, self) - if name is self: + def visit_FunctionDef(self, node: ast.FunctionDef) -> AST: + if node.name not in symbols: return self.generic_visit(node) - return FunctionDef( - name=name, - args=node.args, - body=list(map(self.visit, node.body)), - decorator_list=getattr(node, "decorator_list", []), - lineno=None, - ) + name = symbols[node.name] + assert isinstance(name, str) + body: list[ast.stmt] = [self.visit(stmt) for stmt in node.body] + if sys.version_info >= (3, 12): + # mypy complains if type_params is missing + funcdef = FunctionDef( + name=name, + args=node.args, + body=body, + decorator_list=node.decorator_list, + returns=node.returns, + type_params=node.type_params, + ) + else: + funcdef = FunctionDef( + name=name, + args=node.args, + body=body, + decorator_list=node.decorator_list, + returns=node.returns, + ) + copy_location(funcdef, node) + fix_missing_locations(funcdef) + return funcdef def visit_Name(self, node: ast.Name) -> AST: value = symbols.get(node.id, self) @@ -170,15 +189,17 @@ def require(self, value: type[Any] | Hashable) -> ast.Name: def visit_Module(self, module: Module) -> AST: assert isinstance(module, Module) module = super().generic_visit(module) # type: ignore[assignment] - preamble: list[AST] = [] + preamble: list[ast.stmt] = [] for name, node in self.defines.items(): - assignment = Assign(targets=[store(name)], value=node, lineno=None) + assignment = Assign(targets=[store(name)], value=node) + copy_location(assignment, node) + fix_missing_locations(assignment) preamble.append(self.visit(assignment)) - imports: list[AST] = [] + imports: list[ast.stmt] = [] for value, node in self.imports.items(): - stmt: AST + stmt: ast.stmt if isinstance(value, types.ModuleType): stmt = Import( @@ -198,7 +219,7 @@ def visit_Module(self, module: Module) -> AST: imports.append(stmt) - return Module(imports + preamble + module.body, ()) + return Module(imports + preamble + module.body, []) def visit_Comment(self, node: Comment) -> AST: self.comments.append(node.text) diff --git a/src/chameleon/compiler.py b/src/chameleon/compiler.py index 608ded1a..ef161557 100644 --- a/src/chameleon/compiler.py +++ b/src/chameleon/compiler.py @@ -71,7 +71,7 @@ def mangle(string: int | str) -> str: def load_econtext(name): - return template("getname(KEY)", KEY=ast.Str(s=name), mode="eval") + return template("getname(KEY)", KEY=ast.Constant(name), mode="eval") def store_econtext(name: object) -> ast.Subscript: @@ -94,9 +94,9 @@ def eval_token(token): return template( "(string, line, col)", - string=ast.Str(s=string), - line=ast.Num(n=line), - col=ast.Num(n=column), + string=ast.Constant(string), + line=ast.Constant(line), + col=ast.Constant(column), mode="eval" ) @@ -339,7 +339,7 @@ def __call__(self, name, engine): m = self.regex.search(matched) if m is None: text = text.replace('$$', '$') - nodes.append(ast.Str(s=text)) + nodes.append(ast.Constant(text)) break part = text[:m.start()] @@ -352,7 +352,7 @@ def __call__(self, name, engine): i += 1 skip = i & 1 part = part.replace('$$', '$') - node = ast.Str(s=part) + node = ast.Constant(part) nodes.append(node) if skip: text = text[1:] @@ -383,7 +383,8 @@ def __call__(self, name, engine): continue else: s = m.group() - assign = ast.Assign(targets=[target], value=ast.Str(s=s)) + assign = ast.Assign( + targets=[target], value=ast.Constant(s)) body += [assign] break @@ -406,7 +407,11 @@ def __call__(self, name, engine): if len(nodes) == 1: target = nodes[0] - if translate and isinstance(target, ast.Str): + if ( + translate + and isinstance(target, ast.Constant) + and isinstance(target.value, str) + ): target = template( "translate(msgid, domain=__i18n_domain, context=__i18n_context, target_language=target_language)", # noqa: E501 line too long msgid=target, @@ -419,25 +424,32 @@ def __call__(self, name, engine): values = [] for node in nodes: - if isinstance(node, ast.Str): - formatting_string += node.s + if ( + isinstance(node, ast.Constant) + and isinstance(node.value, str) + ): + formatting_string += node.value else: string = expr_map[node] formatting_string += "${%s}" % string - keys.append(ast.Str(s=string)) + keys.append(ast.Constant(string)) values.append(node) target = template( "translate(msgid, mapping=mapping, domain=__i18n_domain, context=__i18n_context, target_language=target_language)", # noqa: E501 line too long - msgid=ast.Str( - s=formatting_string), + msgid=ast.Constant( + formatting_string), mapping=ast.Dict( keys=keys, values=values), mode="eval") else: nodes = [ - node if isinstance(node, ast.Str) else + node + if ( + isinstance(node, ast.Constant) + and isinstance(node.value, str) + ) else template( "NODE if NODE is not None else ''", NODE=node, mode="eval" @@ -446,7 +458,7 @@ def __call__(self, name, engine): ] target = ast.BinOp( - left=ast.Str(s="%s" * len(nodes)), + left=ast.Constant("%s" * len(nodes)), op=ast.Mod(), right=ast.Tuple(elts=nodes, ctx=ast.Load())) @@ -567,7 +579,7 @@ def _convert_bool(self, target, char_escape, s): """ return emit_bool( - target, ast.Str(s=s), + target, ast.Constant(s), default=self._default, default_marker=self._default_marker ) @@ -608,8 +620,8 @@ def _convert_text(self, target, char_escape): return template( "TARGET = __quote(TARGET, QUOTE, Q_ENTITY, DEFAULT, MARKER)", TARGET=target, - QUOTE=ast.Str(s=quote), - Q_ENTITY=ast.Str(s=entity), + QUOTE=ast.Constant(quote), + Q_ENTITY=ast.Constant(entity), DEFAULT=self._default, MARKER=self._default_marker, ) @@ -775,7 +787,7 @@ def __call__(self, node): return template( "get(key, name)", mode="eval", - key=ast.Str(s=name), + key=ast.Constant(name), name=Builtin(name), ) @@ -817,7 +829,7 @@ def __call__(self, expression, target): p = pickle.dumps(exc, -1) stmts = template( - "__exc = loads(p)", loads=self.loads_symbol, p=ast.Str(s=p) + "__exc = loads(p)", loads=self.loads_symbol, p=ast.Constant(p) ) stmts += [ @@ -931,12 +943,12 @@ def visit_Replace(self, node, target): return stmts + template( "if TARGET: TARGET = S", TARGET=target, - S=ast.Str(s=node.s) + S=ast.Constant(node.s) ) def visit_Translate(self, node, target): if node.msgid is not None: - msgid = ast.Str(s=node.msgid) + msgid = ast.Constant(node.msgid) else: msgid = target return self._translate(node.node, target) + \ @@ -1027,10 +1039,8 @@ def visit_EmitText(self, node) -> ast.AST: append = load(self.scopes[-1].append or "__append") expr = ast.Expr(ast.Call( func=append, - args=[ast.Str(s=node.s)], + args=[ast.Constant(node.s)], keywords=[], - starargs=None, - kwargs=None )) return self.visit(expr) # type: ignore[no-any-return] @@ -1052,11 +1062,13 @@ def visit_TranslationContext(self, node) -> list[ast.AST]: def visit_TokenRef(self, node: TokenRef) -> ast.AST: self.tokens.append((node.token.pos, len(node.token))) - return ast.Assign( + assignment = ast.Assign( [store("__token")], - ast.Num(n=node.token.pos), - lineno=None, + ast.Constant(node.token.pos), ) + ast.copy_location(assignment, node) + ast.fix_missing_locations(assignment) + return assignment generator = Generator(module) tokens = [ @@ -1155,7 +1167,7 @@ def visit_MacroProgram(self, node): # Return function dictionary functions += [ast.Return(value=ast.Dict( - keys=[ast.Str(s=name) for name in names], + keys=[ast.Constant(name) for name in names], values=[load(name) for name in names], ))] @@ -1182,7 +1194,7 @@ def visit_Macro(self, node): for name in self.defaults: body += template( "NAME = econtext[KEY]", - NAME=name, KEY=ast.Str(s="__" + name) + NAME=name, KEY=ast.Constant("__" + name) ) # Internal set of defined slots @@ -1196,7 +1208,7 @@ def visit_Macro(self, node): body += template( "try: NAME = econtext[KEY].pop()\n" "except: NAME = None", - KEY=ast.Str(s=name), NAME=store(name)) + KEY=ast.Constant(name), NAME=store(name)) exc = template( "exc_info()[1]", exc_info=Symbol(sys.exc_info), mode="eval" @@ -1252,7 +1264,7 @@ def visit_Text(self, node): def visit_Domain(self, node): backup = "__previous_i18n_domain_%s" % mangle(id(node)) return template("BACKUP = __i18n_domain", BACKUP=backup) + \ - template("__i18n_domain = NAME", NAME=ast.Str(s=node.name)) + \ + template("__i18n_domain = NAME", NAME=ast.Constant(node.name)) + \ self.visit(node.node) + \ template("__i18n_domain = BACKUP", BACKUP=backup) @@ -1268,7 +1280,7 @@ def visit_Target(self, node): def visit_TxContext(self, node): backup = "__previous_i18n_context_%s" % mangle(id(node)) return template("BACKUP = __i18n_context", BACKUP=backup) + \ - template("__i18n_context = NAME", NAME=ast.Str(s=node.name)) + \ + template("__i18n_context = NAME", NAME=ast.Constant(node.name)) + \ self.visit(node.node) + \ template("__i18n_context = BACKUP", BACKUP=backup) @@ -1287,7 +1299,7 @@ def visit_OnError(self, node): "if handler is not None: handler(__exc)", cls=ErrorInfo, handler=load("on_error_handler"), - key=ast.Str(s=node.name), + key=ast.Constant(node.name), ) body += [ast.Try( @@ -1364,8 +1376,8 @@ def visit_Assignment(self, node): for name in node.names: if not node.local: assignment += template( - "rcontext[KEY] = __value", KEY=ast.Str( - s=str(name))) + "rcontext[KEY] = __value", KEY=ast.Constant( + str(name))) return assignment @@ -1475,14 +1487,14 @@ def visit_Translate(self, node): for name in names: stream, append = self._get_translation_identifiers(name) - keys.append(ast.Str(s=name)) + keys.append(ast.Constant(name)) values.append(load(stream)) # Initialize value body.insert( 0, ast.Assign( targets=[store(stream)], - value=ast.Str(s=""))) + value=ast.Constant(""))) mapping = ast.Dict(keys=keys, values=values) else: @@ -1490,7 +1502,7 @@ def visit_Translate(self, node): # if this translation node has a name, use it as the message id if node.msgid: - msgid = ast.Str(s=node.msgid) + msgid = ast.Constant(node.msgid) # emit the translation expression translation = template( @@ -1542,23 +1554,24 @@ def visit_Attribute(self, node): filter_condition = template( "NAME not in CHAIN", - NAME=ast.Str(s=node.name), + NAME=ast.Constant(node.name), CHAIN=ast.Call( func=load("__chain"), args=filter_args, keywords=[], - starargs=None, - kwargs=None, ), mode="eval" ) # Static attributes are just outputted directly - if isinstance(node.expression, ast.Str): - s = attr_format % node.expression.s + if ( + isinstance(node.expression, ast.Constant) + and isinstance(node.expression.value, str) + ): + s = attr_format % node.expression.value if node.filters: return template( - "if C: __append(S)", C=filter_condition, S=ast.Str(s=s) + "if C: __append(S)", C=filter_condition, S=ast.Constant(s) ) else: return [EmitText(s)] @@ -1576,7 +1589,7 @@ def visit_Attribute(self, node): return body + template( "if CONDITION: __append(FORMAT % TARGET)", - FORMAT=ast.Str(s=attr_format), + FORMAT=ast.Constant(attr_format), TARGET=target, CONDITION=condition, ) @@ -1587,14 +1600,14 @@ def visit_DictAttributes(self, node): bool_names = Static(template( "set(LIST)", LIST=ast.List( - elts=[ast.Str(s=name) for name in node.bool_names], + elts=[ast.Constant(name) for name in node.bool_names], ctx=ast.Load(), ), mode="eval" )) exclude = Static(template( "set(LIST)", LIST=ast.List( - elts=[ast.Str(s=name) for name in node.exclude], + elts=[ast.Constant(name) for name in node.exclude], ctx=ast.Load(), ), mode="eval" )) @@ -1621,8 +1634,8 @@ def visit_DictAttributes(self, node): TARGET=target, EXCLUDE=exclude, QUOTE_FUNC="__quote", - QUOTE=ast.Str(s=node.quote), - QUOTE_ENTITY=ast.Str(s=char2entity(node.quote or '\0')), + QUOTE=ast.Constant(node.quote), + QUOTE_ENTITY=ast.Constant(char2entity(node.quote or '\0')), BOOL_NAMES=bool_names ) @@ -1767,7 +1780,7 @@ def visit_UseExternalMacro(self, node): lineno=None, )) - key = ast.Str(s=key) + key = ast.Constant(key) assignment = template( "_slots = econtext[KEY] = DEQUE((NAME,))", @@ -1828,7 +1841,7 @@ def visit_Repeat(self, node): ] key = ast.Tuple( - elts=[ast.Str(s=name) for name in node.names], + elts=[ast.Constant(name) for name in node.names], ctx=ast.Load()) else: name = node.names[0] @@ -1837,7 +1850,7 @@ def visit_Repeat(self, node): for context in contexts ] - key = ast.Str(s=node.names[0]) + key = ast.Constant(node.names[0]) index = identifier("__index", id(node)) assignment = [ast.Assign(targets=targets, value=load("__item"))] @@ -1873,7 +1886,7 @@ def visit_Repeat(self, node): # For items up to N - 1, emit repeat whitespace inner += template( "if INDEX > 0: __append(WHITESPACE)", - INDEX=index, WHITESPACE=ast.Str(s=node.whitespace) + INDEX=index, WHITESPACE=ast.Constant(node.whitespace) ) # Main repeat loop @@ -1904,7 +1917,7 @@ def _enter_assignment(self, names): yield from template( "BACKUP = get(KEY, __marker)", BACKUP=identifier("backup_%s" % name, id(names)), - KEY=ast.Str(s=str(name)), + KEY=ast.Constant(str(name)), ) def _leave_assignment(self, names): @@ -1913,5 +1926,5 @@ def _leave_assignment(self, names): "if BACKUP is __marker: del econtext[KEY]\n" "else: econtext[KEY] = BACKUP", BACKUP=identifier("backup_%s" % name, id(names)), - KEY=ast.Str(s=str(name)), + KEY=ast.Constant(str(name)), ) diff --git a/src/chameleon/tales.py b/src/chameleon/tales.py index e228a161..0de79d92 100644 --- a/src/chameleon/tales.py +++ b/src/chameleon/tales.py @@ -61,7 +61,7 @@ def transform_attribute(node): "lookup(object, name)", lookup=Symbol(lookup_attr), object=node.value, - name=ast.Str(s=node.attr), + name=ast.Constant(node.attr), mode="eval" ) @@ -266,7 +266,7 @@ def __call__(self, target, engine): value = template( "RESOLVE(NAME)", RESOLVE=Symbol(resolve_dotted), - NAME=ast.Str(s=string), + NAME=ast.Constant(string), mode="eval", ) return [ast.Assign(targets=[target], value=value)] @@ -428,7 +428,7 @@ class StringExpr: ... return [ ... ast.Assign( ... targets=[target], - ... value=ast.Num(n=len(expression)) + ... value=ast.Constant(len(expression)) ... )] ... ... return compiler @@ -473,8 +473,6 @@ def translate_proxy(self, engine, expression, target): func=load(self.name), args=[target], keywords=[], - starargs=None, - kwargs=None )) ] diff --git a/src/chameleon/zpt/program.py b/src/chameleon/zpt/program.py index 40e5542a..2769b257 100644 --- a/src/chameleon/zpt/program.py +++ b/src/chameleon/zpt/program.py @@ -234,7 +234,7 @@ def visit_element(self, start, end, children): macro_name = macro_name.rsplit('/', 1)[-1] inner = nodes.Define( [nodes.Assignment( - ["macroname"], Static(ast.Str(macro_name)), True)], + ["macroname"], Static(ast.Constant(macro_name)), True)], inner, ) STATIC_ATTRIBUTES = None @@ -592,7 +592,8 @@ def CASE(node): nodes.Sequence( [attr for attr in attributes if isinstance(attr, nodes.Attribute) and - isinstance(attr.expression, ast.Str)] + isinstance(attr.expression, ast.Constant) and + isinstance(attr.expression.value, str)] ) ) if end_tag is None: @@ -796,7 +797,7 @@ def _create_attributes_nodes(self, prepared, I18N_ATTRIBUTES): if boolean: value = nodes.Replace(value, name) else: - default = ast.Str(s=text) if text is not None else None + default = ast.Constant(text) if text is not None else None # If the expression is non-trivial, the attribute is # dynamic (computed). @@ -833,7 +834,7 @@ def _create_attributes_nodes(self, prepared, I18N_ATTRIBUTES): # here if there's one or more "computed" attributes # (dynamic, from one or more dict values). else: - value = ast.Str(s=text) + value = ast.Constant(text) if msgid is missing and implicit_i18n: msgid = text @@ -855,7 +856,10 @@ def _create_attributes_nodes(self, prepared, I18N_ATTRIBUTES): filtering[-1], ) - if not isinstance(value, ast.Str): + if ( + not isinstance(value, ast.Constant) + or not isinstance(value.value, str) + ): # Always define a ``default`` alias for non-static # expressions. attribute = nodes.Define( diff --git a/tox.ini b/tox.ini index 51ebcd20..6518cb2b 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ deps = pytest setuptools commands = - pytest --doctest-modules -W error:"the load_module":DeprecationWarning -W error:"zipimport.zipimporter.load_module":DeprecationWarning {posargs} + pytest --doctest-modules -W error:"the load_module":DeprecationWarning -W error:"zipimport.zipimporter.load_module":DeprecationWarning -W error:"ast.":DeprecationWarning {posargs} extras = test setenv = @@ -114,6 +114,7 @@ commands = mypy -p chameleon --python-version 3.10 mypy -p chameleon --python-version 3.11 mypy -p chameleon --python-version 3.12 + mypy -p chameleon --python-version 3.13 [testenv:z3c.macro] basepython = python3