mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-38378 PY-38424 Fix updating variables in debugger console
We now access the `frame.f_locals` attribute only once. Multiple access leads to the loss of the changes we have made. We also alter the second argument of the `PyFrame_LocalsToFast()` to 1 to ensure that all the deleted variables will be wiped out from the frame locals. (cherry picked from commit 28abff39b2c37f30651db220e8700032a629ce73) GitOrigin-RevId: 8514da7ec187c1192ea9cdf851f89f008242fc30
This commit is contained in:
committed by
intellij-monorepo-bot
parent
40ac8526c3
commit
eb956ca29d
@@ -1,4 +1,4 @@
|
||||
from _pydevd_bundle.pydevd_constants import dict_keys
|
||||
from _pydevd_bundle.pydevd_constants import dict_keys, dict_iter_items
|
||||
|
||||
try:
|
||||
from code import InteractiveConsole
|
||||
@@ -229,14 +229,18 @@ def console_exec(thread_id, frame_id, expression, dbg):
|
||||
return False
|
||||
|
||||
|
||||
def update_frame_local_variables_and_save(frame, var_dict):
|
||||
"""Update the frame local variables with the values from `var_dict`, remove those that no longer exist, and save."""
|
||||
for var_name in var_dict:
|
||||
if var_name not in frame.f_globals:
|
||||
frame.f_locals[var_name] = var_dict[var_name]
|
||||
elif var_dict[var_name] is not frame.f_globals[var_name]:
|
||||
frame.f_locals[var_name] = var_dict[var_name]
|
||||
for var_name in frame.f_locals:
|
||||
if var_name not in var_dict:
|
||||
frame.f_locals.pop(var_name)
|
||||
def update_frame_local_variables_and_save(frame, values):
|
||||
"""Update the frame local variables with the values from `values`, remove those that no longer exist, and save."""
|
||||
# It is important to access to `frame.f_locals` through a local reference.
|
||||
# Any changes done to it will be lost as soon as we access the attribute next time.
|
||||
f_locals, f_globals = frame.f_locals, frame.f_globals
|
||||
|
||||
for key, value in dict_iter_items(values):
|
||||
if key not in f_globals or value is not f_globals[key]:
|
||||
f_locals[key] = value
|
||||
|
||||
for key in dict_keys(f_locals):
|
||||
if key not in values:
|
||||
f_locals.pop(key)
|
||||
|
||||
pydevd_save_locals.save_locals(frame)
|
||||
|
||||
@@ -58,7 +58,7 @@ def make_save_locals_impl():
|
||||
pass
|
||||
else:
|
||||
def save_locals_ctypes_impl(frame):
|
||||
locals_to_fast(ctypes.py_object(frame), ctypes.c_int(0))
|
||||
locals_to_fast(ctypes.py_object(frame), ctypes.c_int(1))
|
||||
|
||||
return save_locals_ctypes_impl
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class C:
|
||||
@staticmethod
|
||||
def f():
|
||||
|
||||
def g():
|
||||
return pd.Series(index=[40, 50, 60], data=[1, 2, 3])
|
||||
|
||||
a = g()
|
||||
a # breakpoint
|
||||
|
||||
|
||||
C.f()
|
||||
@@ -418,8 +418,14 @@ public abstract class PyBaseDebuggerTask extends PyExecutionFixtureTestTask {
|
||||
return result.first;
|
||||
}
|
||||
|
||||
protected void consoleExec(String command) throws PyDebuggerException {
|
||||
// We can't wait for result with a callback, because console just prints it to output
|
||||
/**
|
||||
* Run a command in the debugger console without waiting for the result.
|
||||
*
|
||||
* @param command to run.
|
||||
*
|
||||
* @see #consoleExecAndWait(String)
|
||||
*/
|
||||
protected void consoleExec(String command) {
|
||||
myDebugProcess.consoleExec(command, new PyDebugCallback<String>() {
|
||||
@Override
|
||||
public void ok(String value) {
|
||||
@@ -431,7 +437,30 @@ public abstract class PyBaseDebuggerTask extends PyExecutionFixtureTestTask {
|
||||
});
|
||||
}
|
||||
|
||||
protected Variable eval(String name) throws InterruptedException {
|
||||
/**
|
||||
* Run a command in the debugger console and wait until it is executed. It raises the assertion error if the command
|
||||
* hasn't finished withing {@link XDebuggerTestUtil#TIMEOUT_MS} milliseconds. It doesn't matter if the command itself
|
||||
* has finished successfully or failed (e.g. incorrect commands can be used in tests in purpose).
|
||||
*
|
||||
* @param command to run.
|
||||
*/
|
||||
protected void consoleExecAndWait(String command) {
|
||||
EvaluationCallback<String> callback = new EvaluationCallback<>();
|
||||
myDebugProcess.consoleExec(command, new PyDebugCallback<String>() {
|
||||
@Override
|
||||
public void ok(String value) {
|
||||
callback.evaluated(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(PyDebuggerException exception) {
|
||||
callback.errorOccurred(exception.getMessage());
|
||||
}
|
||||
});
|
||||
callback.waitFor(XDebuggerTestUtil.TIMEOUT_MS);
|
||||
}
|
||||
|
||||
protected Variable eval(String name) {
|
||||
Assert.assertTrue("Eval works only while suspended", mySession.isSuspended());
|
||||
XValue var = XDebuggerTestUtil.evaluate(mySession, name).first;
|
||||
Assert.assertNotNull("There is no variable named " + name, var);
|
||||
|
||||
@@ -289,4 +289,28 @@ public class PyDebuggerTask extends PyBaseDebuggerTask {
|
||||
myDebugProcess.getProcessHandler().destroyProcess();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles breakpoint in the script returned by {@link PyDebuggerTask#getScriptName()}.
|
||||
*
|
||||
* @param line starting with 0
|
||||
*/
|
||||
protected void toggleBreakpoint(int line) {
|
||||
toggleBreakpoint(getFilePath(getScriptName()), line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles multiple breakpoints with {@link PyDebuggerTask#toggleBreakpoint(int)}.
|
||||
*/
|
||||
protected void toggleBreakpoints(int... lines) {
|
||||
toggleBreakpoints(getFilePath(getScriptName()), lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles multiple breakpoints with {@link PyDebuggerTask#toggleBreakpoint(String, int)}.
|
||||
*/
|
||||
protected void toggleBreakpoints(@NotNull String file, int... lines) {
|
||||
for(int line : lines)
|
||||
toggleBreakpoint(file, line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.jetbrains.env.python.debug;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.jetbrains.env.PyEnvTestCase;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class PythonDebugConsoleTest extends PyEnvTestCase {
|
||||
|
||||
@Test
|
||||
public void testDebugConsoleExecAndOutput() {
|
||||
runPythonTest(new PyDebuggerTask("/debug", "test1.py") {
|
||||
@Override
|
||||
public void before() {
|
||||
toggleBreakpoint(getFilePath(getScriptName()), 3);
|
||||
setWaitForTermination(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testing() throws Exception {
|
||||
waitForPause();
|
||||
eval("i").hasValue("0");
|
||||
resume();
|
||||
waitForPause();
|
||||
consoleExec("'i=%d'%i");
|
||||
waitForOutput("'i=1'");
|
||||
consoleExec("x");
|
||||
waitForOutput("name 'x' is not defined");
|
||||
consoleExec("1-;");
|
||||
waitForOutput("SyntaxError");
|
||||
resume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PY-38378
|
||||
@Test
|
||||
public void testUpdateVariableInDebugConsole() {
|
||||
runPythonTest(new PyDebuggerTask("/debug", "test2.py") {
|
||||
@Override
|
||||
public void before() {
|
||||
toggleBreakpoints(5, 7);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testing() throws Exception {
|
||||
waitForPause();
|
||||
consoleExecAndWait("z = 42");
|
||||
resume();
|
||||
waitForPause();
|
||||
eval("z").hasValue("43");
|
||||
resume();
|
||||
waitForTerminate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PY-38424
|
||||
@Test
|
||||
public void testUpdateSeriesInDebugConsole() {
|
||||
runPythonTest(new PyDebuggerTask("/debug", "test_update_series_in_debug_console.py") {
|
||||
@Override
|
||||
public void before() throws Exception {
|
||||
toggleBreakpoint(11);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testing() throws Exception {
|
||||
waitForPause();
|
||||
eval("str(a[40])").hasValue("'1'");
|
||||
consoleExecAndWait("a[40] = 100");
|
||||
eval("str(a[40])").hasValue("'100'");
|
||||
consoleExecAndWait(" def g():\n" +
|
||||
" return pd.Series(index=[40,50,60], data=[1,2,3])");
|
||||
consoleExecAndWait("a = g()");
|
||||
eval("str(a[40])").hasValue("'1'");
|
||||
resume();
|
||||
waitForTerminate();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<String> getTags() {
|
||||
return ImmutableSet.of("pandas");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -108,42 +108,6 @@ public class PythonDebuggerTest extends PyEnvTestCase {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDebugConsole() {
|
||||
runPythonTest(new PyDebuggerTask("/debug", "test1.py") {
|
||||
@Override
|
||||
public void before() {
|
||||
toggleBreakpoint(getFilePath(getScriptName()), 3);
|
||||
setWaitForTermination(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testing() throws Exception {
|
||||
waitForPause();
|
||||
|
||||
eval("i").hasValue("0");
|
||||
|
||||
resume();
|
||||
|
||||
waitForPause();
|
||||
|
||||
consoleExec("'i=%d'%i");
|
||||
|
||||
waitForOutput("'i=1'");
|
||||
|
||||
consoleExec("x");
|
||||
|
||||
waitForOutput("name 'x' is not defined");
|
||||
|
||||
consoleExec("1-;");
|
||||
|
||||
waitForOutput("SyntaxError");
|
||||
|
||||
resume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDebugCompletion() {
|
||||
runPythonTest(new PyDebuggerTask("/debug", "test4.py") {
|
||||
|
||||
Reference in New Issue
Block a user