From d2912a83a0609efe2a149c06ccd8c899057743ea Mon Sep 17 00:00:00 2001 From: Maxim Popov Date: Mon, 12 Jan 2026 23:19:50 +0000 Subject: [PATCH] [debugger] PY-85315 add "view as image" hyperlink for variables in debugpy Merge-request: IJ-MR-181659 Merged-by: Maxim Popov GitOrigin-RevId: d44706f7aaa706ec7b4d368de1cea309428b47a5 --- .../debugProtocolCustomPyCharm.json | 91 ++++- .../_debug_adapter/pydevd_schema.py | 359 +++++++++++++++--- .../tables/images/pydevd_matplotlib_image.py | 2 +- .../tables/images/pydevd_numpy_based_image.py | 2 +- .../tables/images/pydevd_numpy_image.py | 2 +- .../tables/images/pydevd_pillow_image.py | 2 +- .../pydevd/_pydevd_bundle/pydevd_api.py | 10 +- .../pydevd/_pydevd_bundle/pydevd_comm.py | 70 +++- .../_pydevd_bundle/pydevd_comm_constants.py | 6 + .../pydevd_net_command_factory_json.py | 15 +- .../pydevd_process_net_command_json.py | 37 +- .../_pydevd_bundle/pydevd_suspended_frames.py | 4 + .../python/debugger/PyDebugValue.java | 16 +- .../python/debugger/PyXDebugValue.kt | 8 + .../python/debugger/PyDebuggerEvaluator.kt | 2 +- 15 files changed, 552 insertions(+), 74 deletions(-) create mode 100644 python/pydevSrc/src/com/jetbrains/python/debugger/PyXDebugValue.kt diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/_debug_adapter/debugProtocolCustomPyCharm.json b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/_debug_adapter/debugProtocolCustomPyCharm.json index 334a0b56c547..8f54c1ed3f2a 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/_debug_adapter/debugProtocolCustomPyCharm.json +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/_debug_adapter/debugProtocolCustomPyCharm.json @@ -70,6 +70,20 @@ "INSPECTIONS" ] }, + "parametersJson": { + "$ref": "#/definitions/GetTableOptionalParameters" + } + }, + "required": [ + "threadId", + "frameId", + "commandType" + ] + }, + "GetTableOptionalParameters": { + "type": "object", + "description": "Optional parameters for 'getTable' / 'getTableImage' operations.", + "properties": { "start": { "type": [ "integer", @@ -90,13 +104,23 @@ "null" ], "description": "Optional backend-specific format hint (e.g., 'json', 'csv', dtype/precision hints)." + }, + "offset": { + "type": [ + "integer", + "null" + ], + "description": "Optional offset for image operations." + }, + "imageId": { + "type": [ + "string", + "null" + ], + "description": "Optional image id for image operations." } }, - "required": [ - "threadId", - "frameId", - "commandType" - ] + "additionalProperties": true }, "GetTableResponse": { "allOf": [ @@ -129,6 +153,63 @@ } ] }, + "GetTableImageRequest": { + "allOf": [ + { + "$ref": "#/definitions/Request" + }, + { + "type": "object", + "description": "Retrieve tabular data as an image", + "properties": { + "command": { + "type": "string", + "enum": [ + "getTableImage" + ] + }, + "arguments": { + "$ref": "#/definitions/GetTableArguments" + } + }, + "required": [ + "command", + "arguments" + ] + } + ] + }, + "GetTableImageResponse": { + "allOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "type": "object", + "description": "Response to 'getTableImage' request.", + "properties": { + "command": { + "type": "string", + "enum": [ + "getTableImage" + ] + }, + "body": { + "type": "object", + "properties": { + "result": { + "type": "string", + "description": "Opaque string payload with the result of the getTableImage operation." + } + } + } + }, + "required": [ + "body" + ] + } + ] + }, "GetArrayRequest": { "allOf": [ { diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/_debug_adapter/pydevd_schema.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/_debug_adapter/pydevd_schema.py index 49d50669a82b..c82d926ebfd9 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/_debug_adapter/pydevd_schema.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/_debug_adapter/pydevd_schema.py @@ -17416,6 +17416,85 @@ class GetTableArguments(BaseSchema): "INSPECTIONS" ] }, + "parametersJson": { + "type": "GetTableOptionalParameters" + } + } + __refs__ = set(['parametersJson']) + + __slots__ = list(__props__.keys()) + ['kwargs'] + + def __init__(self, threadId, frameId, commandType, command=None, parametersJson=None, update_ids_from_dap=False, **kwargs): # noqa (update_ids_from_dap may be unused) + """ + :param ['string', 'integer'] threadId: Thread identifier where the frame/expression should be evaluated. + :param ['string', 'integer'] frameId: Frame identifier within the given thread. + :param string commandType: + :param string command: Python expression that evaluates to the table-like object (e.g., variable name or expression). + :param GetTableOptionalParameters parametersJson: + """ + self.threadId = threadId + self.frameId = frameId + self.commandType = commandType + self.command = command + if parametersJson is None: + self.parametersJson = GetTableOptionalParameters() + else: + self.parametersJson = GetTableOptionalParameters(update_ids_from_dap=update_ids_from_dap, **parametersJson) if parametersJson.__class__ != GetTableOptionalParameters else parametersJson + if update_ids_from_dap: + self.threadId = self._translate_id_from_dap(self.threadId) + self.frameId = self._translate_id_from_dap(self.frameId) + self.kwargs = kwargs + + + @classmethod + def update_dict_ids_from_dap(cls, dct): + if 'threadId' in dct: + dct['threadId'] = cls._translate_id_from_dap(dct['threadId']) + if 'frameId' in dct: + dct['frameId'] = cls._translate_id_from_dap(dct['frameId']) + return dct + + def to_dict(self, update_ids_to_dap=False): # noqa (update_ids_to_dap may be unused) + threadId = self.threadId + frameId = self.frameId + commandType = self.commandType + command = self.command + parametersJson = self.parametersJson + if update_ids_to_dap: + if threadId is not None: + threadId = self._translate_id_to_dap(threadId) + if frameId is not None: + frameId = self._translate_id_to_dap(frameId) + dct = { + 'threadId': threadId, + 'frameId': frameId, + 'commandType': commandType, + } + if command is not None: + dct['command'] = command + if parametersJson is not None: + dct['parametersJson'] = parametersJson.to_dict(update_ids_to_dap=update_ids_to_dap) + dct.update(self.kwargs) + return dct + + @classmethod + def update_dict_ids_to_dap(cls, dct): + if 'threadId' in dct: + dct['threadId'] = cls._translate_id_to_dap(dct['threadId']) + if 'frameId' in dct: + dct['frameId'] = cls._translate_id_to_dap(dct['frameId']) + return dct + + +@register +class GetTableOptionalParameters(BaseSchema): + """ + Optional parameters for 'getTable' / 'getTableImage' operations. + + Note: automatically generated code. Do not edit manually. + """ + + __props__ = { "start": { "type": [ "integer", @@ -17436,78 +17515,61 @@ class GetTableArguments(BaseSchema): "null" ], "description": "Optional backend-specific format hint (e.g., 'json', 'csv', dtype/precision hints)." + }, + "offset": { + "type": [ + "integer", + "null" + ], + "description": "Optional offset for image operations." + }, + "imageId": { + "type": [ + "string", + "null" + ], + "description": "Optional image id for image operations." } } __refs__ = set() __slots__ = list(__props__.keys()) + ['kwargs'] - def __init__(self, threadId, frameId, commandType, command=None, start=None, end=None, format=None, update_ids_from_dap=False, **kwargs): # noqa (update_ids_from_dap may be unused) + def __init__(self, start=None, end=None, format=None, offset=None, imageId=None, update_ids_from_dap=False, **kwargs): # noqa (update_ids_from_dap may be unused) """ - :param ['string', 'integer'] threadId: Thread identifier where the frame/expression should be evaluated. - :param ['string', 'integer'] frameId: Frame identifier within the given thread. - :param string commandType: - :param string command: Python expression that evaluates to the table-like object (e.g., variable name or expression). :param ['integer', 'null'] start: Optional start row index (inclusive) for slice operations. :param ['integer', 'null'] end: Optional end row index (exclusive) for slice operations. :param ['string', 'null'] format: Optional backend-specific format hint (e.g., 'json', 'csv', dtype/precision hints). + :param ['integer', 'null'] offset: Optional offset for image operations. + :param ['string', 'null'] imageId: Optional image id for image operations. """ - self.threadId = threadId - self.frameId = frameId - self.commandType = commandType - self.command = command self.start = start self.end = end self.format = format - if update_ids_from_dap: - self.threadId = self._translate_id_from_dap(self.threadId) - self.frameId = self._translate_id_from_dap(self.frameId) + self.offset = offset + self.imageId = imageId self.kwargs = kwargs - - - @classmethod - def update_dict_ids_from_dap(cls, dct): - if 'threadId' in dct: - dct['threadId'] = cls._translate_id_from_dap(dct['threadId']) - if 'frameId' in dct: - dct['frameId'] = cls._translate_id_from_dap(dct['frameId']) - return dct + def to_dict(self, update_ids_to_dap=False): # noqa (update_ids_to_dap may be unused) - threadId = self.threadId - frameId = self.frameId - commandType = self.commandType - command = self.command start = self.start end = self.end format = self.format # noqa (assign to builtin) - if update_ids_to_dap: - if threadId is not None: - threadId = self._translate_id_to_dap(threadId) - if frameId is not None: - frameId = self._translate_id_to_dap(frameId) + offset = self.offset + imageId = self.imageId dct = { - 'threadId': threadId, - 'frameId': frameId, - 'commandType': commandType, } - if command is not None: - dct['command'] = command if start is not None: dct['start'] = start if end is not None: dct['end'] = end if format is not None: dct['format'] = format + if offset is not None: + dct['offset'] = offset + if imageId is not None: + dct['imageId'] = imageId dct.update(self.kwargs) - return dct - - @classmethod - def update_dict_ids_to_dap(cls, dct): - if 'threadId' in dct: - dct['threadId'] = cls._translate_id_to_dap(dct['threadId']) - if 'frameId' in dct: - dct['frameId'] = cls._translate_id_to_dap(dct['frameId']) return dct @@ -17620,6 +17682,181 @@ class GetTableResponse(BaseSchema): return dct +@register_request('getTableImage') +@register +class GetTableImageRequest(BaseSchema): + """ + Retrieve tabular data as an image + + Note: automatically generated code. Do not edit manually. + """ + + __props__ = { + "seq": { + "type": "integer", + "description": "Sequence number of the message (also known as message ID). The `seq` for the first message sent by a client or debug adapter is 1, and for each subsequent message is 1 greater than the previous message sent by that actor. `seq` can be used to order requests, responses, and events, and to associate requests with their corresponding responses. For protocol messages of type `request` the sequence number can be used to cancel the request." + }, + "type": { + "type": "string", + "enum": [ + "request" + ] + }, + "command": { + "type": "string", + "enum": [ + "getTableImage" + ] + }, + "arguments": { + "type": "GetTableArguments" + } + } + __refs__ = set(['arguments']) + + __slots__ = list(__props__.keys()) + ['kwargs'] + + def __init__(self, arguments, seq=-1, update_ids_from_dap=False, **kwargs): # noqa (update_ids_from_dap may be unused) + """ + :param string type: + :param string command: + :param GetTableArguments arguments: + :param integer seq: Sequence number of the message (also known as message ID). The `seq` for the first message sent by a client or debug adapter is 1, and for each subsequent message is 1 greater than the previous message sent by that actor. `seq` can be used to order requests, responses, and events, and to associate requests with their corresponding responses. For protocol messages of type `request` the sequence number can be used to cancel the request. + """ + self.type = 'request' + self.command = 'getTableImage' + if arguments is None: + self.arguments = GetTableArguments() + else: + self.arguments = GetTableArguments(update_ids_from_dap=update_ids_from_dap, **arguments) if arguments.__class__ != GetTableArguments else arguments + self.seq = seq + self.kwargs = kwargs + + + def to_dict(self, update_ids_to_dap=False): # noqa (update_ids_to_dap may be unused) + type = self.type # noqa (assign to builtin) + command = self.command + arguments = self.arguments + seq = self.seq + dct = { + 'type': type, + 'command': command, + 'arguments': arguments.to_dict(update_ids_to_dap=update_ids_to_dap), + 'seq': seq, + } + dct.update(self.kwargs) + return dct + + +@register_response('getTableImage') +@register +class GetTableImageResponse(BaseSchema): + """ + Response to 'getTableImage' request. + + Note: automatically generated code. Do not edit manually. + """ + + __props__ = { + "seq": { + "type": "integer", + "description": "Sequence number of the message (also known as message ID). The `seq` for the first message sent by a client or debug adapter is 1, and for each subsequent message is 1 greater than the previous message sent by that actor. `seq` can be used to order requests, responses, and events, and to associate requests with their corresponding responses. For protocol messages of type `request` the sequence number can be used to cancel the request." + }, + "type": { + "type": "string", + "enum": [ + "response" + ] + }, + "request_seq": { + "type": "integer", + "description": "Sequence number of the corresponding request." + }, + "success": { + "type": "boolean", + "description": "Outcome of the request.\nIf True, the request was successful and the `body` attribute may contain the result of the request.\nIf the value is false, the attribute `message` contains the error in short form and the `body` may contain additional information (see `ErrorResponse.body.error`)." + }, + "command": { + "type": "string", + "enum": [ + "getTableImage" + ] + }, + "message": { + "type": "string", + "description": "Contains the raw error in short form if `success` is false.\nThis raw error might be interpreted by the client and is not shown in the UI.\nSome predefined values exist.", + "_enum": [ + "cancelled", + "notStopped" + ], + "enumDescriptions": [ + "the request was cancelled.", + "the request may be retried once the adapter is in a 'stopped' state." + ] + }, + "body": { + "type": "object", + "properties": { + "result": { + "type": "string", + "description": "Opaque string payload with the result of the getTableImage operation." + } + } + } + } + __refs__ = set(['body']) + + __slots__ = list(__props__.keys()) + ['kwargs'] + + def __init__(self, request_seq, success, body, seq=-1, message=None, update_ids_from_dap=False, **kwargs): # noqa (update_ids_from_dap may be unused) + """ + :param string type: + :param integer request_seq: Sequence number of the corresponding request. + :param boolean success: Outcome of the request. + If true, the request was successful and the `body` attribute may contain the result of the request. + If the value is false, the attribute `message` contains the error in short form and the `body` may contain additional information (see `ErrorResponse.body.error`). + :param string command: + :param GetTableImageResponseBody body: + :param integer seq: Sequence number of the message (also known as message ID). The `seq` for the first message sent by a client or debug adapter is 1, and for each subsequent message is 1 greater than the previous message sent by that actor. `seq` can be used to order requests, responses, and events, and to associate requests with their corresponding responses. For protocol messages of type `request` the sequence number can be used to cancel the request. + :param string message: Contains the raw error in short form if `success` is false. + This raw error might be interpreted by the client and is not shown in the UI. + Some predefined values exist. + """ + self.type = 'response' + self.request_seq = request_seq + self.success = success + self.command = 'getTableImage' + if body is None: + self.body = GetTableImageResponseBody() + else: + self.body = GetTableImageResponseBody(update_ids_from_dap=update_ids_from_dap, **body) if body.__class__ != GetTableImageResponseBody else body + self.seq = seq + self.message = message + self.kwargs = kwargs + + + def to_dict(self, update_ids_to_dap=False): # noqa (update_ids_to_dap may be unused) + type = self.type # noqa (assign to builtin) + request_seq = self.request_seq + success = self.success + command = self.command + body = self.body + seq = self.seq + message = self.message + dct = { + 'type': type, + 'request_seq': request_seq, + 'success': success, + 'command': command, + 'body': body.to_dict(update_ids_to_dap=update_ids_to_dap), + 'seq': seq, + } + if message is not None: + dct['message'] = message + dct.update(self.kwargs) + return dct + + @register_request('getArray') @register class GetArrayRequest(BaseSchema): @@ -20663,6 +20900,42 @@ class GetTableResponseBody(BaseSchema): return dct +@register +class GetTableImageResponseBody(BaseSchema): + """ + "body" of GetTableImageResponse + + Note: automatically generated code. Do not edit manually. + """ + + __props__ = { + "result": { + "type": "string", + "description": "Opaque string payload with the result of the getTableImage operation." + } + } + __refs__ = set() + + __slots__ = list(__props__.keys()) + ['kwargs'] + + def __init__(self, result=None, update_ids_from_dap=False, **kwargs): # noqa (update_ids_from_dap may be unused) + """ + :param string result: Opaque string payload with the result of the getTableImage operation. + """ + self.result = result + self.kwargs = kwargs + + + def to_dict(self, update_ids_to_dap=False): # noqa (update_ids_to_dap may be unused) + result = self.result + dct = { + } + if result is not None: + dct['result'] = result + dct.update(self.kwargs) + return dct + + @register class GetArrayResponseBody(BaseSchema): """ diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_matplotlib_image.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_matplotlib_image.py index ad692f59f3f4..715717185749 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_matplotlib_image.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_matplotlib_image.py @@ -1,5 +1,5 @@ # Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -from _pydevd_bundle.tables.images.pydevd_image_loader import save_image_to_storage, DEFAULT_IMAGE_FORMAT +from _pydevd_bundle.custom.tables.images.pydevd_image_loader import save_image_to_storage, DEFAULT_IMAGE_FORMAT def create_image(figure): diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_numpy_based_image.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_numpy_based_image.py index 34f947ef0aa8..0d1c0b5438e2 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_numpy_based_image.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_numpy_based_image.py @@ -1,6 +1,6 @@ # 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 -from _pydevd_bundle.tables.images.pydevd_image_loader import (save_image_to_storage, GRAYSCALE_MODE, RGB_MODE, RGBA_MODE) +from _pydevd_bundle.custom.tables.images.pydevd_image_loader import (save_image_to_storage, GRAYSCALE_MODE, RGB_MODE, RGBA_MODE) try: import tensorflow as tf diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_numpy_image.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_numpy_image.py index 1148bdc81aff..15b3ab403b5f 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_numpy_image.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_numpy_image.py @@ -1,6 +1,6 @@ # 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 -from _pydevd_bundle.tables.images.pydevd_image_loader import (save_image_to_storage, GRAYSCALE_MODE, RGB_MODE, RGBA_MODE) +from _pydevd_bundle.custom.tables.images.pydevd_image_loader import (save_image_to_storage, GRAYSCALE_MODE, RGB_MODE, RGBA_MODE) MAX_PIXELS = 144_000_000 MAX_DIMENSION = 15_000 diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_pillow_image.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_pillow_image.py index 467eff21494a..6c29e355215a 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_pillow_image.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/custom/tables/images/pydevd_pillow_image.py @@ -1,6 +1,6 @@ # Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. import PIL -from _pydevd_bundle.tables.images.pydevd_image_loader import save_image_to_storage, DEFAULT_IMAGE_FORMAT +from _pydevd_bundle.custom.tables.images.pydevd_image_loader import save_image_to_storage, DEFAULT_IMAGE_FORMAT def create_image(pillow_image): # type: (PIL.Image.Image) -> str diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py index 97db69b2a30a..6310dc3b8b20 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py @@ -25,7 +25,7 @@ from _pydevd_bundle.pydevd_comm import ( internal_get_exception_details_json, internal_step_in_thread, internal_smart_step_into, - InternalTableCommand + InternalTableCommand, InternalTableImageStartCommand, InternalTableImageChunkCommand ) from _pydevd_bundle.pydevd_comm_constants import ( CMD_THREAD_SUSPEND, @@ -346,6 +346,14 @@ class PyDevdAPI(object): int_cmd = InternalTableCommand(seq, thread_id, frame_id, init_command, command_type, start_index, end_index, format) py_db.post_internal_command(int_cmd, thread_id) + def request_get_image_start(self, py_db, seq, thread_id, frame_id, init_command, command_type): + int_cmd = InternalTableImageStartCommand(seq, thread_id, frame_id, init_command, command_type) + py_db.post_internal_command(int_cmd, thread_id) + + def request_get_image_load_chunk(self, py_db, seq, thread_id, frame_id, init_command, command_type, offset, image_id): + int_cmd = InternalTableImageChunkCommand(seq, thread_id, frame_id, init_command, command_type, offset, image_id) + py_db.post_internal_command(int_cmd, thread_id) + def request_load_full_value(self, py_db, seq, thread_id, frame_id, vars): int_cmd = InternalLoadFullValue(seq, thread_id, frame_id, vars) py_db.post_internal_command(int_cmd, thread_id) diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index 30feee972c05..d8748c9771e7 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -95,7 +95,8 @@ from _pydevd_bundle._debug_adapter.pydevd_schema import ( ) from _pydevd_bundle._debug_adapter import pydevd_base_schema, pydevd_schema from _pydevd_bundle.pydevd_net_command import NetCommand -from _pydevd_bundle.custom.pydevd_tables import exec_table_command +from _pydevd_bundle.custom.pydevd_tables import exec_table_command, \ + exec_image_table_command from _pydevd_bundle.pydevd_xml import ExceptionOnEvaluate from _pydevd_bundle.pydevd_constants import ForkSafeLock, NULL from _pydevd_bundle.pydevd_daemon_thread import PyDBDaemonThread @@ -1948,18 +1949,13 @@ class InternalTableCommand(InternalThreadCommand): def do_it(self, dbg): try: - pydev_log.info(f"WE ARE IN INTERNAL TABLE COMMAND, thread_id: {self.thread_id}, frame_id: {self.frame_id}" ) frame = dbg.find_frame(self.thread_id, self.frame_id) - pydev_log.info("frame = dbg.find_frame(self.thread_id, self.frame_id)") - pydev_log.info(f"frame {frame}") success, res = self.exec_command(frame) if success: - pydev_log.info("success") cmd = dbg.cmd_factory.make_get_table_message(self.sequence, res) dbg.writer.add_command(cmd) else: - pydev_log.info(f"error, no success, res: {res}") cmd = dbg.cmd_factory.make_error_message(self.sequence, str(res)) dbg.writer.add_command(cmd) except Exception as e: @@ -1970,3 +1966,65 @@ class InternalTableCommand(InternalThreadCommand): return exec_table_command(self.init_command, self.command_type, self.start_index, self.end_index, self.format, frame.f_globals, frame.f_locals) + + +#======================================================================================================================= +# DebugImageViewerAction +#======================================================================================================================= +class InternalTableImageCommandBase(InternalThreadCommand): + def __init__(self, sequence, thread_id, frame_id, init_command, command_type): + InternalThreadCommand.__init__(self, 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 = dbg.find_frame(self.thread_id, self.frame_id) + success, res = self.exec_command(frame) + + if success: + cmd = dbg.cmd_factory.make_get_table_image_message(self.sequence, res, self.get_command_id()) + 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): + InternalTableImageCommandBase.__init__(self, 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 \ No newline at end of file diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm_constants.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm_constants.py index 05e83f9b3c57..440e86cefa05 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm_constants.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm_constants.py @@ -107,6 +107,9 @@ CMD_RETURN = 502 CMD_SET_PROTOCOL = 503 CMD_ERROR = 901 +CMD_IMAGE_COMMAND_START_LOAD = 213 +CMD_IMAGE_COMMAND_CHUNK_LOAD = 214 + # this number can be changed if there's need to do so # if the io is too big, we'll not send all (could make the debugger too non-responsive) MAX_IO_MSG_SIZE = 10000 @@ -191,10 +194,13 @@ ID_TO_MEANING = { "206": "CMD_STEP_INTO_COROUTINE", "207": "CMD_LOAD_SOURCE_FROM_FRAME_ID", "211": "CMD_TABLE_EXEC", + "213": "CMD_IMAGE_COMMAND_START_LOAD", + "214": "CMD_IMAGE_COMMAND_CHUNK_LOAD", "501": "CMD_VERSION", "502": "CMD_RETURN", "503": "CMD_SET_PROTOCOL", "901": "CMD_ERROR", + } diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py index 77f0b2e6445a..22259db0874e 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py @@ -16,7 +16,7 @@ from _pydevd_bundle._debug_adapter.pydevd_schema import ( OutputEvent, ContinuedEventBody, ExitedEventBody, - ExitedEvent, + ExitedEvent, GetTableImageResponseBody, ) from _pydevd_bundle.pydevd_comm_constants import ( CMD_THREAD_CREATE, @@ -596,15 +596,21 @@ This may mean a number of things: def make_get_table_message(self, seq, res): try: body = GetTableResponseBody(result=res) - pydev_log.info(f"RESPONSE BODY: {body}") response = pydevd_schema.GetTableResponse(request_seq=seq, success=True, body=body) - pydev_log.info(f"RESPONSE: {response}") return NetCommand(CMD_RETURN, 0, response, is_json=True) except Exception as e: - pydev_log.exception(f"Error while building getTable response: {e}") err_response = pydevd_schema.GetTableResponse(request_seq=seq, success=False, body={}) return NetCommand(CMD_RETURN, 0, err_response, is_json=True) + def make_get_table_image_message(self, seq, res, int_cmd): + try: + body = GetTableImageResponseBody(result=res) + response = pydevd_schema.GetTableImageResponse(request_seq=seq, success=True, body=body) + return NetCommand(int_cmd, 0, response, is_json=True) + except Exception as e: + err_response = pydevd_schema.GetTableResponse(request_seq=seq, success=False, body={}) + return NetCommand(int_cmd, 0, err_response, is_json=True) + @overrides(NetCommandFactory.make_get_array_message) def make_get_array_message(self, seq, res): try: @@ -612,6 +618,5 @@ This may mean a number of things: response = pydevd_schema.GetArrayResponse(request_seq=seq, success=True, body=body) return NetCommand(CMD_RETURN, 0, response, is_json=True) except Exception as e: - pydev_log.exception(f"Error while building getTable response: {e}") err_response = pydevd_schema.GetArrayResponse(request_seq=seq, success=False, body={}) return NetCommand(CMD_RETURN, 0, err_response, is_json=True) \ No newline at end of file diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index 7179b5b6ccd2..26e7791accff 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -35,6 +35,7 @@ from _pydevd_bundle._debug_adapter.pydevd_schema import ( BreakpointEventBody, InitializedEvent, ) +from _pydevd_bundle.custom.pydevd_tables import TableCommandType from _pydevd_bundle.pydevd_api import PyDevdAPI from _pydevd_bundle.pydevd_breakpoints import get_exception_class, FunctionBreakpoint from _pydevd_bundle.pydevd_comm_constants import ( @@ -1359,10 +1360,11 @@ class PyDevJsonCommandProcessor(object): frame_id = args.frameId init_command = args.command command_type = args.commandType - start_index = args.start - end_index = args.end - df_format = args.format + start_index = args.parametersJson.start + end_index = args.parametersJson.end + df_format = args.parametersJson.format error_msg = self.api.request_get_table(py_db, request.seq, thread_id, frame_id, init_command, command_type, start_index, end_index, df_format) + if error_msg: response = pydevd_base_schema.build_response( request, @@ -1372,7 +1374,6 @@ class PyDevJsonCommandProcessor(object): "message": error_msg, }, ) - pydev_log.error("ERR WHILE EXECUTING GETTABLE" + error_msg) return NetCommand(CMD_RETURN, 0, response, is_json=True) return None @@ -1396,6 +1397,32 @@ class PyDevJsonCommandProcessor(object): "message": error_msg, }, ) - pydev_log.error("ERR WHILE EXECUTING GETTABLE" + error_msg) return NetCommand(CMD_RETURN, 0, response, is_json=True) return None + + def on_gettableimage_request(self, py_db, request): + args = request.arguments + thread_id = args.threadId + frame_id = args.frameId + init_command = args.command + command_type = args.commandType + offset = args.parametersJson.offset + image_id = args.parametersJson.imageId + + if command_type == TableCommandType.IMAGE_CHUNK_LOAD: + error_msg = self.api.request_get_image_load_chunk(py_db, request.seq, thread_id, frame_id, init_command, command_type, offset, image_id) + elif command_type == TableCommandType.IMAGE_START_CHUNK_LOAD: + error_msg = self.api.request_get_image_start(py_db, request.seq, thread_id, frame_id, init_command, command_type) + else: + error_msg = f"Unknown command type: {command_type}" + if error_msg: + response = pydevd_base_schema.build_response( + request, + kwargs={ + "body": {}, + "success": False, + "message": error_msg, + }, + ) + return NetCommand(CMD_RETURN, 0, response, is_json=True) + return None \ No newline at end of file diff --git a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_suspended_frames.py b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_suspended_frames.py index 08c9bf3e3872..99e256fa6316 100644 --- a/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_suspended_frames.py +++ b/python/helpers/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_suspended_frames.py @@ -74,6 +74,10 @@ class _AbstractVariable(object): elif has_attribute_safe(self.value, '__len__') and not is_string(self.value): shape = str(len(self.value)) attributes.append(f"shape: {shape}") + + if has_attribute_safe(self.value, "dtype"): + dtype = str(self.value.dtype) + attributes.append(f"dtype: {dtype}") except: pass diff --git a/python/pydevSrc/src/com/jetbrains/python/debugger/PyDebugValue.java b/python/pydevSrc/src/com/jetbrains/python/debugger/PyDebugValue.java index c3cfba99da8d..41aeb4c75538 100644 --- a/python/pydevSrc/src/com/jetbrains/python/debugger/PyDebugValue.java +++ b/python/pydevSrc/src/com/jetbrains/python/debugger/PyDebugValue.java @@ -34,7 +34,7 @@ import java.util.regex.Pattern; import static com.jetbrains.python.debugger.PyDebugValueGroupsKt.*; -public class PyDebugValue extends XNamedValue { +public class PyDebugValue extends XNamedValue implements PyXDebugValue { protected static final Logger LOG = Logger.getInstance(PyDebugValue.class); private static final String ARRAY = "Array"; private static final String DATA_FRAME = "DataFrame"; @@ -107,12 +107,13 @@ public class PyDebugValue extends XNamedValue { @Nullable String typeQualifier, final @Nullable String value, @Nullable String shape, + @Nullable String arrayElementType, boolean isReturnedVal, boolean isIPythonHidden, boolean errorOnEval, @Nullable String typeRendererId, final @NotNull PyFrameAccessor frameAccessor) { - this(name, type, typeQualifier, value, EVALUATOR_POSTFIXES.containsKey(type), shape, isReturnedVal, isIPythonHidden, errorOnEval, typeRendererId, null, + this(name, type, typeQualifier, value, EVALUATOR_POSTFIXES.containsKey(type), shape, arrayElementType, isReturnedVal, isIPythonHidden, errorOnEval, typeRendererId, null, frameAccessor); } @@ -218,6 +219,7 @@ public class PyDebugValue extends XNamedValue { myTempName = tempName; } + @Override public @Nullable String getType() { return myType; } @@ -551,10 +553,14 @@ public class PyDebugValue extends XNamedValue { node.setFullValueEvaluator(new PyNumericContainerValueEvaluator(linkText, myFrameAccessor, treeName)); } - private static void addViewAsImageLink(XValueNodeEx valueNode) { + protected static void addViewAsImageLink(XValueNodeEx valueNode) { PyDebugValue debugValue = (PyDebugValue)valueNode.getXValue(); if (!checkAndShowViewAsImageOnScreen(debugValue)) return; + addViewAsImageHyperlink(valueNode); + } + + protected static void addViewAsImageHyperlink(XValueNodeEx valueNode) { String viewAsImageText = PydevBundle.message("pydev.view.as.image"); valueNode.addAdditionalHyperlink(new XDebuggerTreeNodeHyperlink(viewAsImageText) { @Override @@ -577,7 +583,7 @@ public class PyDebugValue extends XNamedValue { }); } - private static boolean checkAndShowViewAsImageOnScreen(PyDebugValue debugValue) { + protected static boolean checkAndShowViewAsImageOnScreen(PyDebugValue debugValue) { return Registry.is("actions.show.as.image.visibility", false) && !PluginManagerCore.isDisabled(PluginManagerCore.ULTIMATE_PLUGIN_ID) && checkAndEnableViewAsImageVisibility(debugValue); @@ -697,10 +703,12 @@ public class PyDebugValue extends XNamedValue { } } + @Override public @NotNull PyFrameAccessor getFrameAccessor() { return myFrameAccessor; } + @Override public void setFrameAccessor(@NotNull PyFrameAccessor frameAccessor) { myFrameAccessor = frameAccessor; } diff --git a/python/pydevSrc/src/com/jetbrains/python/debugger/PyXDebugValue.kt b/python/pydevSrc/src/com/jetbrains/python/debugger/PyXDebugValue.kt new file mode 100644 index 000000000000..c75df6d1ba29 --- /dev/null +++ b/python/pydevSrc/src/com/jetbrains/python/debugger/PyXDebugValue.kt @@ -0,0 +1,8 @@ +// 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; + +interface PyXDebugValue { + fun getName(): String + val type: String? + var frameAccessor: PyFrameAccessor +} diff --git a/python/src/com/jetbrains/python/debugger/PyDebuggerEvaluator.kt b/python/src/com/jetbrains/python/debugger/PyDebuggerEvaluator.kt index 400b1cdbd4f6..021e2898c13f 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebuggerEvaluator.kt +++ b/python/src/com/jetbrains/python/debugger/PyDebuggerEvaluator.kt @@ -39,7 +39,7 @@ internal class PyDebuggerEvaluator(private val myProject: Project, private val m // todo: think on getting results from EXEC val value = myDebugProcess.evaluate(expression, !isExpression, doTrunc) if (value.isErrorOnEval) { - callback.errorOccurred("{" + value.getType() + "}" + value.value) //NON-NLS + callback.errorOccurred("{" + value.type + "}" + value.value) //NON-NLS } else { callback.evaluated(value)