[pycharm] PY-79461 PY-79531 Debugger: load data in chunks

(cherry picked from commit 0e557f905cfafca704266d3a4cbacfe60820cd57)

GitOrigin-RevId: 6cb9d292e0b2d246c364c9442899509f3d35fbb1
This commit is contained in:
ekaterina.itsenko
2025-04-17 23:09:15 +02:00
committed by intellij-monorepo-bot
parent edf3517b98
commit 7c2c005a18
39 changed files with 1862 additions and 83 deletions

View File

@@ -7,7 +7,7 @@
package com.jetbrains.python.console.protocol;
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class ArrayData implements org.apache.thrift.TBase<ArrayData, ArrayData._Fields>, java.io.Serializable, Cloneable, Comparable<ArrayData> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("ArrayData");

View File

@@ -7,7 +7,7 @@
package com.jetbrains.python.console.protocol;
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class ArrayHeaders implements org.apache.thrift.TBase<ArrayHeaders, ArrayHeaders._Fields>, java.io.Serializable, Cloneable, Comparable<ArrayHeaders> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("ArrayHeaders");

View File

@@ -7,7 +7,7 @@
package com.jetbrains.python.console.protocol;
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class ColHeader implements org.apache.thrift.TBase<ColHeader, ColHeader._Fields>, java.io.Serializable, Cloneable, Comparable<ColHeader> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("ColHeader");

View File

@@ -7,7 +7,7 @@
package com.jetbrains.python.console.protocol;
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class CompletionOption implements org.apache.thrift.TBase<CompletionOption, CompletionOption._Fields>, java.io.Serializable, Cloneable, Comparable<CompletionOption> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("CompletionOption");

View File

@@ -10,7 +10,7 @@ package com.jetbrains.python.console.protocol;
/**
* Corresponds to completion types declared in "_pydev_bundle/_pydev_imports_tipper.py".
*/
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public enum CompletionOptionType implements org.apache.thrift.TEnum {
IMPORT(0),
CLASS(1),

View File

@@ -10,7 +10,7 @@ package com.jetbrains.python.console.protocol;
* Corresponds to `PyDebugValue`.
*/
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class DebugValue implements org.apache.thrift.TBase<DebugValue, DebugValue._Fields>, java.io.Serializable, Cloneable, Comparable<DebugValue> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("DebugValue");

View File

@@ -11,7 +11,7 @@ package com.jetbrains.python.console.protocol;
*
*/
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class ExceedingArrayDimensionsException extends org.apache.thrift.TException implements org.apache.thrift.TBase<ExceedingArrayDimensionsException, ExceedingArrayDimensionsException._Fields>, java.io.Serializable, Cloneable, Comparable<ExceedingArrayDimensionsException> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("ExceedingArrayDimensionsException");

View File

@@ -11,7 +11,7 @@ package com.jetbrains.python.console.protocol;
*
*/
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class GetArrayResponse implements org.apache.thrift.TBase<GetArrayResponse, GetArrayResponse._Fields>, java.io.Serializable, Cloneable, Comparable<GetArrayResponse> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("GetArrayResponse");

View File

@@ -7,7 +7,7 @@
package com.jetbrains.python.console.protocol;
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class KeyboardInterruptException extends org.apache.thrift.TException implements org.apache.thrift.TBase<KeyboardInterruptException, KeyboardInterruptException._Fields>, java.io.Serializable, Cloneable, Comparable<KeyboardInterruptException> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("KeyboardInterruptException");

View File

@@ -6,7 +6,7 @@
*/
package com.jetbrains.python.console.protocol;
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
public class PythonConsoleFrontendService {

View File

@@ -7,7 +7,7 @@
package com.jetbrains.python.console.protocol;
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class PythonTableException extends org.apache.thrift.TException implements org.apache.thrift.TBase<PythonTableException, PythonTableException._Fields>, java.io.Serializable, Cloneable, Comparable<PythonTableException> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("PythonTableException");

View File

@@ -7,7 +7,7 @@
package com.jetbrains.python.console.protocol;
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class PythonUnhandledException extends org.apache.thrift.TException implements org.apache.thrift.TBase<PythonUnhandledException, PythonUnhandledException._Fields>, java.io.Serializable, Cloneable, Comparable<PythonUnhandledException> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("PythonUnhandledException");

View File

@@ -7,7 +7,7 @@
package com.jetbrains.python.console.protocol;
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class RowHeader implements org.apache.thrift.TBase<RowHeader, RowHeader._Fields>, java.io.Serializable, Cloneable, Comparable<RowHeader> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("RowHeader");

View File

@@ -7,7 +7,7 @@
package com.jetbrains.python.console.protocol;
@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2024-07-26")
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.20.0)", date = "2025-04-23")
public class UnsupportedArrayTypeException extends org.apache.thrift.TException implements org.apache.thrift.TBase<UnsupportedArrayTypeException, UnsupportedArrayTypeException._Fields>, java.io.Serializable, Cloneable, Comparable<UnsupportedArrayTypeException> {
private static final org.apache.thrift.protocol.TStruct STRUCT_DESC = new org.apache.thrift.protocol.TStruct("UnsupportedArrayTypeException");

View File

@@ -1,5 +1,6 @@
from _pydevd_bundle.pydevd_constants import dict_keys, NEXT_VALUE_SEPARATOR
from _pydevd_bundle.pydevd_tables import exec_table_command
from _pydevd_bundle.pydevd_tables import exec_image_table_command
from _pydevd_bundle.pydevd_user_type_renderers import parse_set_type_renderers_message
from _pydevd_bundle.pydevd_vars import resolve_compound_var_object_fields, \
table_like_struct_to_xml, eval_in_context, resolve_var_object
@@ -95,6 +96,20 @@ def table_command(command_text):
status, res = exec_table_command(command, command_type, start_index, end_index, format, namespace, namespace)
print(res)
def image_table_command(command_text):
ipython_shell = get_ipython()
namespace = ipython_shell.user_ns
command, command_type, offset, image_id = command_text.split(NEXT_VALUE_SEPARATOR)
try:
offset = int(offset)
except ValueError:
offset = None
status, res = exec_image_table_command(command, command_type, offset, image_id, namespace, namespace)
print(res)
def serializeImage(img):
if len(img.shape) != 2:
return None

View File

@@ -12,6 +12,7 @@ from _pydevd_bundle import pydevd_vars
from _pydevd_bundle.pydevd_comm import InternalDataViewerAction
from _pydevd_bundle.pydevd_constants import IS_JYTHON, dict_iter_items
from _pydevd_bundle.pydevd_tables import exec_table_command
from _pydevd_bundle.pydevd_tables import exec_image_table_command
from _pydevd_bundle.pydevd_user_type_renderers import parse_set_type_renderers_message
from pydev_console.pydev_protocol import CompletionOption, CompletionOptionType, \
PythonUnhandledException, PythonTableException
@@ -431,6 +432,23 @@ class BaseInterpreterInterface(BaseCodeExecutor):
if not success:
raise PythonTableException(str(res))
#
def execTableImageCommand(self, command, command_type, offset, image_id):
# type: (str, str, int, str) -> Any
try:
try:
offset = int(offset)
except ValueError:
offset = None
success, res = exec_image_table_command(command, command_type, offset, image_id, self.get_namespace(), self.get_namespace())
if success:
return res
except:
raise PythonUnhandledException(traceback.format_exc())
if not success:
raise PythonTableException(str(res))
def handshake(self):
if self.connect_status_queue is not None:
self.connect_status_queue.put(True)

View File

@@ -106,6 +106,7 @@ from _pydev_bundle.pydev_is_thread_alive import is_thread_alive
from _pydev_bundle import pydev_log
from _pydev_bundle import _pydev_completer
from _pydevd_bundle.pydevd_tables import exec_table_command
from _pydevd_bundle.pydevd_tables import exec_image_table_command
from pydevd_tracing import get_exception_traceback_str
from _pydevd_bundle import pydevd_console
@@ -144,7 +145,7 @@ from _pydevd_bundle.pydevd_comm_constants import (
CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION, CMD_THREAD_RESUME_SINGLE_NOTIFICATION,
CMD_REDIRECT_OUTPUT, CMD_GET_NEXT_STATEMENT_TARGETS, CMD_SET_PROJECT_ROOTS, CMD_VERSION,
CMD_RETURN, CMD_SET_PROTOCOL, CMD_ERROR, CMD_GET_SMART_STEP_INTO_VARIANTS, CMD_DATAVIEWER_ACTION,
CMD_TABLE_EXEC, CMD_INTERRUPT_DEBUG_CONSOLE, CMD_SET_USER_TYPE_RENDERERS)
CMD_TABLE_EXEC, CMD_INTERRUPT_DEBUG_CONSOLE, CMD_IMAGE_COMMAND_START_LOAD, CMD_IMAGE_COMMAND_CHUNK_LOAD, CMD_SET_USER_TYPE_RENDERERS)
MAX_IO_MSG_SIZE = 1000 #if the io is too big, we'll not send all (could make the debugger too non-responsive)
#this number can be changed if there's need to do so
@@ -1418,6 +1419,68 @@ class InternalTableCommand(InternalThreadCommand):
frame.f_globals, frame.f_locals)
#=======================================================================================================================
# DebugImageViewerAction
#=======================================================================================================================
class InternalTableImageCommandBase(InternalThreadCommand):
def __init__(self, sequence, thread_id, frame_id, init_command, command_type):
super().__init__(thread_id)
self.sequence = sequence
self.frame_id = frame_id
self.init_command = init_command
self.command_type = command_type
def do_it(self, dbg):
try:
frame = pydevd_vars.find_frame(self.thread_id, self.frame_id)
success, res = self.exec_command(frame)
if success:
cmd = NetCommand(self.get_command_id(), self.sequence, res)
dbg.writer.add_command(cmd)
else:
cmd = dbg.cmd_factory.make_error_message(self.sequence, str(res))
dbg.writer.add_command(cmd)
except Exception as e:
cmd = dbg.cmd_factory.make_error_message(self.sequence, get_exception_traceback_str())
dbg.writer.add_command(cmd)
def get_command_id(self):
raise NotImplementedError()
def exec_command(self, frame):
return exec_image_table_command(self.init_command, self.command_type,
self.get_offset(), self.get_image_id(),
frame.f_globals, frame.f_locals)
def get_offset(self):
return None
def get_image_id(self):
return None
class InternalTableImageStartCommand(InternalTableImageCommandBase):
def get_command_id(self):
return CMD_IMAGE_COMMAND_START_LOAD
class InternalTableImageChunkCommand(InternalTableImageCommandBase):
def __init__(self, sequence, thread_id, frame_id, init_command, command_type, offset, image_id):
super().__init__(sequence, thread_id, frame_id, init_command, command_type)
self._offset = offset
self._image_id = image_id
def get_command_id(self):
return CMD_IMAGE_COMMAND_CHUNK_LOAD
def get_offset(self):
return self._offset
def get_image_id(self):
return self._image_id
#=======================================================================================================================
# InternalChangeVariable
#=======================================================================================================================

View File

@@ -95,6 +95,9 @@ CMD_TABLE_EXEC = 211
CMD_INTERRUPT_DEBUG_CONSOLE = 212
CMD_IMAGE_COMMAND_START_LOAD = 213
CMD_IMAGE_COMMAND_CHUNK_LOAD = 214
CMD_VERSION = 501
CMD_RETURN = 502
CMD_SET_PROTOCOL = 503

View File

@@ -64,6 +64,8 @@ from _pydevd_bundle.pydevd_comm import (CMD_RUN, CMD_VERSION, CMD_LIST_THREADS,
CMD_DATAVIEWER_ACTION, InternalDataViewerAction,
CMD_TABLE_EXEC, InternalTableCommand,
CMD_INTERRUPT_DEBUG_CONSOLE,
CMD_IMAGE_COMMAND_START_LOAD, InternalTableImageStartCommand,
CMD_IMAGE_COMMAND_CHUNK_LOAD, InternalTableImageChunkCommand,
CMD_SET_USER_TYPE_RENDERERS)
from _pydevd_bundle.pydevd_constants import (get_thread_id, IS_PY3K, DebugInfoHolder,
dict_keys, STATE_RUN,
@@ -954,6 +956,22 @@ def process_net_command(py_db, cmd_id, seq, text):
except:
traceback.print_exc()
elif cmd_id == CMD_IMAGE_COMMAND_START_LOAD:
try:
thread_id, frame_id, init_command, command_type = text.split('\t')
int_cmd = InternalTableImageStartCommand(seq, thread_id, frame_id, init_command, command_type)
py_db.post_internal_command(int_cmd, thread_id)
except:
traceback.print_exc()
elif cmd_id == CMD_IMAGE_COMMAND_CHUNK_LOAD:
try:
thread_id, frame_id, init_command, command_type, offset, image_id = text.split('\t')
int_cmd = InternalTableImageChunkCommand(seq, thread_id, frame_id, init_command, command_type, int(offset), image_id)
py_db.post_internal_command(int_cmd, thread_id)
except:
traceback.print_exc()
elif cmd_id == CMD_SET_USER_TYPE_RENDERERS:
try:
type_renderers = parse_set_type_renderers_message(text)

View File

@@ -4,6 +4,7 @@
from _pydevd_bundle import pydevd_vars
from _pydevd_bundle.pydevd_constants import NEXT_VALUE_SEPARATOR
from _pydevd_bundle.pydevd_xml import ExceptionOnEvaluate
from _pydevd_bundle.tables.images.pydevd_image_loader import load_image_chunk
class TableCommandType:
@@ -12,7 +13,8 @@ class TableCommandType:
SLICE_CSV = "SLICE_CSV"
DESCRIBE = "DF_DESCRIBE"
VISUALIZATION_DATA = "VISUALIZATION_DATA"
IMAGE = "IMAGE"
IMAGE_START_CHUNK_LOAD = "IMAGE_START_CHUNK_LOAD"
IMAGE_CHUNK_LOAD = "IMAGE_CHUNK_LOAD"
def is_error_on_eval(val):
@@ -24,6 +26,22 @@ def is_error_on_eval(val):
is_exception_on_eval = False
return is_exception_on_eval
def exec_image_table_command(init_command, command_type, offset, image_id, f_globals, f_locals):
# type: (str, str, [int, None], [str, None], dict, dict) -> (bool, str)
table = pydevd_vars.eval_in_context(init_command, f_globals, f_locals)
is_exception_on_eval = is_error_on_eval(table)
if is_exception_on_eval:
return False, table.result
image_provider = __get_image_provider(table)
if not image_provider:
raise RuntimeError('No image provider for: {}'.format(type(table)))
if command_type == TableCommandType.IMAGE_START_CHUNK_LOAD:
return True, image_provider.create_image(table)
return True, load_image_chunk(offset, image_id)
def exec_table_command(init_command, command_type, start_index, end_index, format, f_globals,
f_locals):
@@ -34,7 +52,7 @@ def exec_table_command(init_command, command_type, start_index, end_index, forma
return False, table.result
table_provider = __get_table_provider(table)
if not table_provider and command_type != TableCommandType.IMAGE:
if not table_provider:
raise RuntimeError('No table data provider for: {}'.format(type(table)))
res = []
@@ -59,10 +77,6 @@ def exec_table_command(init_command, command_type, start_index, end_index, forma
elif command_type == TableCommandType.SLICE_CSV:
res.append(table_provider.get_data(table, True, start_index, end_index, format))
elif command_type == TableCommandType.IMAGE:
image_provider = __get_image_provider(table)
res.append(image_provider.get_bytes(table))
return True, ''.join(res)

View File

@@ -0,0 +1,30 @@
# Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
import base64
IMAGE_DATA_STORAGE = {}
DEFAULT_IMAGE_FORMAT = 'PNG'
DEFAULT_ENCODING = 'utf-8'
GRAYSCALE_MODE = 'L'
RGB_MODE = 'RGB'
RGBA_MODE = 'RGBA'
CHUNK_SIZE = 8192
def load_image_chunk(offset, image_id):
# type: (int, str) -> str
try:
bytes_data = IMAGE_DATA_STORAGE.get(image_id)
if bytes_data is None:
return "Error: No image data found."
chunk = bytes_data[offset:offset + CHUNK_SIZE]
next_offset = offset + CHUNK_SIZE
if next_offset >= len(bytes_data):
next_offset = -1
IMAGE_DATA_STORAGE.pop(image_id, None)
chunk_bytes = base64.b64encode(chunk)
if not isinstance(chunk_bytes, str):
chunk_bytes = chunk_bytes.decode(DEFAULT_ENCODING)
return "{};{}".format(chunk_bytes, next_offset)
except ValueError:
return "Error: Invalid offset format."
except Exception as e:
return "Error: {}".format(e)

View File

@@ -1,18 +1,22 @@
# Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
import base64
import io
import matplotlib
import uuid
from _pydevd_bundle.tables.images.pydevd_image_loader import IMAGE_DATA_STORAGE
from _pydevd_bundle.tables.images.pydevd_image_loader import DEFAULT_IMAGE_FORMAT
DEFAULT_IMAGE_FORMAT = 'PNG'
DEFAULT_ENCODING = 'utf-8'
def get_bytes(matplotlib_figure):
def create_image(matplotlib_figure):
# type: (matplotlib.figure.Figure) -> str
try:
bytes_buffer = io.BytesIO()
matplotlib_figure.savefig(bytes_buffer, format=DEFAULT_IMAGE_FORMAT)
bytes_buffer.seek(0)
return base64.b64encode(bytes_buffer.getvalue()).decode(DEFAULT_ENCODING)
try:
matplotlib_figure.savefig(bytes_buffer, format=DEFAULT_IMAGE_FORMAT)
bytes_buffer.seek(0)
bytes_data = bytes_buffer.getvalue()
image_id = str(uuid.uuid4())
IMAGE_DATA_STORAGE[image_id] = bytes_data
return "{}".format(image_id)
finally:
bytes_buffer.close()
except Exception as e:
return "Error: {}".format(e)
return "Error: {}".format(e)

View File

@@ -1,8 +1,12 @@
# Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
import numpy as np
import io
import base64
import uuid
from _pydevd_bundle.tables.images.pydevd_image_loader import IMAGE_DATA_STORAGE
from _pydevd_bundle.tables.images.pydevd_image_loader import DEFAULT_IMAGE_FORMAT
from _pydevd_bundle.tables.images.pydevd_image_loader import RGB_MODE
from _pydevd_bundle.tables.images.pydevd_image_loader import RGBA_MODE
from _pydevd_bundle.tables.images.pydevd_image_loader import GRAYSCALE_MODE
try:
import tensorflow as tf
@@ -13,15 +17,7 @@ try:
except ImportError:
pass
DEFAULT_IMAGE_FORMAT = 'PNG'
DEFAULT_ENCODING = 'utf-8'
GRAYSCALE_MODE = 'L'
RGB_MODE = 'RGB'
RGBA_MODE = 'RGBA'
def get_bytes(arr):
def create_image(arr):
# type: (np.ndarray) -> str
try:
from PIL import Image
@@ -51,7 +47,8 @@ def get_bytes(arr):
if arr_min == arr_max: # handle constant values
arr_to_convert = np.full_like(arr_to_convert, 127, dtype=np.uint8)
else:
arr_to_convert = ((arr_to_convert - arr_min) / (arr_max - arr_min) * 255).astype(np.uint8)
arr_to_convert = ((arr_to_convert - arr_min) / (
arr_max - arr_min) * 255).astype(np.uint8)
arr_to_convert_ndim = arr_to_convert.ndim
if arr_to_convert_ndim == 2:
@@ -60,14 +57,20 @@ def get_bytes(arr):
mode = RGBA_MODE
else:
mode = RGB_MODE
bytes_buffer = io.BytesIO()
image = Image.fromarray(arr_to_convert, mode=mode)
image.save(bytes_buffer, format=DEFAULT_IMAGE_FORMAT)
return base64.b64encode(bytes_buffer.getvalue()).decode(DEFAULT_ENCODING)
try:
image = Image.fromarray(arr_to_convert, mode=mode)
image.save(bytes_buffer, format=DEFAULT_IMAGE_FORMAT)
bytes_data = bytes_buffer.getvalue()
image_id = str(uuid.uuid4())
IMAGE_DATA_STORAGE[image_id] = bytes_data
return image_id
finally:
bytes_buffer.close()
except ImportError:
return "Error: Pillow library is not installed."
except (TypeError, ValueError):
return "Error: Only non-complex numeric array types are supported."
except Exception as e:
return "Error: {}".format(e)
return "Error: {}".format(e)

View File

@@ -1,17 +1,14 @@
# Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
import numpy as np
import io
import base64
import uuid
from _pydevd_bundle.tables.images.pydevd_image_loader import IMAGE_DATA_STORAGE
from _pydevd_bundle.tables.images.pydevd_image_loader import DEFAULT_IMAGE_FORMAT
from _pydevd_bundle.tables.images.pydevd_image_loader import RGB_MODE
from _pydevd_bundle.tables.images.pydevd_image_loader import RGBA_MODE
from _pydevd_bundle.tables.images.pydevd_image_loader import GRAYSCALE_MODE
DEFAULT_IMAGE_FORMAT = 'PNG'
DEFAULT_ENCODING = 'utf-8'
GRAYSCALE_MODE = 'L'
RGB_MODE = 'RGB'
RGBA_MODE = 'RGBA'
def get_bytes(arr):
def create_image(arr):
# type: (np.ndarray) -> str
try:
from PIL import Image
@@ -41,13 +38,21 @@ def get_bytes(arr):
mode = RGBA_MODE
else:
mode = RGB_MODE
bytes_buffer = io.BytesIO()
image = Image.fromarray(arr_to_convert, mode=mode)
image.save(bytes_buffer, format=DEFAULT_IMAGE_FORMAT)
return base64.b64encode(bytes_buffer.getvalue()).decode(DEFAULT_ENCODING)
try:
image = Image.fromarray(arr_to_convert, mode=mode)
image.save(bytes_buffer, format=DEFAULT_IMAGE_FORMAT)
bytes_data = bytes_buffer.getvalue()
image_id = str(uuid.uuid4())
IMAGE_DATA_STORAGE[image_id] = bytes_data
return image_id
finally:
bytes_buffer.close()
except ImportError:
return "Error: Pillow library is not installed."
except (TypeError, ValueError):
return "Error: Only non-complex numeric array types are supported."
except Exception as e:
return "Error: {}".format(e)
return "Error: {}".format(e)

View File

@@ -1,20 +1,22 @@
# Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
import base64
import io
import PIL
import uuid
from _pydevd_bundle.tables.images.pydevd_image_loader import IMAGE_DATA_STORAGE
from _pydevd_bundle.tables.images.pydevd_image_loader import DEFAULT_IMAGE_FORMAT
DEFAULT_IMAGE_FORMAT = 'PNG'
DEFAULT_ENCODING = 'utf-8'
GRAYSCALE_MODE = 'L'
RGB_MODE = 'RGB'
def get_bytes(pillow_image):
def create_image(pillow_image):
# type: (PIL.Image.Image) -> str
try:
bytes_buffer = io.BytesIO()
image_format = pillow_image.format if pillow_image.format else DEFAULT_IMAGE_FORMAT
pillow_image.save(bytes_buffer, format=image_format)
return base64.b64encode(bytes_buffer.getvalue()).decode(DEFAULT_ENCODING)
try:
image_format = pillow_image.format if pillow_image.format else DEFAULT_IMAGE_FORMAT
pillow_image.save(bytes_buffer, format=image_format)
bytes_data = bytes_buffer.getvalue()
image_id = str(uuid.uuid4())
IMAGE_DATA_STORAGE[image_id] = bytes_data
return "{}".format(image_id)
finally:
bytes_buffer.close()
except Exception as e:
return "Error: {}".format(e)
return "Error: {}".format(e)

View File

@@ -202,6 +202,9 @@ service PythonConsoleBackendService {
void loadFullValue(1: LoadFullValueRequestSeq seq, 2: list<string> variables) throws (1: PythonUnhandledException unhandledException),
string execTableCommand(1: string tableVariable, 2: string commandType, 3: string startIndex, 4: string endIndex, 5: string format) throws (1: PythonUnhandledException unhandledException, 2: PythonTableException tableException)
string execTableImageCommand(1: string tableVariable, 2: string commandType, 3: string offset, 4: string imageId) throws (1: PythonUnhandledException unhandledException, 2: PythonTableException tableException)
}
exception KeyboardInterruptException {

View File

@@ -499,7 +499,7 @@ public class PyDebugValue extends XNamedValue {
private static void addViewAsImageLink(XValueNodeImpl valueNode) {
PyDebugValue debugValue = (PyDebugValue)valueNode.getXValue();
if (!checkAndShowViewAsImageOnScreen(debugValue) || hasJupyterFrameAccessor(debugValue.getFrameAccessor()))
if (!checkAndShowViewAsImageOnScreen(debugValue))
return;
String viewAsImageText = PydevBundle.message("pydev.view.as.image");
valueNode.addAdditionalHyperlink(new XDebuggerTreeNodeHyperlink(viewAsImageText) {
@@ -523,12 +523,6 @@ public class PyDebugValue extends XNamedValue {
});
}
// TODO: remove this check after dealing with IOPub
private static boolean hasJupyterFrameAccessor(PyFrameAccessor frameAccessor) {
if (frameAccessor == null) return true;
return frameAccessor.getClass().getName().contains("JupyterVarsFrameAccessor");
}
private static boolean checkAndShowViewAsImageOnScreen(PyDebugValue debugValue) {
boolean showViewAsImage = Registry.get("actions.show.as.image.visibility").asBoolean();
if (!showViewAsImage) {

View File

@@ -97,6 +97,9 @@ public interface PyFrameAccessor {
@Nullable
String execTableCommand(String command, TableCommandType commandType, @Nullable TableCommandParameters tableCommandParameters) throws PyDebuggerException;
@Nullable
String execTableImageCommand(String command, TableCommandType commandType, @Nullable TableCommandParameters tableCommandParameters) throws PyDebuggerException;
/**
* @return result as a preview image packed into json array. Image can be compressed if necessary.
*/

View File

@@ -75,6 +75,9 @@ public abstract class AbstractCommand<T> {
public static final int INTERRUPT_DEBUG_CONSOLE = 212;
public static final int IMAGE_COMMAND_START_LOAD = 213;
public static final int IMAGE_COMMAND_CHUNK_LOAD = 214;
/**
* The code of the message that means that IDE received
* {@link #PROCESS_CREATED} message from the Python debugger script.

View File

@@ -212,6 +212,15 @@ public class ClientModeMultiProcessDebugger implements ProcessDebugger {
return debugger(threadId).execTableCommand(threadId, frameId, command, commandType, tableCommandParameters);
}
@Override
public @Nullable String execTableImageCommand(String threadId,
String frameId,
String command,
TableCommandType commandType, TableCommandParameters tableCommandParameters)
throws PyDebuggerException {
return debugger(threadId).execTableCommand(threadId, frameId, command, commandType, tableCommandParameters);
}
@Override
public List<Pair<String, Boolean>> getSmartStepIntoVariants(String threadId, String frameId, int startContextLine, int endContextLine)
throws PyDebuggerException {

View File

@@ -225,6 +225,15 @@ public class MultiProcessDebugger implements ProcessDebugger {
return debugger(threadId).execTableCommand(threadId, frameId, command, commandType, tableCommandParameters);
}
@Override
public @Nullable String execTableImageCommand(String threadId,
String frameId,
String command,
TableCommandType commandType, TableCommandParameters tableCommandParameters)
throws PyDebuggerException {
return debugger(threadId).execTableImageCommand(threadId, frameId, command, commandType, tableCommandParameters);
}
@Override
public List<Pair<String, Boolean>> getSmartStepIntoVariants(String threadId, String frameId, int startContextLine, int endContextLine)
throws PyDebuggerException {

View File

@@ -42,6 +42,10 @@ public interface ProcessDebugger {
String execTableCommand(String threadId, String frameId, String command, TableCommandType commandType,
TableCommandParameters tableCommandParameters) throws PyDebuggerException;
@Nullable
String execTableImageCommand(String threadId, String frameId, String command, TableCommandType commandType,
TableCommandParameters tableCommandParameters) throws PyDebuggerException;
enum GROUP_TYPE {
DEFAULT,
SPECIAL,

View File

@@ -186,6 +186,14 @@ public class RemoteDebugger implements ProcessDebugger {
return tableCommand.getCommandResult();
}
@Override
public @Nullable String execTableImageCommand(String threadId, String frameId, String command, TableCommandType commandType,
TableCommandParameters tableCommandParameters) throws PyDebuggerException {
final TableImageCommand tableImageCommand = new TableImageCommand(this, threadId, frameId, command, commandType, tableCommandParameters);
tableImageCommand.execute();
return tableImageCommand.getCommandResult();
}
@Override
public XValueChildrenList loadFrame(final String threadId, final String frameId, GROUP_TYPE groupType) throws PyDebuggerException {
return executeCommand(new GetFrameCommand(this, threadId, frameId, groupType)).getVariables();

View File

@@ -0,0 +1,47 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.debugger.pydev
import com.intellij.util.asSafely
import com.jetbrains.python.debugger.pydev.tables.PyDevImageCommandParameters
import com.jetbrains.python.tables.TableCommandParameters
import com.jetbrains.python.tables.TableCommandType
class TableImageCommand(
debugger: RemoteDebugger?, threadId: String?, frameId: String?,
private val initExpr: String,
private val commandType: TableCommandType,
private val tableCommandParameters: TableCommandParameters?
) : AbstractFrameCommand<String?>(debugger, getCommandCode(tableCommandParameters), threadId, frameId) {
var commandResult: String? = null
override fun buildPayload(payload: Payload) {
super.buildPayload(payload)
payload.add(initExpr).add(commandType.name)
if (commandType == TableCommandType.IMAGE_CHUNK_LOAD) {
addChunkParameters(payload)
}
}
private fun addChunkParameters(payload: Payload) {
tableCommandParameters?.asSafely<PyDevImageCommandParameters>()?.let {
payload.add(it.offset ?: -1).add(it.imageId)
}
}
override fun isResponseExpected(): Boolean = true
override fun processResponse(response: ProtocolFrame) {
super.processResponse(response)
commandResult = response.payload
}
companion object {
private fun getCommandCode(parameters: TableCommandParameters?): Int =
if (parameters?.asSafely<PyDevImageCommandParameters>() == null) {
IMAGE_COMMAND_START_LOAD
}
else {
IMAGE_COMMAND_CHUNK_LOAD
}
}
}

View File

@@ -0,0 +1,9 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.jetbrains.python.debugger.pydev.tables
import com.intellij.openapi.util.IntellijInternalApi
import com.jetbrains.python.tables.TableCommandParameters
class PyDevImageCommandParameters(val offset: Int?, val imageId: String?) : TableCommandParameters

View File

@@ -9,5 +9,6 @@ enum class TableCommandType {
SLICE_CSV,
DF_DESCRIBE,
VISUALIZATION_DATA,
IMAGE
IMAGE_START_CHUNK_LOAD,
IMAGE_CHUNK_LOAD
}

View File

@@ -35,6 +35,7 @@ import com.jetbrains.python.debugger.pydev.SetUserTypeRenderersCommand;
import com.jetbrains.python.debugger.pydev.dataviewer.DataViewerCommandBuilder;
import com.jetbrains.python.debugger.pydev.dataviewer.DataViewerCommandResult;
import com.jetbrains.python.debugger.pydev.tables.PyDevCommandParameters;
import com.jetbrains.python.debugger.pydev.tables.PyDevImageCommandParameters;
import com.jetbrains.python.debugger.settings.PyDebuggerSettings;
import com.jetbrains.python.debugger.variablesview.usertyperenderers.ConfigureTypeRenderersHyperLink;
import com.jetbrains.python.debugger.variablesview.usertyperenderers.PyUserNodeRenderer;
@@ -395,6 +396,34 @@ public abstract class PydevConsoleCommunication extends AbstractConsoleCommunica
}
}
@Override
public String execTableImageCommand(String command, TableCommandType commandType, TableCommandParameters tableCommandParameters) throws PyDebuggerException {
if (!isCommunicationClosed()) {
return executeBackgroundTask(
() -> {
String offset = "";
String imageId = "";
try {
if (tableCommandParameters instanceof PyDevImageCommandParameters) {
offset = String.valueOf(((PyDevImageCommandParameters)tableCommandParameters).getOffset());
imageId = String.valueOf(((PyDevImageCommandParameters)tableCommandParameters).getImageId());
}
return getPythonConsoleBackendClient().execTableImageCommand(command, commandType.name(), offset, imageId);
}
catch (PythonTableException e) {
throw new PyDebuggerException(e.message);
}
},
true,
createRuntimeMessage(PyBundle.message("console.getting.table.data")),
PyBundle.message("console.table.failed.to.load")
);
}
else {
return null;
}
}
@TestOnly
public List<PydevCompletionVariant> gerCompletionVariants(String text, String actTok) throws Exception {
return doGetCompletions(text, actTok);

View File

@@ -832,6 +832,13 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr
return myDebugger.execTableCommand(frame.getThreadId(), frame.getFrameId(), command, commandType, tableCommandParameters);
}
@Override
public String execTableImageCommand(String command, TableCommandType commandType, TableCommandParameters tableCommandParameters)
throws PyDebuggerException {
final PyStackFrame frame = currentFrame();
return myDebugger.execTableImageCommand(frame.getThreadId(), frame.getFrameId(), command, commandType, tableCommandParameters);
}
@Override
public boolean isFrameCached(@NotNull XStackFrame contextFrame) {
synchronized (myFrameCacheObject) {