Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make "Run line" to work on partial statements (e.g. code blocks, loops, etc.) #4431

Open
ghost opened this issue May 5, 2017 · 8 comments · May be fixed by #21557
Open

Make "Run line" to work on partial statements (e.g. code blocks, loops, etc.) #4431

ghost opened this issue May 5, 2017 · 8 comments · May be fixed by #21557

Comments

@ghost
Copy link

ghost commented May 5, 2017

Feature request

In RStudio, given a function with arguments spread out over multiple lines, e.g.:

data.frame(a = c(1, 2, 3),
           b = c(4, 5, 6),
           row.names = c('x', 'y', 'z'))

The use of "run current line" (using shortcut or menu) is less literal in RStudio than in Spyder, as the entire function is run rather than just isolated lines, returning the desired data frame. This works in a number of cases (loops, functions, dplyr/tidyr pipelines, etc.) and can be quite a time-saver, since you don't have to worry about highlighting longish blocks of code to run them.

Similar functionality is possible in Spyder using cells, but I think cells are a better tool for segmenting code according to logical steps in the coding/analysis process, and not so much for isolating individual functions and loops. Would it be possible to implement this feature in some future Spyder version?

@goanpeca
Copy link
Member

goanpeca commented May 5, 2017

@GershTri this is a good feature to have, @ccordoba12, what do you think?

@ccordoba12 ccordoba12 changed the title feature request => "run line" that recognizes code blocks, loops, etc. Make "Run line" to work on partial statements (e.g. code blocks, loops, etc.) May 5, 2017
@ccordoba12 ccordoba12 added this to the v4.0beta2 milestone May 5, 2017
@ccordoba12
Copy link
Member

ccordoba12 commented May 5, 2017

This will require restructuring the code we use now to execute a line, which is very simple:

  1. Strip a line from all blank spaces
  2. Execute the result in a console.

So step 1. won't be needed anymore and step 2. needs to be changed to "press Enter in the console" or something like that.

I know RStudio users are fans of this feature, but it's not easy to implement it correctly in Spyder (at least not as easy as what we have now). So I'm marking this for Spyder 4, but I can't make promises that this will be available in the final release.

@ccordoba12 ccordoba12 modified the milestones: v4.0beta2, v4.0beta3 Aug 13, 2017
@ccordoba12 ccordoba12 modified the milestones: v4.0betaX, Not sorted Oct 11, 2018
@ccordoba12 ccordoba12 modified the milestones: not sorted, future, v4.0beta3 Apr 16, 2019
@bcolsen
Copy link
Member

bcolsen commented Apr 16, 2019

My idea would be just to run the whole indented level of code when you are at the start. For example:

def foo(bars):
    print(bars)
    for bar in bars:
        if bar == 2:
            print(bar)
        else:
            print(bar+1)
bars2 = [23,
         24,
         25]

Cursor on line 1 would run the whole def.
Cursor on line 2 would run only the print.
Cursor on line 3 would run the for loop.
Cursor on line 8 would define the list bars.

Is it too much to assume that the user would want to run that much code if they are on these lines?

Some problems:

  1. Cursor on line 4 would run the only the if and not the else.
    • it would be good if it ran the else
  2. Cursor on line 6 would error on the else.
    • it would be great if it ran the whole if else statement
  3. Cursor on line 10 would error on the bracket.
    • it would be great if this defined the list bars

Are these safe assumptions? How many more corner cases would there be? @CAM-Gerlach Do you know what RStudio does?

@ccordoba12 said:

IPython has code to determine if a line is a complete Python statement or not
So we should use that to decide when to execute and when to add a continuation prompt

Are you saying we use IPython to filter non-runnable code as a solution or to use this on corner cases?

@CAM-Gerlach
Copy link
Member

Do you know what RStudio does?

Rstudio runs anything that's a continuation of the current line, forward or backward. However, in R, you explicitly need braces ({) around if/else that isn't inside some other enclosing braces, like a function, so they are considered one statement.

So, for example, putting the cursor on the first or last line of the following would run the full if statement, while putting it on the center line would just run the print("test"):

if (TRUE) {
    print("test")
}

However, if we had the following, putting the cursor on the if line would not work and either of the braces lines would just run print("test") without checking the if:

if (TRUE)
{
    print("test")
}

With if/else, it will run both if you put your cursor on any line with a brace (and you must put the else on the same line as the if's }. in R, if your if/else isn't surrounded by other braces, like a function definition):

if (FALSE) {
    print("True")
} else {
    print("False")
}

Cursor on line 10 would error on the bracket. It would be great if this defined the list bars

This is the key motivating use case for this feature both IMO and as presented in Rstudio when it was introduced; the ability to run indented levels is decidedly secondary. The most common use case where users need something like this is after entering or modifying a multi-line statement, in which case they want to run the whole thing. If we did it by indent level, running a continuation would only work if the cursor is on the first line, which is a decided minority of cases.

So we should use that to decide when to execute and when to add a continuation prompt

This is for the Editor, not the Console, so I'm not sure what you talking about with "add a continuation prompt". Furthermore, it is not necessarily a robust solution to finding the boundaries of a continuation statement, if that's what you're suggesting. Consider:

spam = 42 * (
    2 + 2
    )

2 + 2 is a complete Python statement by itself, but is part of continuation line and thus should be executed together.

@bcolsen
Copy link
Member

bcolsen commented Apr 16, 2019

I guess one simple thing would be to look at bracket matching. If the line you are on has an unmatched bracket move down until you find the match. I guess this is what the python parser does.

This isn't that general but it is really easy to do and would capture most use cases.

@CAM-Gerlach
Copy link
Member

Have we thought about using the standard library ast module to do our work for us in terms of syntax parsing? Or is it too heavy-duty for what we need?

@ccordoba12
Copy link
Member

ccordoba12 commented Apr 17, 2019

Are you saying we use IPython to filter non-runnable code as a solution or to use this on corner cases?

I'm saying that we should use what IPython already has to decide when a line is a complete Python statement or not. Specifically, IPython has the following machinery:

https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.inputsplitter.html

Although that's deprecated, it's still compatible with Python 2 and 3.

So we can use this method:

https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.inputsplitter.html#IPython.core.inputsplitter.InputSplitter.check_complete

before trying to execute a a line with F9. If it returns True, then we call directly the execute method of ipythonconsole/widgets/shell.py (which is what we're doing right now). If not, we should simply paste the text and call its _set_continuation_prompt method:

https://github.com/jupyter/qtconsole/blob/4545df79fd961c22b68f3e9b9e19a0fde5df0259/qtconsole/console_widget.py#L2288

I guess there would arise some complications along the way, but that's the main idea.

@CAM-Gerlach
Copy link
Member

Although that's deprecated, it's still compatible with Python 2 and 3.

But what if IPython 8 removes it, breaking Spyder? If we're going to do this, at the very least, we should check for inputtransformer2 and use its check_complete() method instead, and only if not found fall back to the deprecated inputsplitter. We'll need to do so anyway eventually, so may as well be prepared and correct from the get-go and do it now.

If it returns True

Which it will in cases like the above where the line itself happens to be a complete statement, but that is part of continuations. Ultimately, its issues here stem from being designed to parse line-by-line, sequential Python input, rather than pre-written Python code in a file, where the originally selected line may be at any point in the statement the user wants to evaluate.,

If not, we should simply paste the text and call its _set_continuation_prompt method:

This is not really a useful solution to the problem presented here, either as framed by the original reporter, @bcolsen , myself or as in Rstudio. The only scenario where it is of any help is if the user happens to be on the first line of a continuation (which, as I describe above, is a minority of instances) and then sequentially presses the Run Line/Selection shortcut on each line in turn. That's hardly any time save over just selecting it with the mouse or keyboard and pressing Run Line/Selection once, and it only works in a minority of cases. Furthermore, if the user is on any line but the first, it will not work at all and could even trap the user into a continuation prompt they need to escape.

Instead, what the requested feature here is when Run Line is pressed (without a selection), properly detecting all lines in the current statement including continuations, and actually running those lines directly; optionally, this could be extended to all lines in the current indent scope if the cursor is on a statement that ends in :, but the core functionality (IMO) is running logical, not physical lines.

Other than writing our own logic to parse what is part of a continuation line, unless we already have something suitable, we could do something like the following (which should properly handle all of the cases discussed):

  1. If the current line is a cell marker, run the cell
  2. If the current line is a blank line or a comment, don't run it and go to the next line.
  3. Otherwise, parse the current cell (or file, if no cells are defined) with astroid.parse() (astroid is already a Spyder dependency, through pylint, etc) and recursively iterate through the AST to find the lowest-level body node that contains the line Run line is called on between fromline and toline, and run that.

Notes:

  • If and TryFinally statements have one root node, with orelse pointing to the next if statement (for elif) or the expr to run (for else); you'd want to iterate through them as well in turn and run the bounds of the highest-level statement in the orelse chain if the lowest level cursor_line is included in is an elif (i.e. a non-root if).

  • For TryFinally, you'd need to also check finalbody, and for TryExcept (which may be nested inside a TryFinally), iterate through the body of each of the handlers for the except statements. For any of these, if the lowest level cursor_line is included in is a ExceptionHandler (i.e. the cursor is on an except: line), you'll want to run the enclosing TryExcept, or the enclosing TryFinally if the TryExcept is immediately inside one.

  • You would make sure to include a try-except around at least the AST part, so just in case something goes wrong it falls back to just trying to run the current line. And, of course, this feature would be on by default but optional (like it is in Rstudio).

@ccordoba12 ccordoba12 modified the milestones: v4.0beta3, v4.0beta4 May 18, 2019
@ccordoba12 ccordoba12 modified the milestones: v4.0betaX, future Aug 10, 2019
@ccordoba12 ccordoba12 modified the milestones: future, v4.2.0 Feb 1, 2020
@ccordoba12 ccordoba12 modified the milestones: v5.3.3, v5.3.4 Aug 17, 2022
@ccordoba12 ccordoba12 modified the milestones: v5.4.0, v5.4.1 Oct 5, 2022
@ccordoba12 ccordoba12 modified the milestones: v5.4.1, v5.4.2 Dec 21, 2022
@ccordoba12 ccordoba12 modified the milestones: v5.4.2, v5.4.3 Jan 5, 2023
@ccordoba12 ccordoba12 modified the milestones: v5.4.3, v6.0alpha2 Jan 20, 2023
@ccordoba12 ccordoba12 modified the milestones: v6.0alpha2, v6.0alpha3 Jun 8, 2023
@ccordoba12 ccordoba12 self-assigned this Aug 21, 2023
@ccordoba12 ccordoba12 modified the milestones: v6.0alpha3, v6.0beta1 Dec 1, 2023
@dalthviz dalthviz mentioned this issue Dec 13, 2023
10 tasks
@ccordoba12 ccordoba12 modified the milestones: v6.0alpha4, v6.0beta1 Feb 6, 2024
@ccordoba12 ccordoba12 modified the milestones: v6.0alpha5, v6.0beta1 Mar 12, 2024
@ccordoba12 ccordoba12 modified the milestones: v6.0beta1, v6.1.0 May 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment