Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# parse_options: { "target-version": "3.8" }
async def foo():
@await bar
def baz(): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# parse_options: { "target-version": "3.8" }
@{3: 3}
def bar(): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# parse_options: { "target-version": "3.8" }
@3.14
def bar(): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# parse_options: { "target-version": "3.9" }
async def foo():
@await bar
def baz(): ...
17 changes: 14 additions & 3 deletions crates/ruff_python_parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,9 @@ pub enum UnsupportedSyntaxErrorKind {
/// [PEP 614]: https://peps.python.org/pep-0614/
/// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name
/// [decorator grammar]: https://docs.python.org/3/reference/compound_stmts.html#grammar-token-python-grammar-decorator
RelaxedDecorator,
RelaxedDecorator {
invalid_node_name: &'static str,
},

/// Represents the use of a [PEP 570] positional-only parameter before Python 3.8.
///
Expand Down Expand Up @@ -633,7 +635,14 @@ impl Display for UnsupportedSyntaxError {
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Yield) => {
"Cannot use iterable unpacking in yield expressions"
}
UnsupportedSyntaxErrorKind::RelaxedDecorator => "Unsupported expression in decorators",
UnsupportedSyntaxErrorKind::RelaxedDecorator { invalid_node_name } => {
return write!(
f,
"Cannot use {invalid_node_name} outside function-call arguments in a decorator \
on Python {target_version} (syntax was added in Python 3.9)",
target_version = self.target_version,
);
}
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
"Cannot use positional-only parameter separator"
}
Expand Down Expand Up @@ -677,7 +686,9 @@ impl UnsupportedSyntaxErrorKind {
UnsupportedSyntaxErrorKind::Walrus => Change::Added(PythonVersion::PY38),
UnsupportedSyntaxErrorKind::ExceptStar => Change::Added(PythonVersion::PY311),
UnsupportedSyntaxErrorKind::StarTuple(_) => Change::Added(PythonVersion::PY38),
UnsupportedSyntaxErrorKind::RelaxedDecorator => Change::Added(PythonVersion::PY39),
UnsupportedSyntaxErrorKind::RelaxedDecorator { .. } => {
Change::Added(PythonVersion::PY39)
}
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
Change::Added(PythonVersion::PY38)
}
Expand Down
56 changes: 49 additions & 7 deletions crates/ruff_python_parser/src/parser/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use ruff_python_ast::{self as ast, CmpOp, Expr, ExprContext};
use ruff_python_ast::{self as ast, CmpOp, Expr, ExprContext, Number};
use ruff_text_size::{Ranged, TextRange};

use crate::TokenKind;

Expand Down Expand Up @@ -47,11 +48,52 @@ pub(super) const fn token_kind_to_cmp_op(tokens: [TokenKind; 2]) -> Option<CmpOp
/// Helper for `parse_decorators` to determine if `expr` is a [`dotted_name`] from the decorator
/// grammar before Python 3.9.
///
/// Returns `None` if `expr` is a `dotted_name`. Returns `Some((description, range))` if it is not,
/// where `description` is a string describing the invalid node and `range` is the node's range.
///
/// [`dotted_name`]: https://docs.python.org/3.8/reference/compound_stmts.html#grammar-token-dotted-name
pub(super) fn is_name_or_attribute_expression(expr: &Expr) -> bool {
match expr {
Expr::Attribute(attr) => is_name_or_attribute_expression(&attr.value),
Expr::Name(_) => true,
_ => false,
}
pub(super) fn invalid_pre_py39_decorator_node(expr: &Expr) -> Option<(&'static str, TextRange)> {
let description = match expr {
Expr::Attribute(attr) => return invalid_pre_py39_decorator_node(&attr.value),

Expr::Name(_) => None,

Expr::NumberLiteral(number) => match &number.value {
Number::Int(_) => Some("an int literal"),
Number::Float(_) => Some("a float literal"),
Number::Complex { .. } => Some("a complex literal"),
},

Expr::BoolOp(_) => Some("boolean expression"),
Expr::BinOp(_) => Some("binary-operation expression"),
Expr::UnaryOp(_) => Some("unary-operation expression"),
Expr::Await(_) => Some("`await` expression"),
Expr::Lambda(_) => Some("lambda expression"),
Expr::If(_) => Some("conditional expression"),
Expr::Dict(_) => Some("a dict literal"),
Expr::Set(_) => Some("a set literal"),
Expr::List(_) => Some("a list literal"),
Expr::Tuple(_) => Some("a tuple literal"),
Expr::Starred(_) => Some("starred expression"),
Expr::Slice(_) => Some("slice expression"),
Expr::BytesLiteral(_) => Some("bytes literal"),
Expr::StringLiteral(_) => Some("string literal"),
Expr::EllipsisLiteral(_) => Some("ellipsis literal"),
Expr::NoneLiteral(_) => Some("`None` literal"),
Expr::BooleanLiteral(_) => Some("boolean literal"),
Expr::ListComp(_) => Some("list comprehension"),
Expr::SetComp(_) => Some("set comprehension"),
Expr::DictComp(_) => Some("dict comprehension"),
Expr::Generator(_) => Some("generator expression"),
Expr::Yield(_) => Some("`yield` expression"),
Expr::YieldFrom(_) => Some("`yield from` expression"),
Expr::Compare(_) => Some("comparison expression"),
Expr::Call(_) => Some("function call"),
Expr::FString(_) => Some("f-string"),
Expr::Named(_) => Some("assignment expression"),
Expr::Subscript(_) => Some("subscript expression"),
Expr::IpyEscapeCommand(_) => Some("IPython escape command"),
};

description.map(|description| (description, expr.range()))
}
37 changes: 31 additions & 6 deletions crates/ruff_python_parser/src/parser/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2678,17 +2678,42 @@ impl<'src> Parser<'src> {
// # parse_options: { "target-version": "3.7" }
// @(x := lambda x: x)(foo)
// def bar(): ...
let allowed_decorator = match &parsed_expr.expr {

// test_err decorator_dict_literal_py38
// # parse_options: { "target-version": "3.8" }
// @{3: 3}
// def bar(): ...

// test_err decorator_float_literal_py38
// # parse_options: { "target-version": "3.8" }
// @3.14
// def bar(): ...

// test_ok decorator_await_expression_py39
// # parse_options: { "target-version": "3.9" }
// async def foo():
// @await bar
// def baz(): ...

// test_err decorator_await_expression_py38
// # parse_options: { "target-version": "3.8" }
// async def foo():
// @await bar
// def baz(): ...

let disallowed_expression = match &parsed_expr.expr {
Expr::Call(expr_call) => {
helpers::is_name_or_attribute_expression(&expr_call.func)
helpers::invalid_pre_py39_decorator_node(&expr_call.func)
}
expr => helpers::is_name_or_attribute_expression(expr),
expr => helpers::invalid_pre_py39_decorator_node(expr),
};

if !allowed_decorator {
if let Some((description, range)) = disallowed_expression {
self.add_unsupported_syntax_error(
UnsupportedSyntaxErrorKind::RelaxedDecorator,
parsed_expr.range(),
UnsupportedSyntaxErrorKind::RelaxedDecorator {
invalid_node_name: description,
},
range,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/decorator_await_expression_py38.py
---
## AST

```
Module(
ModModule {
range: 0..96,
body: [
FunctionDef(
StmtFunctionDef {
range: 45..95,
is_async: true,
decorator_list: [],
name: Identifier {
id: Name("foo"),
range: 55..58,
},
type_params: None,
parameters: Parameters {
range: 58..60,
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
FunctionDef(
StmtFunctionDef {
range: 66..95,
is_async: false,
decorator_list: [
Decorator {
range: 66..76,
expression: Await(
ExprAwait {
range: 67..76,
value: Name(
ExprName {
range: 73..76,
id: Name("bar"),
ctx: Load,
},
),
},
),
},
],
name: Identifier {
id: Name("baz"),
range: 85..88,
},
type_params: None,
parameters: Parameters {
range: 88..90,
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
Expr(
StmtExpr {
range: 92..95,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 92..95,
},
),
},
),
],
},
),
],
},
),
],
},
)
```
## Unsupported Syntax Errors

|
1 | # parse_options: { "target-version": "3.8" }
2 | async def foo():
3 | @await bar
| ^^^^^^^^^ Syntax Error: Cannot use `await` expression outside function-call arguments in a decorator on Python 3.8 (syntax was added in Python 3.9)
Copy link
Member

@dhruvmanila dhruvmanila Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually find the "outside function-call arguments in a decorator" part a bit confusing because it made me think that this is only allowed in function-call arguments. I'd possibly remove it and keep it simply "Cannot use await expression in a decorator on Python 3.8 ..." but then the message cannot be used in a general way because, in this example, the await expression is only not allowed at the top-level (@(await x)) while @decorator(await x) would be ok.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. So you interpreted "outside function-call arguments" as meaning "If I add parentheses, it will be valid syntax"?

Hmm, I'm not sure how to clarify that :/ adding the parentheses doesn't make it a function call -- but I can see how a beginner might find the language confusing. Would you find "outside call expression arguments" any clearer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late reply, what I thought it could be interpreted is related to the function that's being decorated and not the decorator itself. But, I think what you've is fine as well. We can iterate if anyone finds it confusing.

4 | def baz(): ...
|
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/decorator_dict_literal_py38.py
---
## AST

```
Module(
ModModule {
range: 0..68,
body: [
FunctionDef(
StmtFunctionDef {
range: 45..67,
is_async: false,
decorator_list: [
Decorator {
range: 45..52,
expression: Dict(
ExprDict {
range: 46..52,
items: [
DictItem {
key: Some(
NumberLiteral(
ExprNumberLiteral {
range: 47..48,
value: Int(
3,
),
},
),
),
value: NumberLiteral(
ExprNumberLiteral {
range: 50..51,
value: Int(
3,
),
},
),
},
],
},
),
},
],
name: Identifier {
id: Name("bar"),
range: 57..60,
},
type_params: None,
parameters: Parameters {
range: 60..62,
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
Expr(
StmtExpr {
range: 64..67,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 64..67,
},
),
},
),
],
},
),
],
},
)
```
## Unsupported Syntax Errors

|
1 | # parse_options: { "target-version": "3.8" }
2 | @{3: 3}
| ^^^^^^ Syntax Error: Cannot use a dict literal outside function-call arguments in a decorator on Python 3.8 (syntax was added in Python 3.9)
3 | def bar(): ...
|
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,6 @@ Module(
|
1 | # parse_options: { "target-version": "3.8" }
2 | @buttons[0].clicked.connect
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unsupported expression in decorators on Python 3.8 (syntax was added in Python 3.9)
| ^^^^^^^^^^ Syntax Error: Cannot use subscript expression outside function-call arguments in a decorator on Python 3.8 (syntax was added in Python 3.9)
3 | def spam(): ...
|
Loading
Loading