Merge pull request #6961 from youknowone/unparse

Update _ast_unparse and fix unparse, type_comments
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-02-03 15:41:01 +09:00
commit f0ac90b740
20 files changed
+1314 -70

No files matched your search

+1 -1
View File
@@ -93,7 +93,7 @@ rm -r target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -r
```bash
# Run Rust unit tests
cargo test --workspace --exclude rustpython_wasm
cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher
# Run Python snippets tests (debug mode recommended for faster compilation)
cargo run -- extra_tests/snippets/builtin_bytes.py
+1 -1
View File
@@ -65,7 +65,7 @@ $ pytest -v
Rust unit tests can be run with `cargo`:
```shell
$ cargo test --workspace --exclude rustpython_wasm
$ cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher
```
Python unit tests can be run by compiling RustPython and running the test module:
+28 -35
View File
@@ -97,7 +97,7 @@ class AST_Tests(unittest.TestCase):
# "ast.AST constructor takes 0 positional arguments"
ast.AST(2)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "type object 'ast.AST' has no attribute '_fields'" does not match "'AST' object has no attribute '_fields'"
def test_AST_fields_NULL_check(self):
# See: https://github.com/python/cpython/issues/126105
old_value = ast.AST._fields
@@ -115,7 +115,7 @@ class AST_Tests(unittest.TestCase):
with self.assertRaisesRegex(AttributeError, msg):
ast.AST()
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <test.test_ast.test_ast.AST_Tests.test_AST_garbage_collection.<locals>.X object at 0x7e85c3a80> is not None
def test_AST_garbage_collection(self):
class X:
pass
@@ -140,7 +140,7 @@ class AST_Tests(unittest.TestCase):
with self.subTest(action="compiling", input=i, kind=kind):
compile(ast_tree, "?", kind)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected some sort of expr, but got <_ast.TemplateStr object at 0x7e85c34e0>
def test_ast_validation(self):
# compile() is the only function that calls PyAST_Validate
snippets_to_validate = exec_tests + single_tests + eval_tests
@@ -439,7 +439,7 @@ class AST_Tests(unittest.TestCase):
kwargs[name] = self._construct_ast_class(typ)
return cls(**kwargs)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: type object 'arguments' has no attribute '__annotations__'
def test_arguments(self):
x = ast.arguments()
self.assertEqual(x._fields, ('posonlyargs', 'args', 'vararg', 'kwonlyargs',
@@ -590,7 +590,7 @@ class AST_Tests(unittest.TestCase):
x = ast.Sub()
self.assertEqual(x._fields, ())
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'but got expr()' not found in 'expected some sort of expr, but got <_ast.expr object at 0x7e911a9a0>'
def test_invalid_sum(self):
pos = dict(lineno=2, col_offset=3)
m = ast.Module([ast.Expr(ast.expr(**pos), **pos)], [])
@@ -598,7 +598,7 @@ class AST_Tests(unittest.TestCase):
compile(m, "<test>", "exec")
self.assertIn("but got expr()", str(cm.exception))
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: expected str for name
def test_invalid_identifier(self):
m = ast.Module([ast.Expr(ast.Name(42, ast.Load()))], [])
ast.fix_missing_locations(m)
@@ -615,7 +615,7 @@ class AST_Tests(unittest.TestCase):
):
compile(e, "<test>", "eval")
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected some sort of expr, but got None
def test_empty_yield_from(self):
# Issue 16546: yield from value is not optional.
empty_yield_from = ast.parse("def f():\n yield from g()")
@@ -670,7 +670,7 @@ class AST_Tests(unittest.TestCase):
attr_b = tree.body[0].decorator_list[0].value
self.assertEqual(attr_b.end_col_offset, 4)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: None != 'withitem(expr context_expr, expr? optional_vars)'
def test_ast_asdl_signature(self):
self.assertEqual(ast.withitem.__doc__, "withitem(expr context_expr, expr? optional_vars)")
self.assertEqual(ast.GtE.__doc__, "GtE")
@@ -776,7 +776,6 @@ class AST_Tests(unittest.TestCase):
del a2.id
self.assertTrue(ast.compare(a1, a2))
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: type object '_ast.Module' has no attribute '_field_types'
def test_compare_modes(self):
for mode, sources in (
("exec", exec_tests),
@@ -1100,10 +1099,6 @@ class AST_Tests(unittest.TestCase):
self.assertIsInstance(tree.body[0].value.values[0], ast.Constant)
self.assertIsInstance(tree.body[0].value.values[1], ast.Interpolation)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_classattrs_deprecated(self):
return super().test_classattrs_deprecated()
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_optimization_levels_const_folding(self):
return super().test_optimization_levels_const_folding()
@@ -1472,7 +1467,6 @@ class CopyTests(unittest.TestCase):
class ASTHelpers_Test(unittest.TestCase):
maxDiff = None
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_parse(self):
a = ast.parse('foo(1 + 1)')
b = compile('foo(1 + 1)', '<unknown>', 'exec', ast.PyCF_ONLY_AST)
@@ -1486,7 +1480,7 @@ class ASTHelpers_Test(unittest.TestCase):
ast.literal_eval(r"'\U'")
self.assertIsNotNone(e.exception.__context__)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; + Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load()), args=[Name(id='eggs', ctx=Load()), Constant(value='and cheese')]))])
def test_dump(self):
node = ast.parse('spam(eggs, "and cheese")')
self.assertEqual(ast.dump(node),
@@ -1507,7 +1501,7 @@ class ASTHelpers_Test(unittest.TestCase):
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=24)])"
)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; - type_ignores=[])
def test_dump_indent(self):
node = ast.parse('spam(eggs, "and cheese")')
self.assertEqual(ast.dump(node, indent=3), """\
@@ -1563,7 +1557,7 @@ Module(
end_lineno=1,
end_col_offset=24)])""")
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; + Raise()
def test_dump_incomplete(self):
node = ast.Raise(lineno=3, col_offset=4)
self.assertEqual(ast.dump(node),
@@ -1731,7 +1725,7 @@ Module(
full="Module(body=[Import(names=[alias(name='_ast', asname='ast')]), ImportFrom(module='module', names=[alias(name='sub')], level=0)], type_ignores=[])",
)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; ? ^^^^^^^^^ ^^^^^^^^^
def test_copy_location(self):
src = ast.parse('1 + 1', mode='eval')
src.body.right = ast.copy_location(ast.Constant(2), src.body.right)
@@ -1749,7 +1743,7 @@ Module(
self.assertEqual(new.lineno, 1)
self.assertEqual(new.col_offset, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; + Module(body=[Expr(value=Call(func=Name(id='write', ctx=Load(), lineno=1, col_offset=0, end_lineno=1, end_col_offset=5), args=[Constant(value='spam', lineno=1, col_offset=6, end_lineno=1, end_col_offset=12)], lineno=1, col_offset=0, end_lineno=1, end_col_offset=13), lineno=1, col_offset=0, end_lineno=1, end_col_offset=13), Expr(value=Call(func=Name(id='spam', ctx=Load(), lineno=1, col_offset=0, end_lineno=1, end_col_offset=0), args=[Constant(value='eggs', lineno=1, col_offset=0, end_lineno=1, end_col_offset=0)], lineno=1, col_offset=0, end_lineno=1, end_col_offset=0), lineno=1, col_offset=0, end_lineno=1, end_col_offset=0)])
def test_fix_missing_locations(self):
src = ast.parse('write("spam")')
src.body.append(ast.Expr(ast.Call(ast.Name('spam', ast.Load()),
@@ -1769,7 +1763,7 @@ Module(
"end_col_offset=0), lineno=1, col_offset=0, end_lineno=1, end_col_offset=0)])"
)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; ? ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
def test_increment_lineno(self):
src = ast.parse('1 + 1', mode='eval')
self.assertEqual(ast.increment_lineno(src, n=3), src)
@@ -1813,7 +1807,7 @@ Module(
self.assertEqual(d.pop('func').id, 'foo')
self.assertEqual(d, {'keywords': [], 'args': []})
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; + keyword(arg='eggs', value=Constant(value='leek'))
def test_iter_child_nodes(self):
node = ast.parse("spam(23, 42, eggs='leek')", mode='eval')
self.assertEqual(len(list(ast.iter_child_nodes(node.body))), 4)
@@ -1966,7 +1960,7 @@ Module(
malformed = ast.Dict(keys=[ast.Constant(1)], values=[ast.Constant(2), ast.Constant(3)])
self.assertRaises(ValueError, ast.literal_eval, malformed)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: expected an expression
def test_literal_eval_trailing_ws(self):
self.assertEqual(ast.literal_eval(" -1"), -1)
self.assertEqual(ast.literal_eval("\t\t-1"), -1)
@@ -1985,7 +1979,7 @@ Module(
with self.assertRaisesRegex(ValueError, msg):
ast.literal_eval(node)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "unexpected indent" does not match "expected an expression (<unknown>, line 2)"
def test_literal_eval_syntax_errors(self):
with self.assertRaisesRegex(SyntaxError, "unexpected indent"):
ast.literal_eval(r'''
@@ -1993,7 +1987,7 @@ Module(
(\
\ ''')
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: required field "lineno" missing from alias
def test_bad_integer(self):
# issue13436: Bad error message with invalid numeric values
body = [ast.ImportFrom(module='time',
@@ -2222,7 +2216,7 @@ class ASTValidatorTests(unittest.TestCase):
[ast.Expr(ast.Name("x", ast.Store()))])
self.stmt(i, "must have Load context")
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: empty items on With
def test_with(self):
p = ast.Pass()
self.stmt(ast.With([], [p]), "empty items on With")
@@ -2263,7 +2257,7 @@ class ASTValidatorTests(unittest.TestCase):
t = ast.Try([p], e, [p], [ast.Expr(ast.Name("x", ast.Store()))])
self.stmt(t, "must have Load context")
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ValueError not raised
def test_try_star(self):
p = ast.Pass()
t = ast.TryStar([], [], [], [p])
@@ -2296,7 +2290,7 @@ class ASTValidatorTests(unittest.TestCase):
def test_import(self):
self.stmt(ast.Import([]), "empty names on Import")
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; OverflowError: Python int too large to convert to Rust u32
def test_importfrom(self):
imp = ast.ImportFrom(None, [ast.alias("x", None)], -42)
self.stmt(imp, "Negative ImportFrom level")
@@ -2354,7 +2348,7 @@ class ASTValidatorTests(unittest.TestCase):
d = ast.Dict([ast.Name("x", ast.Load())], [None])
self.expr(d, "None disallowed")
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected some sort of expr, but got None
def test_set(self):
self.expr(ast.Set([None]), "None disallowed")
s = ast.Set([ast.Name("x", ast.Store())])
@@ -2412,7 +2406,7 @@ class ASTValidatorTests(unittest.TestCase):
return ast.DictComp(k, v, comps)
self._check_comprehension(factory)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: 'yield' outside function
def test_yield(self):
self.expr(ast.Yield(ast.Name("x", ast.Store())), "must have Load")
self.expr(ast.YieldFrom(ast.Name("x", ast.Store())), "must have Load")
@@ -2478,11 +2472,11 @@ class ASTValidatorTests(unittest.TestCase):
self.expr(fac([ast.Name("x", ast.Store())], ast.Load()),
"must have Load context")
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected some sort of expr, but got None
def test_list(self):
self._sequence(ast.List)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected some sort of expr, but got None
def test_tuple(self):
self._sequence(ast.Tuple)
@@ -3264,7 +3258,7 @@ class NodeTransformerTests(ASTTestMixin, unittest.TestCase):
class ASTConstructorTests(unittest.TestCase):
"""Test the autogenerated constructors for AST nodes."""
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: DeprecationWarning not triggered
def test_FunctionDef(self):
args = ast.arguments()
self.assertEqual(args.args, [])
@@ -3278,7 +3272,7 @@ class ASTConstructorTests(unittest.TestCase):
self.assertEqual(node.name, 'foo')
self.assertEqual(node.decorator_list, [])
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: None is not an instance of <class '_ast.Load'>
def test_expr_context(self):
name = ast.Name("x")
self.assertEqual(name.id, "x")
@@ -3382,7 +3376,7 @@ class ASTConstructorTests(unittest.TestCase):
with self.assertWarnsRegex(DeprecationWarning, r"Field b'\\xff\\xff.*' .*"):
obj = BadFields()
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: None != []
def test_complete_field_types(self):
class _AllFieldTypes(ast.AST):
_fields = ('a', 'b')
@@ -3508,7 +3502,6 @@ class CommandLineTests(unittest.TestCase):
expect = self.text_normalize(expect)
self.assertEqual(res, expect)
@unittest.expectedFailure # TODO: RUSTPYTHON
@support.requires_resource('cpu')
def test_invocation(self):
# test various combinations of parameters
-2
View File
@@ -9,8 +9,6 @@ class TestExceptionGroupTypeHierarchy(unittest.TestCase):
self.assertTrue(issubclass(ExceptionGroup, BaseExceptionGroup))
self.assertTrue(issubclass(BaseExceptionGroup, BaseException))
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_exception_is_not_generic_type(self):
with self.assertRaisesRegex(TypeError, 'Exception'):
Exception[OSError]
-2
View File
@@ -151,7 +151,6 @@ class BaseTest(unittest.TestCase):
self.assertEqual(alias.__args__, (int,))
self.assertEqual(alias.__parameters__, ())
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_unsubscriptable(self):
for t in int, str, float, Sized, Hashable:
tname = t.__name__
@@ -365,7 +364,6 @@ class BaseTest(unittest.TestCase):
self.assertEqual(t(test), Test)
self.assertEqual(t(0), int)
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_type_subclass_generic(self):
class MyType(type):
pass
-1
View File
@@ -228,7 +228,6 @@ class TestClassGetitem(unittest.TestCase):
with self.assertRaises(TypeError):
C_too_many[int]
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_class_getitem_errors_2(self):
class C:
def __class_getitem__(cls, item):
+1072
View File
File diff suppressed because it is too large. Load diff
+11 -9
View File
@@ -5039,8 +5039,9 @@ impl Compiler {
}
fn compile_error_forbidden_name(&mut self, name: &str) -> CodegenError {
// TODO: make into error (fine for now since it realistically errors out earlier)
panic!("Failing due to forbidden name {name:?}");
self.error(CodegenErrorType::SyntaxError(format!(
"cannot use forbidden name '{name}' in pattern"
)))
}
/// Ensures that `pc.fail_pop` has at least `n + 1` entries.
@@ -5387,12 +5388,9 @@ impl Compiler {
// Check for too many sub-patterns.
if nargs > u32::MAX as usize || (nargs + n_attrs).saturating_sub(1) > i32::MAX as usize {
let msg = format!(
"too many sub-patterns in class pattern {:?}",
match_class.cls
);
panic!("{}", msg);
// return self.compiler_error(&msg);
return Err(self.error(CodegenErrorType::SyntaxError(
"too many sub-patterns in class pattern".to_owned(),
)));
}
// Validate keyword attributes if any.
@@ -5677,7 +5675,11 @@ impl Compiler {
// Ensure the pattern is a MatchOr.
let end = self.new_block(); // Create a new jump target label.
let size = p.patterns.len();
assert!(size > 1, "MatchOr must have more than one alternative");
if size <= 1 {
return Err(self.error(CodegenErrorType::SyntaxError(
"MatchOr requires at least 2 patterns".to_owned(),
)));
}
// Save the current pattern context.
let old_pc = pc.clone();
+9 -2
View File
@@ -273,8 +273,15 @@ impl StringParser {
}
pub(crate) fn parse_string_literal(source: &str, flags: ast::AnyStringFlags) -> Box<Wtf8> {
let source = &source[flags.opener_len().to_usize()..];
let source = &source[..source.len() - flags.quote_len().to_usize()];
let opener_len = flags.opener_len().to_usize();
let quote_len = flags.quote_len().to_usize();
if source.len() < opener_len + quote_len {
// Source unavailable (e.g., compiling from an AST object with no
// backing source text). Return the raw source as-is.
return Box::<Wtf8>::from(source);
}
let source = &source[opener_len..];
let source = &source[..source.len() - quote_len];
StringParser::new(source.into(), flags)
.parse_string()
.unwrap_or_else(|x| match x {})
+14 -1
View File
@@ -363,7 +363,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
self.p(")")?;
}
ast::Expr::FString(ast::ExprFString { value, .. }) => self.unparse_fstring(value)?,
ast::Expr::TString(_) => self.p("t\"\"")?,
ast::Expr::TString(ast::ExprTString { value, .. }) => self.unparse_tstring(value)?,
ast::Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if value.is_unicode() {
self.p("u")?
@@ -626,6 +626,19 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
.str_repr()
.write(self.f)
}
fn unparse_tstring(&mut self, value: &ast::TStringValue) -> fmt::Result {
self.p("t")?;
let body = fmt::from_fn(|f| {
value.iter().try_for_each(|tstring| {
Unparser::new(f, self.source).unparse_fstring_body(&tstring.elements)
})
})
.to_string();
UnicodeEscape::new_repr(body.as_str().as_ref())
.str_repr()
.write(self.f)
}
}
pub struct UnparseExpr<'a> {
+5
View File
@@ -704,9 +704,14 @@ impl PyObject {
if let Some(class_getitem) =
vm.get_attribute_opt(self.to_owned(), identifier!(vm, __class_getitem__))?
&& !vm.is_none(&class_getitem)
{
return class_getitem.call((needle,), vm);
}
return Err(vm.new_type_error(format!(
"type '{}' is not subscriptable",
self.downcast_ref::<PyType>().unwrap().name()
)));
}
Err(vm.new_type_error(format!("'{}' object is not subscriptable", self.class())))
}
+36
View File
@@ -265,6 +265,42 @@ fn node_add_location(
.unwrap();
}
/// Return the expected AST mod type class for a compile() mode string.
pub(crate) fn mode_type_and_name(
ctx: &Context,
mode: &str,
) -> Option<(PyRef<PyType>, &'static str)> {
match mode {
"exec" => Some((pyast::NodeModModule::make_class(ctx), "Module")),
"eval" => Some((pyast::NodeModExpression::make_class(ctx), "Expression")),
"single" => Some((pyast::NodeModInteractive::make_class(ctx), "Interactive")),
"func_type" => Some((pyast::NodeModFunctionType::make_class(ctx), "FunctionType")),
_ => None,
}
}
/// Create an empty `arguments` AST node (no parameters).
fn empty_arguments_object(vm: &VirtualMachine) -> PyObjectRef {
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeArguments::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
for list_field in [
"posonlyargs",
"args",
"kwonlyargs",
"kw_defaults",
"defaults",
] {
dict.set_item(list_field, vm.ctx.new_list(vec![]).into(), vm)
.unwrap();
}
for none_field in ["vararg", "kwarg"] {
dict.set_item(none_field, vm.ctx.none(), vm).unwrap();
}
node.into()
}
#[cfg(feature = "parser")]
pub(crate) fn parse(
vm: &VirtualMachine,
+6 -2
View File
@@ -327,8 +327,12 @@ impl Node for ast::ExprLambda {
.into_ref_with_type(vm, pyast::NodeExprLambda::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("args", parameters.ast_to_object(vm, source_file), vm)
.unwrap();
// Lambda with no parameters should have an empty arguments object, not None
let args = match parameters {
Some(params) => params.ast_to_object(vm, source_file),
None => empty_arguments_object(vm),
};
dict.set_item("args", args, vm).unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm)
.unwrap();
node_add_location(&dict, _range, vm, source_file);
+1 -1
View File
@@ -3,7 +3,7 @@ use rustpython_compiler_core::SourceFile;
impl Node for ast::ConversionFlag {
fn ast_to_object(self, vm: &VirtualMachine, _source_file: &SourceFile) -> PyObjectRef {
vm.ctx.new_int(self as u8).into()
vm.ctx.new_int(self as i8).into()
}
fn ast_from_object(
+2 -2
View File
@@ -126,8 +126,8 @@ impl Node for ast::Parameter {
_vm,
)
.unwrap();
// dict.set_item("type_comment", type_comment.ast_to_object(_vm), _vm)
// .unwrap();
// Ruff AST doesn't track type_comment, so always set to None
dict.set_item("type_comment", _vm.ctx.none(), _vm).unwrap();
node_add_location(&dict, range, _vm, source_file);
node.into()
}
+9 -3
View File
@@ -37,6 +37,12 @@ macro_rules! impl_node {
),*
]).into(),
);
// Signal that this is a built-in AST node with field defaults
class.set_attr(
ctx.intern_str("_field_types"),
ctx.new_dict().into(),
);
}
}
};
@@ -902,21 +908,21 @@ impl_node!(
impl_node!(
#[pyclass(module = "_ast", name = "TypeVar", base = NodeTypeParam)]
pub(crate) struct NodeTypeParamTypeVar,
fields: ["name", "bound"],
fields: ["name", "bound", "default_value"],
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"],
);
impl_node!(
#[pyclass(module = "_ast", name = "ParamSpec", base = NodeTypeParam)]
pub(crate) struct NodeTypeParamParamSpec,
fields: ["name"],
fields: ["name", "default_value"],
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"],
);
impl_node!(
#[pyclass(module = "_ast", name = "TypeVarTuple", base = NodeTypeParam)]
pub(crate) struct NodeTypeParamTypeVarTuple,
fields: ["name"],
fields: ["name", "default_value"],
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"],
);
+76
View File
@@ -65,8 +65,13 @@ pub(crate) mod _ast {
if fields.len() == 1 { "" } else { "s" },
)));
}
// Track which fields were set
let mut set_fields = std::collections::HashSet::new();
for (name, arg) in fields.iter().zip(args.args) {
zelf.set_attr(name, arg, vm)?;
set_fields.insert(name.as_str().to_string());
}
for (key, value) in args.kwargs {
if let Some(pos) = fields.iter().position(|f| f.as_str() == key)
@@ -78,9 +83,80 @@ pub(crate) mod _ast {
key
)));
}
set_fields.insert(key.clone());
zelf.set_attr(vm.ctx.intern_str(key), value, vm)?;
}
// Set default values only for built-in AST nodes (_field_types present).
// Custom AST subclasses without _field_types do NOT get automatic defaults.
let has_field_types = zelf
.class()
.get_attr(vm.ctx.intern_str("_field_types"))
.is_some();
if has_field_types {
// ASDL list fields (type*) default to empty list,
// optional/required fields default to None.
// Fields that are always list-typed regardless of node class.
const LIST_FIELDS: &[&str] = &[
"argtypes",
"bases",
"cases",
"comparators",
"decorator_list",
"defaults",
"elts",
"finalbody",
"generators",
"handlers",
"ifs",
"items",
"keys",
"kw_defaults",
"kwd_attrs",
"kwd_patterns",
"keywords",
"kwonlyargs",
"names",
"ops",
"patterns",
"posonlyargs",
"targets",
"type_ignores",
"type_params",
"values",
];
let class_name = zelf.class().name().to_string();
for field in &fields {
if !set_fields.contains(field.as_str()) {
let field_name = field.as_str();
// Some field names have different ASDL types depending on the node.
// For example, "args" is `expr*` in Call but `arguments` in Lambda.
// "body" and "orelse" are `stmt*` in most nodes but `expr` in IfExp.
let is_list_field = if field_name == "args" {
class_name == "Call" || class_name == "arguments"
} else if field_name == "body" || field_name == "orelse" {
!matches!(class_name.as_str(), "Lambda" | "Expression" | "IfExp")
} else {
LIST_FIELDS.contains(&field_name)
};
let default: PyObjectRef = if is_list_field {
vm.ctx.new_list(vec![]).into()
} else {
vm.ctx.none()
};
zelf.set_attr(vm.ctx.intern_str(field_name), default, vm)?;
}
}
// Special defaults that are not None or empty list
if class_name == "ImportFrom" && !set_fields.contains("level") {
zelf.set_attr("level", vm.ctx.new_int(0), vm)?;
}
}
Ok(())
}
+6 -7
View File
@@ -182,9 +182,8 @@ impl Node for ast::StmtFunctionDef {
.unwrap();
dict.set_item("returns", returns.ast_to_object(vm, source_file), vm)
.unwrap();
// TODO: Ruff ignores type_comment during parsing
// dict.set_item("type_comment", type_comment.ast_to_object(_vm), _vm)
// .unwrap();
// Ruff AST doesn't track type_comment, so always set to None
dict.set_item("type_comment", vm.ctx.none(), vm).unwrap();
dict.set_item(
"type_params",
type_params
@@ -647,8 +646,8 @@ impl Node for ast::StmtFor {
.unwrap();
dict.set_item("orelse", orelse.ast_to_object(_vm, source_file), _vm)
.unwrap();
// dict.set_item("type_comment", type_comment.ast_to_object(_vm), _vm)
// .unwrap();
// Ruff AST doesn't track type_comment, so always set to None
dict.set_item("type_comment", _vm.ctx.none(), _vm).unwrap();
node_add_location(&dict, _range, _vm, source_file);
node.into()
}
@@ -799,8 +798,8 @@ impl Node for ast::StmtWith {
.unwrap();
dict.set_item("body", body.ast_to_object(_vm, source_file), _vm)
.unwrap();
// dict.set_item("type_comment", type_comment.ast_to_object(_vm), _vm)
// .unwrap();
// Ruff AST doesn't track type_comment, so always set to None
dict.set_item("type_comment", _vm.ctx.none(), _vm).unwrap();
node_add_location(&dict, _range, _vm, source_file);
node.into()
}
+7 -1
View File
@@ -78,7 +78,7 @@ impl Node for ast::TypeParamTypeVar {
name,
bound,
range: _range,
default: _,
default,
} = self;
let node = NodeAst
.into_ref_with_type(_vm, pyast::NodeTypeParamTypeVar::static_type().to_owned())
@@ -88,6 +88,12 @@ impl Node for ast::TypeParamTypeVar {
.unwrap();
dict.set_item("bound", bound.ast_to_object(_vm, source_file), _vm)
.unwrap();
dict.set_item(
"default_value",
default.ast_to_object(_vm, source_file),
_vm,
)
.unwrap();
node_add_location(&dict, _range, _vm, source_file);
node.into()
}
+30
View File
@@ -141,6 +141,36 @@ mod builtins {
.source
.fast_isinstance(&ast::NodeAst::make_class(&vm.ctx))
{
use num_traits::Zero;
let flags: i32 = args.flags.map_or(Ok(0), |v| v.try_to_primitive(vm))?;
let is_ast_only = !(flags & ast::PY_COMPILE_FLAG_AST_ONLY).is_zero();
// func_type mode requires PyCF_ONLY_AST
if mode_str == "func_type" && !is_ast_only {
return Err(vm.new_value_error(
"compile() mode 'func_type' requires flag PyCF_ONLY_AST".to_owned(),
));
}
// compile(ast_node, ..., PyCF_ONLY_AST) returns the AST after validation
if is_ast_only {
let (expected_type, expected_name) = ast::mode_type_and_name(&vm.ctx, mode_str)
.ok_or_else(|| {
vm.new_value_error(
"compile() mode must be 'exec', 'eval', 'single' or 'func_type'"
.to_owned(),
)
})?;
if !args.source.fast_isinstance(&expected_type) {
return Err(vm.new_type_error(format!(
"expected {} node, got {}",
expected_name,
args.source.class().name()
)));
}
return Ok(args.source);
}
#[cfg(not(feature = "rustpython-codegen"))]
{
return Err(vm.new_type_error(CODEGEN_NOT_SUPPORTED.to_owned()));