Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion environment-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ dependencies:
- itkwidgets
- matplotlib
- ipympl
- debugpy>=1.6.5
2 changes: 2 additions & 0 deletions include/xeus-python/xdebugger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions include/xeus-python/xutils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
51 changes: 51 additions & 0 deletions src/xdebugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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>();
std::string dst_var_name = message["arguments"]["dstVariableName"].get<std::string>();
int src_frame_id = message["arguments"]["srcFrameId"].get<int>();

// It basically runs a setExpression in the globals dictionary of Python.
int seq = message["seq"].get<int>();
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())
Expand Down Expand Up @@ -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<std::string>();

// 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";
}
Expand Down
14 changes: 14 additions & 0 deletions src/xutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Copy link
Member

Choose a reason for hiding this comment

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

If we assume that version1 and version2 have the format "MAJOR MINOR PATCH", then the implementation of this function can be improved:

using iterator_type = std::istream_iterator<int>;
std::istringstream iv1(version1), iv2(version2);
return std::lexicographical_compare(
    iterator_type(iv1),
    iterator_type(),
    iterator_type(iv2),
    iterator_type()
);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @JohanMabille.
Not sure we can assume it as users can still have alpha/beta versions I thinks.

Copy link
Member

@JohanMabille JohanMabille Jan 19, 2023

Choose a reason for hiding this comment

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

In that case we need to split the string to remove the alpha / beta part.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, let's do it this way

using iterator_type = std::istream_iterator<int>;
std::istringstream iv1(version1), iv2(version2);
return std::lexicographical_compare(
iterator_type(iv1),
iterator_type(),
iterator_type(iv2),
iterator_type()
);
}
}
113 changes: 113 additions & 0 deletions test/test_debugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(\"<p>test content</p>\")\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<std::string>();
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<int>();

// 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<int>();
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<int>();
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());
Expand Down Expand Up @@ -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();
}
}
}