1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
|
From: =?utf-8?b?TWljaGHFgiBHw7Nybnk=?= <mgorny@gentoo.org>
Date: Wed, 14 May 2025 19:52:30 +0200
Subject: Fix compatibility with Python 3.14 (mostly)
Fix the code and the test suite to work with Python 3.14, where
deprecated constant-like AST nodes were removed. Notably:
1. Skip tests for deprecated nodes in Python 3.14.
2. Use `ast.Constant` over `ast.Num` for non-deprecated code
in Python 3.6+.
3. Check for `ast.Str` only in Python < 3.14, and handle `ast.Constant`
being used to represent a string instead.
With these changes, all tests except for:
tests/test_rtrip.py::RtripTestCase::test_convert_stdlib
pass. However, this particular test also hanged for me with older Python
versions.
Related to #217
Origin: other, https://github.com/berkerpeksag/astor/pull/233
Bug: https://github.com/berkerpeksag/astor/issues/217
Bug-Debian: https://bugs.debian.org/1123191
Last-Update: 2026-01-08
---
astor/code_gen.py | 9 +++++++--
tests/test_code_gen.py | 11 ++++++++---
2 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/astor/code_gen.py b/astor/code_gen.py
index e004807..7fd90a5 100644
--- a/astor/code_gen.py
+++ b/astor/code_gen.py
@@ -588,6 +588,7 @@ class SourceGenerator(ExplicitNodeVisitor):
current_line = ''.join(current_line)
has_ast_constant = sys.version_info >= (3, 6)
+ has_ast_str = sys.version_info < (3, 14)
if is_joined:
# Handle new f-strings. This is a bit complicated, because
@@ -596,7 +597,7 @@ class SourceGenerator(ExplicitNodeVisitor):
def recurse(node):
for value in node.values:
- if isinstance(value, ast.Str):
+ if has_ast_str and isinstance(value, ast.Str):
# Double up braces to escape them.
self.write(value.s.replace('{', '{{').replace('}', '}}'))
elif isinstance(value, ast.FormattedValue):
@@ -613,7 +614,11 @@ class SourceGenerator(ExplicitNodeVisitor):
self.write(':')
recurse(value.format_spec)
elif has_ast_constant and isinstance(value, ast.Constant):
- self.write(value.value)
+ if isinstance(value.value, str):
+ # Double up braces to escape them.
+ self.write(value.value.replace('{', '{{').replace('}', '}}'))
+ else:
+ self.write(value.value)
else:
kind = type(value).__name__
assert False, 'Invalid node %s inside JoinedStr' % kind
diff --git a/tests/test_code_gen.py b/tests/test_code_gen.py
index c9ccb90..d8a7ba8 100644
--- a/tests/test_code_gen.py
+++ b/tests/test_code_gen.py
@@ -28,7 +28,10 @@ def astorexpr(x):
return eval(astor.to_source(ast.Expression(body=x)))
def astornum(x):
- return astorexpr(ast.Num(n=x))
+ if sys.version_info >= (3, 6):
+ return astorexpr(ast.Constant(x))
+ else:
+ return astorexpr(ast.Num(n=x))
class Comparisons(object):
@@ -509,8 +512,8 @@ class CodegenTestCase(unittest.TestCase, Comparisons):
ast.Assign(targets=[ast.Name(id='spam')], value=ast.Name(id='None')),
"spam = None")
- @unittest.skipUnless(sys.version_info >= (3, 4),
- "ast.NameConstant introduced in Python 3.4")
+ @unittest.skipUnless((3, 4) <= sys.version_info < (3, 14),
+ "ast.NameConstant introduced in Python 3.4, removed in 3.14")
def test_deprecated_name_constants(self):
self.assertAstEqualsSource(
ast.Assign(targets=[ast.Name(id='spam')], value=ast.NameConstant(value=True)),
@@ -524,6 +527,8 @@ class CodegenTestCase(unittest.TestCase, Comparisons):
ast.Assign(targets=[ast.Name(id='spam')], value=ast.NameConstant(value=None)),
"spam = None")
+ @unittest.skipIf(sys.version_info >= (3, 14),
+ "Deprecated Constant nodes removed in Python 3.14")
def test_deprecated_constant_nodes(self):
self.assertAstEqualsSource(
ast.Assign(targets=[ast.Name(id='spam')], value=ast.Num(3)),
|