mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 11:14:38 +08:00
Merge pull request #6961 from youknowone/unparse
Update _ast_unparse and fix unparse, type_comments
This commit is contained in:
commit
f0ac90b740
20 files changed
+1314
-70
No files matched your search
@@ -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
@@ -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:
|
||||
|
||||
Vendored
+28
-35
@@ -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
|
||||
|
||||
Vendored
-2
@@ -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]
|
||||
|
||||
Vendored
-2
@@ -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
|
||||
|
||||
Vendored
-1
@@ -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):
|
||||
|
||||
Vendored
+1072
File diff suppressed because it is too large.
Load diff
@@ -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();
|
||||
|
||||
@@ -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 {})
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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())))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
);
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
Reference in new issue
Block a user