Skip to content

Commit

Permalink
Add support for help end IPython escape commands (#6358)
Browse files Browse the repository at this point in the history
## Summary

This PR adds support for a stricter version of help end escape
commands[^1] in the parser. By stricter, I mean that the escape tokens
are only at the end of the command and there are no tokens at the start.
This makes it difficult to implement it in the lexer without having to
do a lot of look aheads or keeping track of previous tokens.

Now, as we're adding this in the parser, the lexer needs to recognize
and emit a new token for `?`. So, `Question` token is added which will
be recognized only in `Jupyter` mode.

The conditions applied are the same as the ones in the original
implementation in IPython codebase (which is a regex):
* There can only be either 1 or 2 question mark(s) at the end
* The node before the question mark can be a `Name`, `Attribute`,
`Subscript` (only with integer constants in slice position), or any
combination of the 3 nodes.

## Test Plan

Added test cases for various combination of the possible nodes in the
command value position and update the snapshots.

fixes: #6359
fixes: #5030 (This is the final piece)

[^1]: #6272 (comment)
  • Loading branch information
dhruvmanila committed Aug 9, 2023
1 parent 887a47c commit e257c5a
Show file tree
Hide file tree
Showing 6 changed files with 20,449 additions and 14,961 deletions.
3 changes: 3 additions & 0 deletions crates/ruff_python_parser/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,9 @@ impl<'source> Lexer<'source> {

self.lex_magic_command(kind)
}

'?' if self.mode == Mode::Jupyter => Tok::Question,

'/' => {
if self.cursor.eat_char('=') {
Tok::SlashEqual
Expand Down
9 changes: 9 additions & 0 deletions crates/ruff_python_parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1180,6 +1180,15 @@ foo = %foo \
% foo
foo = %foo # comment
# Help end line magics
foo?
foo.bar??
foo.bar.baz?
foo[0]??
foo[0][1]?
foo.bar[0].baz[1]??
foo.bar[0].baz[2].egg??
"#
.trim(),
Mode::Jupyter,
Expand Down
75 changes: 75 additions & 0 deletions crates/ruff_python_parser/src/python.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{
string::parse_strings,
token::{self, StringKind},
};
use lalrpop_util::ParseError;

grammar(mode: Mode);

Expand Down Expand Up @@ -89,6 +90,7 @@ SmallStatement: ast::Stmt = {
AssertStatement,
TypeAliasStatement,
LineMagicStatement,
HelpEndLineMagic,
};

PassStatement: ast::Stmt = {
Expand Down Expand Up @@ -366,6 +368,78 @@ LineMagicExpr: ast::Expr = {
}
}

HelpEndLineMagic: ast::Stmt = {
// We are permissive than the original implementation because we would allow whitespace
// between the expression and the suffix while the IPython implementation doesn't allow it.
// For example, `foo ?` would be valid in our case but invalid from IPython.
<location:@L> <e:Expression<"All">> <suffix:("?")+> <end_location:@R> =>? {
fn unparse_expr(expr: &ast::Expr, buffer: &mut String) -> Result<(), LexicalError> {
match expr {
ast::Expr::Name(ast::ExprName { id, .. }) => {
buffer.push_str(id.as_str());
},
ast::Expr::Subscript(ast::ExprSubscript { value, slice, range, .. }) => {
let ast::Expr::Constant(ast::ExprConstant { value: ast::Constant::Int(integer), .. }) = slice.as_ref() else {
return Err(LexicalError {
error: LexicalErrorType::OtherError("only integer constants are allowed in Subscript expressions in help end escape command".to_string()),
location: range.start(),
});
};
unparse_expr(value, buffer)?;
buffer.push('[');
buffer.push_str(&format!("{}", integer));
buffer.push(']');
},
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
unparse_expr(value, buffer)?;
buffer.push('.');
buffer.push_str(attr.as_str());
},
_ => {
return Err(LexicalError {
error: LexicalErrorType::OtherError("only Name, Subscript and Attribute expressions are allowed in help end escape command".to_string()),
location: expr.range().start(),
});
}
}
Ok(())
}

if mode != Mode::Jupyter {
return Err(ParseError::User {
error: LexicalError {
error: LexicalErrorType::OtherError("IPython escape commands are only allowed in Jupyter mode".to_string()),
location,
},
});
}

let kind = match suffix.len() {
1 => MagicKind::Help,
2 => MagicKind::Help2,
_ => {
return Err(ParseError::User {
error: LexicalError {
error: LexicalErrorType::OtherError("maximum of 2 `?` tokens are allowed in help end escape command".to_string()),
location,
},
});
}
};

let mut value = String::new();
unparse_expr(&e, &mut value)?;

Ok(ast::Stmt::LineMagic(
ast::StmtLineMagic {
kind,
value,
range: (location..end_location).into()
}
))
}
}

CompoundStatement: ast::Stmt = {
MatchStatement,
IfStatement,
Expand Down Expand Up @@ -1732,6 +1806,7 @@ extern {
Dedent => token::Tok::Dedent,
StartModule => token::Tok::StartModule,
StartExpression => token::Tok::StartExpression,
"?" => token::Tok::Question,
"+" => token::Tok::Plus,
"-" => token::Tok::Minus,
"~" => token::Tok::Tilde,
Expand Down
Loading

0 comments on commit e257c5a

Please sign in to comment.