diff --git a/environment-dev.yml b/environment-dev.yml index 6c29490e..dd1d4f63 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -14,7 +14,7 @@ dependencies: - pybind11>=2.6.1,<3.0 - pybind11_json>=0.2.6,<0.3 - xeus-python-shell>=0.5.0,<0.6 - - debugpy + - debugpy>=1.6.5 # Test dependencies - pytest - nbval diff --git a/environment.yml b/environment.yml index c0aa1c59..7a9d46cd 100644 --- a/environment.yml +++ b/environment.yml @@ -9,3 +9,4 @@ dependencies: - itkwidgets - matplotlib - ipympl + - debugpy>=1.6.5 diff --git a/include/xeus-python/xdebugger.hpp b/include/xeus-python/xdebugger.hpp index 6acf8f71..1b06fbab 100644 --- a/include/xeus-python/xdebugger.hpp +++ b/include/xeus-python/xdebugger.hpp @@ -54,6 +54,7 @@ namespace xpyt nl::json rich_inspect_variables_request(const nl::json& message); nl::json attach_request(const nl::json& message); nl::json configuration_done_request(const nl::json& message); + nl::json copy_to_globals_request(const nl::json& message); nl::json variables_request_impl(const nl::json& message) override; @@ -70,6 +71,7 @@ namespace xpyt std::string m_debugpy_port; nl::json m_debugger_config; py::object m_pydebugger; + bool m_copy_to_globals_available; }; XEUS_PYTHON_API diff --git a/include/xeus-python/xutils.hpp b/include/xeus-python/xutils.hpp index 7342c4d3..a4092cdc 100644 --- a/include/xeus-python/xutils.hpp +++ b/include/xeus-python/xutils.hpp @@ -67,6 +67,9 @@ namespace xpyt XEUS_PYTHON_API void print_pythonhome(); + + XEUS_PYTHON_API + bool less_than_version(std::string a, std::string b); } #endif diff --git a/src/xdebugger.cpp b/src/xdebugger.cpp index 23b4f2f1..af3c86df 100644 --- a/src/xdebugger.cpp +++ b/src/xdebugger.cpp @@ -65,6 +65,7 @@ namespace xpyt register_request_handler("richInspectVariables", std::bind(&debugger::rich_inspect_variables_request, this, _1), false); register_request_handler("attach", std::bind(&debugger::attach_request, this, _1), true); register_request_handler("configurationDone", std::bind(&debugger::configuration_done_request, this, _1), true); + register_request_handler("copyToGlobals", std::bind(&debugger::copy_to_globals_request, this, _1), true); } debugger::~debugger() @@ -191,6 +192,41 @@ namespace xpyt return reply; } + nl::json debugger::copy_to_globals_request(const nl::json& message) + { + // This request cannot be processed if the version of debugpy is lower than 1.6.5. + if (m_copy_to_globals_available) + { + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", false}, + {"command", message["command"]}, + {"body", "The debugpy version must be greater than or equal 1.6.5 to allow copying a variable to the global scope."} + }; + return reply; + } + + std::string src_var_name = message["arguments"]["srcVariableName"].get(); + std::string dst_var_name = message["arguments"]["dstVariableName"].get(); + int src_frame_id = message["arguments"]["srcFrameId"].get(); + + // It basically runs a setExpression in the globals dictionary of Python. + int seq = message["seq"].get(); + std::string expression = "globals()['" + dst_var_name + "']"; + nl::json request = { + {"type", "request"}, + {"command", "setExpression"}, + {"seq", seq+1}, + {"arguments", { + {"expression", expression}, + {"value", src_var_name}, + {"frameId", src_frame_id} + }} + }; + return forward_message(request); + } + nl::json debugger::variables_request_impl(const nl::json& message) { if (base_type::get_stopped_threads().empty()) @@ -250,6 +286,21 @@ namespace xpyt py::gil_scoped_acquire acquire; py::module xeus_python_shell = py::module::import("xeus_python_shell.debugger"); m_pydebugger = xeus_python_shell.attr("XDebugger")(); + + // Get debugpy version + std::string expression = "debugpy.__version__"; + std::string version = (eval(py::str(expression))).cast(); + + // Format the version to match [0-9]+(\s[0-9]+)* + size_t pos = version.find_first_of("abrc"); + if (pos != std::string::npos ) + { + version.erase(pos, version.length() - pos); + } + std::replace(version.begin(), version.end(), '.', ' '); + + // Check if the copy_to_globals feature is available + m_copy_to_globals_available = less_than_version(version, "1 6 5"); } return status == "ok"; } diff --git a/src/xutils.cpp b/src/xutils.cpp index 969f1059..1b7d6207 100644 --- a/src/xutils.cpp +++ b/src/xutils.cpp @@ -149,4 +149,18 @@ namespace xpyt std::clog << "PYTHONHOME set to " << mbstr << std::endl; } + + // Compares 2 versions and return true if version1 < version2. + // The versions must be strings formatted as the regex: [0-9]+(\s[0-9]+)* + bool less_than_version(std::string version1, std::string version2) + { + using iterator_type = std::istream_iterator; + std::istringstream iv1(version1), iv2(version2); + return std::lexicographical_compare( + iterator_type(iv1), + iterator_type(), + iterator_type(iv2), + iterator_type() + ); + } } diff --git a/test/test_debugger.cpp b/test/test_debugger.cpp index e7028b76..836d1781 100644 --- a/test/test_debugger.cpp +++ b/test/test_debugger.cpp @@ -336,6 +336,21 @@ nl::json make_rich_inspect_variables_request(int seq, const std::string& var_nam return req; } +nl::json make_copy_to_globals_request(int seq, const std::string& src_var_name, const std::string& dst_var_name, int src_frame_id) +{ + nl::json req = { + {"type", "request"}, + {"seq", seq}, + {"command", "copyToGlobals"}, + {"arguments", { + {"srcVariableName", src_var_name}, + {"dstVariableName", dst_var_name}, + {"srcFrameId", src_frame_id} + }} + }; + return req; +} + nl::json make_exception_breakpoint_request(int seq) { nl::json except_option = { @@ -384,6 +399,7 @@ class debugger_client bool test_inspect_variables(); bool test_rich_inspect_variables(); bool test_variables(); + bool test_copy_to_globals(); void shutdown(); private: @@ -867,6 +883,88 @@ bool debugger_client::test_variables() return res; } +bool debugger_client::test_copy_to_globals() +{ + std::string local_var_name = "var"; + std::string global_var_name = "var_copy"; + std::string code = "from IPython.core.display import HTML\ndef my_test():\n\t" + local_var_name + " = HTML(\"

test content

\")\n\tpass\nmy_test()"; + int seq = 12; + + // Init debugger and set breakpoint + attach(); + m_client.send_on_control("debug_request", make_dump_cell_request(seq, code)); + ++seq; + nl::json dump_res = m_client.receive_on_control(); + std::string path = dump_res["content"]["body"]["sourcePath"].get(); + m_client.send_on_control("debug_request", make_breakpoint_request(seq, path, 4)); + ++seq; + m_client.receive_on_control(); + + // Execute code + m_client.send_on_shell("execute_request", make_execute_request(code)); + + // Get breakpoint event + nl::json ev = m_client.wait_for_debug_event("stopped"); + m_client.send_on_control("debug_request", make_stacktrace_request(seq, 1)); + ++seq; + nl::json json1 = m_client.receive_on_control(); + if(json1["content"]["body"]["stackFrames"].empty()) + { + m_client.send_on_control("debug_request", make_stacktrace_request(seq, 1)); + ++seq; + json1 = m_client.receive_on_control(); + } + + // Get local frame id + int frame_id = json1["content"]["body"]["stackFrames"][0]["id"].get(); + + // Copy the variable + m_client.send_on_control("debug_request", make_copy_to_globals_request(seq, local_var_name, global_var_name, frame_id)); + ++seq; + + // Get the scopes + nl::json reply = m_client.receive_on_control(); + m_client.send_on_control("debug_request", make_scopes_request(seq, frame_id)); + ++seq; + nl::json json2 = m_client.receive_on_control(); + + // Get the local variable + int variable_ref_local = json2["content"]["body"]["scopes"][0]["variablesReference"].get(); + m_client.send_on_control("debug_request", make_variables_request(seq, variable_ref_local)); + ++seq; + nl::json json3 = m_client.receive_on_control(); + nl::json local_var = {}; + for (auto &var: json3["content"]["body"]["variables"]){ + if (var["evaluateName"] == local_var_name) { + local_var = var; + } + } + if (!local_var.contains("evaluateName")) { + std::cout << "Local variable \"" + local_var_name + "\" not found"; + return false; + } + + // Get the global variable (copy of the local variable) + nl::json global_scope = json2["content"]["body"]["scopes"].back(); + int variable_ref_global = global_scope["variablesReference"].get(); + m_client.send_on_control("debug_request", make_variables_request(seq, variable_ref_global)); + ++seq; + nl::json json4 = m_client.receive_on_control(); + nl::json global_var = {}; + for (auto &var: json4["content"]["body"]["variables"]){ + if (var["evaluateName"] == global_var_name) { + global_var = var; + } + } + if (!global_var.contains("evaluateName")) { + std::cout << "Global variable \"" + global_var_name + "\" not found"; + return false; + } + + // Compare local and global variable + return global_var["value"] == local_var["value"] && global_var["type"] == local_var["type"]; +} + void debugger_client::shutdown() { m_client.send_on_control("shutdown_request", make_shutdown_request()); @@ -1284,4 +1382,19 @@ TEST_SUITE("debugger") t.notify_done(); } } + + TEST_CASE("copy_to_globals") + { + start_kernel(); + timer t; + zmq::context_t context; + { + debugger_client deb(context, KERNEL_JSON, "debugger_copy_to_globals.log"); + bool res = deb.test_copy_to_globals(); + deb.shutdown(); + std::this_thread::sleep_for(2s); + CHECK(res); + t.notify_done(); + } + } }